Skip to content

Commit db9850f

Browse files
hh
1 parent 2049238 commit db9850f

14 files changed

Lines changed: 390 additions & 193 deletions

File tree

components/Layout.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ export default function Layout( { children } ) {
6060
<Link href="/" className="flex-1 text-sm font-semibold leading-6 text-shadow text-white">
6161
CARD PRICE APP
6262
</Link>
63-
<div className=" flex items-center">
64-
<span className="">User profile placeholder</span>
63+
<div className="flex items-center">
64+
<span className="rounded-full border border-dashed border-white py-1 px-1 font-black text-shadow text-sm">PIC</span>
6565
</div>
6666
</div>
6767
<main className="mx-auto">
68-
<div className="h-full mx-1 my-2 ">
68+
<div className="min-h-screen p-3 w-full max-w-screen-2xl">
6969
{ children }
7070
</div>
7171
</main>

components/Navigation/SideNav.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ export default function SideNav() {
133133
<Image
134134
src={ item.icon }
135135
alt={ `${ item.label } icon` }
136-
width={ 24 }
137-
height={ 24 }
136+
width={ 32 }
137+
height={ 32 }
138138
className="navLink-iconImage object-center object-cover"
139139
/>
140140
</span>
@@ -149,7 +149,7 @@ export default function SideNav() {
149149
{ isAuthenticated ? (
150150
<button
151151
onClick={ handleLogout }
152-
className="mx-auto w-3/4 block text-nowrap px-16 py-3 rounded-lg border border-white bg-red-500/20 text-center text-sm font-semibold tracking-wide text-red-100 transition hover:bg-red-500/30"
152+
className="mx-auto w-4/5 block text-nowrap px-5 py-3 rounded-lg border border-white bg-red-500/20 text-center text-sm font-semibold tracking-wide text-red-100 transition hover:bg-red-500/30"
153153
title="Log out"
154154
>
155155
Log Out
@@ -159,7 +159,7 @@ export default function SideNav() {
159159
<Link
160160
href="/login"
161161
title="Log in"
162-
className="mx-auto w-3/4 text-nowrap block px-16 py-3 rounded-lg border border-white bg-white/5 text-center text-sm font-semibold tracking-wide text-white/90 transition hover:bg-white/10"
162+
className="mx-auto w-4/5 text-nowrap block px-5 py-3 rounded-lg border border-white bg-white/5 text-center text-sm font-semibold tracking-wide text-white/90 transition hover:bg-white/10"
163163
>
164164
Log In
165165
</Link>

middleware.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ const getToken = ( request: NextRequest ): string | null => {
5858
return cookieToken?.value ?? null;
5959
};
6060

61+
const PUBLIC_GET_API_PREFIXES = [
62+
"/api/Yugioh/cards",
63+
"/api/Yugioh/setNameIdMap",
64+
];
65+
66+
const isPublicApiRequest = ( request: NextRequest ): boolean => {
67+
if ( request.method !== "GET" ) {
68+
return false;
69+
}
70+
71+
const pathname = request.nextUrl.pathname;
72+
return PUBLIC_GET_API_PREFIXES.some( ( prefix ) =>
73+
pathname === prefix || pathname.startsWith( `${ prefix }/` )
74+
);
75+
};
76+
6177
const handleUnauthorized = ( request: NextRequest ) => {
6278
if ( request.nextUrl.pathname.startsWith( "/api/" ) ) {
6379
const response = NextResponse.json(
@@ -90,6 +106,10 @@ export function middleware( request: NextRequest ) {
90106
return NextResponse.next();
91107
}
92108

109+
if ( pathname.startsWith( "/api/Yugioh" ) && isPublicApiRequest( request ) ) {
110+
return NextResponse.next();
111+
}
112+
93113
const token = getToken( request );
94114
if ( !token ) {
95115
return handleUnauthorized( request );

pages/api/Yugioh/cards.js

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import clientPromise from "@/utils/mongo.js";
22
import { requireUser } from "@/middleware/authenticate";
3+
import { ensureSafeUserId, coerceNumberField, coerceStringField } from "@/utils/securityValidators.js";
34

45
export default async function handler( req, res ) {
56
if ( req.method !== "POST" ) {
@@ -21,26 +22,46 @@ export default async function handler( req, res ) {
2122
const client = await clientPromise;
2223
const db = client.db( "cardPriceApp" );
2324
const collection = db.collection( "myCollection" );
25+
const safeUserId = ensureSafeUserId( auth.decoded.username );
2426

25-
const sanitizedCards = cards
26-
.filter( ( card ) => card && typeof card === "object" )
27-
.map( ( card ) => {
28-
const rawCardId = card?.cardId;
29-
const normalizedCardId =
30-
rawCardId === null || rawCardId === undefined ? null : String( rawCardId );
31-
return {
32-
...card,
33-
cardId: normalizedCardId,
27+
const sanitizedCards = [];
28+
29+
for ( const card of cards ) {
30+
if ( !card || typeof card !== "object" ) {
31+
continue;
32+
}
33+
34+
try {
35+
const sanitizedCard = {
36+
productName: coerceStringField( card.productName, { maxLength: 256 } ),
37+
setName: coerceStringField( card.setName, { maxLength: 256 } ),
38+
number: coerceStringField( card.number ?? "", { maxLength: 128, allowEmpty: true } ),
39+
printing: coerceStringField( card.printing ?? "", { maxLength: 128, allowEmpty: true } ),
40+
rarity: coerceStringField( card.rarity ?? "", { maxLength: 128, allowEmpty: true } ),
41+
condition: coerceStringField( card.condition ?? "", { maxLength: 128, allowEmpty: true } ),
42+
marketPrice: coerceNumberField( card.marketPrice ?? 0 ),
43+
lowPrice: coerceNumberField( card.lowPrice ?? 0 ),
44+
quantity: coerceNumberField( card.quantity ?? 1, { min: 0 } ),
45+
cardId:
46+
card.cardId === null || card.cardId === undefined
47+
? null
48+
: coerceStringField( card.cardId, { maxLength: 128 } ),
3449
};
35-
} );
50+
51+
sanitizedCards.push( sanitizedCard );
52+
} catch ( error ) {
53+
console.warn( "Skipping invalid card payload:", error?.message ?? error );
54+
}
55+
}
56+
3657
if ( sanitizedCards.length === 0 ) {
3758
return res.status( 400 ).json( { error: "No valid card data provided." } );
3859
}
3960

4061
const bulkOps = sanitizedCards.map( ( card ) => ( {
4162
updateOne: {
4263
filter: {
43-
userId: auth.decoded.username,
64+
userId: safeUserId,
4465
productName: card.productName,
4566
setName: card.setName,
4667
number: card.number,
@@ -49,15 +70,15 @@ export default async function handler( req, res ) {
4970
condition: card.condition
5071
},
5172
update: {
52-
$inc: { quantity: card.quantity || 1 },
73+
$inc: { quantity: card.quantity },
5374
$set: {
5475
oldPrice: null,
5576
cardId: card.cardId || null,
5677
},
5778
$setOnInsert: {
5879
marketPrice: card.marketPrice || 0,
5980
lowPrice: card.lowPrice || 0,
60-
userId: auth.decoded.username
81+
userId: safeUserId
6182
}
6283
},
6384
upsert: true

pages/api/Yugioh/deleteAllCards.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { MongoClient } from "mongodb";
21
import { requireUser } from "@/middleware/authenticate";
2+
import clientPromise from "@/utils/mongo.js";
3+
import { ensureSafeUserId } from "@/utils/securityValidators.js";
34

45
export default async function handler( req, res ) {
56
if ( req.method !== "DELETE" ) {
@@ -12,14 +13,13 @@ export default async function handler( req, res ) {
1213
return;
1314
}
1415

15-
const client = new MongoClient( process.env.MONGODB_URI );
16-
1716
try {
18-
await client.connect();
17+
const client = await clientPromise;
1918
const db = client.db( "cardPriceApp" );
2019
const collection = db.collection( "myCollection" );
20+
const safeUserId = ensureSafeUserId( auth.decoded.username );
2121

22-
const result = await collection.deleteMany( { userId: auth.decoded.username } );
22+
const result = await collection.deleteMany( { userId: safeUserId } );
2323

2424
if ( result.deletedCount > 0 ) {
2525
return res.status( 200 ).json( { message: "All cards deleted successfully" } );
@@ -29,7 +29,5 @@ export default async function handler( req, res ) {
2929
} catch ( error ) {
3030
console.error( "Delete error:", error );
3131
return res.status( 500 ).json( { message: `Internal server error: ${ error.message }` } );
32-
} finally {
33-
await client.close();
3432
}
3533
}

pages/api/Yugioh/deleteCards.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { MongoClient, ObjectId } from "mongodb";
1+
import { ObjectId } from "mongodb";
22
import { requireUser } from "@/middleware/authenticate";
3+
import clientPromise from "@/utils/mongo.js";
4+
import { ensureSafeUserId } from "@/utils/securityValidators.js";
35

46
export default async function handler( req, res ) {
57
if ( req.method !== "DELETE" ) {
@@ -18,16 +20,15 @@ export default async function handler( req, res ) {
1820
return res.status( 400 ).json( { message: "Invalid card identifier" } );
1921
}
2022

21-
const client = new MongoClient( process.env.MONGODB_URI );
22-
2323
try {
24-
await client.connect();
24+
const client = await clientPromise;
2525
const db = client.db( "cardPriceApp" );
2626
const cards = db.collection( "myCollection" );
27+
const safeUserId = ensureSafeUserId( auth.decoded.username );
2728

2829
const result = await cards.deleteOne( {
2930
_id: new ObjectId( cardId ),
30-
userId: auth.decoded.username
31+
userId: safeUserId
3132
} );
3233

3334
if ( result.deletedCount >= 1 ) {
@@ -38,7 +39,5 @@ export default async function handler( req, res ) {
3839
} catch ( error ) {
3940
console.error( "Delete error:", error );
4041
return res.status( 500 ).json( { message: `Internal server error: ${ error.message }` } );
41-
} finally {
42-
await client.close();
4342
}
4443
}

pages/api/Yugioh/my-collection.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { MongoClient } from "mongodb";
21
import { requireUser } from "@/middleware/authenticate";
2+
import clientPromise from "@/utils/mongo.js";
3+
import { ensureSafeUserId } from "@/utils/securityValidators.js";
34

45
export default async function handler( req, res ) {
56
const auth = await requireUser( req, res );
67
if ( !auth ) {
78
return;
89
}
910

10-
const userId = auth.decoded.username; // keep using username in the myCollection userId field
11-
const client = new MongoClient( process.env.MONGODB_URI );
11+
const userId = ensureSafeUserId( auth.decoded.username );
12+
const client = await clientPromise;
1213

1314
try {
14-
await client.connect();
1515
const collection = client.db( "cardPriceApp" ).collection( "myCollection" );
1616

1717
switch ( req.method ) {
@@ -48,7 +48,5 @@ export default async function handler( req, res ) {
4848
} catch ( error ) {
4949
console.error( "Error executing aggregation query:", error );
5050
return res.status( 500 ).json( { message: "Server error" } );
51-
} finally {
52-
await client.close();
5351
}
5452
}

pages/api/Yugioh/updateCards.js

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { MongoClient, ObjectId } from "mongodb";
1+
import { ObjectId } from "mongodb";
22
import { requireUser } from "@/middleware/authenticate";
3+
import clientPromise from "@/utils/mongo.js";
4+
import { ensureSafeUserId, coerceNumberField, coerceStringField } from "@/utils/securityValidators.js";
35

46
const ALLOWED_FIELDS = new Set( [
57
"quantity",
@@ -43,30 +45,24 @@ export default async function handler( req, res ) {
4345
return res.status( 400 ).json( { message: "Missing update value" } );
4446
}
4547

46-
let sanitizedValue = value;
47-
48-
if ( NUMERIC_FIELDS.has( field ) ) {
49-
const numericValue = Number( value );
50-
if ( Number.isNaN( numericValue ) || !Number.isFinite( numericValue ) ) {
51-
return res.status( 400 ).json( { message: "Invalid numeric value" } );
52-
}
53-
54-
if ( field === "quantity" && numericValue < 0 ) {
55-
return res.status( 400 ).json( { message: "Quantity cannot be negative" } );
48+
try {
49+
let sanitizedValue;
50+
51+
if ( NUMERIC_FIELDS.has( field ) ) {
52+
sanitizedValue = coerceNumberField( value, {
53+
min: field === "quantity" ? 0 : Number.NEGATIVE_INFINITY,
54+
} );
55+
} else {
56+
sanitizedValue = coerceStringField( value, { maxLength: 256, allowEmpty: true } );
5657
}
5758

58-
sanitizedValue = numericValue;
59-
}
60-
61-
const client = new MongoClient( process.env.MONGODB_URI );
62-
63-
try {
64-
await client.connect();
59+
const client = await clientPromise;
6560
const db = client.db( "cardPriceApp" );
6661
const cards = db.collection( "myCollection" );
62+
const safeUserId = ensureSafeUserId( auth.decoded.username );
6763

6864
const result = await cards.updateOne(
69-
{ _id: new ObjectId( cardId ), userId: auth.decoded.username },
65+
{ _id: new ObjectId( cardId ), userId: safeUserId },
7066
{ $set: { [ field ]: sanitizedValue } }
7167
);
7268

@@ -80,7 +76,5 @@ export default async function handler( req, res ) {
8076
} catch ( error ) {
8177
console.error( "Update error:", error );
8278
return res.status( 500 ).json( { message: `Internal server error: ${ error.message }` } );
83-
} finally {
84-
await client.close();
8579
}
8680
}

pages/login.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@ const resolveRedirectPath = ( queryParam ) => {
1010
return DEFAULT_REDIRECT_PATH;
1111
}
1212

13-
if ( !queryParam.startsWith( "/" ) || queryParam.startsWith( "//" ) ) {
14-
return DEFAULT_REDIRECT_PATH;
15-
}
13+
try {
14+
const candidate = new URL( queryParam, "http://localhost" );
15+
16+
if ( candidate.origin !== "http://localhost" ) {
17+
return DEFAULT_REDIRECT_PATH;
18+
}
1619

17-
const sanitizedPath = queryParam.trim();
20+
if ( candidate.pathname.startsWith( "/api" ) ) {
21+
return DEFAULT_REDIRECT_PATH;
22+
}
1823

19-
if (
20-
sanitizedPath.startsWith( "/api/" ) ||
21-
sanitizedPath === "/api"
22-
) {
24+
return `${ candidate.pathname }${ candidate.search }${ candidate.hash }` || DEFAULT_REDIRECT_PATH;
25+
} catch {
2326
return DEFAULT_REDIRECT_PATH;
2427
}
25-
26-
return sanitizedPath;
2728
};
2829

2930
export default function LoginPage() {

0 commit comments

Comments
 (0)