Skip to content

Commit 0807c53

Browse files
hh
1 parent 6d0d9b8 commit 0807c53

7 files changed

Lines changed: 87 additions & 96 deletions

File tree

components/Yugioh/Card.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const Card = ( { cardData } ) => {
3535
<Image
3636
as="image"
3737
unoptimized="true"
38-
className="lg:object-cover object-scale-down object-center w-full mx-auto h-96 aspect-1"
38+
className="lg:object-contain object-scale-down object-top w-full mx-auto h-96 aspect-1"
3939
src={ imageSrc }
4040
alt={ `Card Image - ${ productName }` }
4141
width={ 1600 }

middleware.ts

Lines changed: 10 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
11
import type { NextRequest } from "next/server";
22
import { NextResponse } from "next/server";
3-
import jwt from "jsonwebtoken";
43

54
const LOGIN_ROUTE = "/login";
65
const PROTECTED_MATCHERS = [
76
"/yugioh/my-collection",
87
"/yugioh/test-page",
98
];
109
const AUTH_STATE_COOKIE = "auth_state";
11-
const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24;
1210
const isProduction = process.env.NODE_ENV === "production";
1311

14-
const setAuthenticatedCookie = ( response: NextResponse ) => {
15-
response.cookies.set( {
16-
name: AUTH_STATE_COOKIE,
17-
value: "1",
18-
maxAge: AUTH_COOKIE_MAX_AGE,
19-
sameSite: "strict",
20-
path: "/",
21-
secure: isProduction,
22-
httpOnly: false,
23-
} );
24-
};
25-
2612
const clearAuthenticatedCookie = ( response: NextResponse ) => {
2713
response.cookies.set( {
2814
name: AUTH_STATE_COOKIE,
@@ -43,21 +29,10 @@ const isProtectedPath = ( pathname: string ): boolean =>
4329
const collectReturnPath = ( request: NextRequest ): string => {
4430
const target = request.nextUrl.pathname + request.nextUrl.search;
4531
return target === LOGIN_ROUTE ? "/" : target;
46-
4732
};
4833

49-
const getToken = ( request: NextRequest ): string | null => {
50-
const bearer = request.headers.get( "authorization" );
51-
if ( bearer?.toLowerCase().startsWith( "bearer " ) ) {
52-
const token = bearer.slice( 7 ).trim();
53-
if ( token ) {
54-
return token;
55-
}
56-
}
57-
58-
const cookieToken = request.cookies.get( "token" );
59-
return cookieToken?.value ?? null;
60-
};
34+
const hasAuthenticatedCookie = ( request: NextRequest ): boolean =>
35+
request.cookies.get( AUTH_STATE_COOKIE )?.value === "1";
6136

6237
const PUBLIC_GET_API_PREFIXES = [
6338
"/api/Yugioh/cards/[setNameId]",
@@ -106,51 +81,29 @@ const handleUnauthorized = ( request: NextRequest ) => {
10681

10782
export function middleware( request: NextRequest ) {
10883
const { pathname } = request.nextUrl;
84+
const isApiPath = pathname.startsWith( "/api/Yugioh/" );
10985

110-
if ( !isProtectedPath( pathname ) && !pathname.startsWith( "/api/Yugioh/" ) ) {
86+
if ( !isProtectedPath( pathname ) && !isApiPath ) {
11187
return NextResponse.next();
11288
}
11389

11490
if ( request.method === "OPTIONS" ) {
11591
return NextResponse.next();
11692
}
11793

118-
if ( pathname.startsWith( "/api/Yugioh/" ) && isPublicApiRequest( request ) ) {
94+
if ( isApiPath && isPublicApiRequest( request ) ) {
11995
return NextResponse.next();
12096
}
12197

122-
const token = getToken( request );
123-
if ( !token ) {
124-
return handleUnauthorized( request );
98+
if ( isApiPath ) {
99+
return NextResponse.next();
125100
}
126101

127-
const secret = process.env.JWT_SECRET;
128-
if ( !secret ) {
129-
console.error( "JWT_SECRET is not configured" );
130-
return handleUnauthorized( request );
102+
if ( hasAuthenticatedCookie( request ) ) {
103+
return NextResponse.next();
131104
}
132105

133-
try {
134-
const decoded = jwt.verify( token, secret );
135-
const payload =
136-
typeof decoded === "object" && decoded !== null
137-
? decoded
138-
: { sub: decoded };
139-
140-
const headers = new Headers( request.headers );
141-
headers.set( "x-authenticated-user", JSON.stringify( payload ) );
142-
143-
const response = NextResponse.next( {
144-
request: {
145-
headers,
146-
},
147-
} );
148-
setAuthenticatedCookie( response );
149-
return response;
150-
} catch ( error ) {
151-
console.error( "Failed to verify token in middleware:", error );
152-
return handleUnauthorized( request );
153-
}
106+
return handleUnauthorized( request );
154107
}
155108

156109
export const config = {

middleware/authenticate.js

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@ import jwt from "jsonwebtoken";
22
import clientPromise from "@/utils/mongo";
33
import { ObjectId } from "mongodb";
44

5+
const isProduction = process.env.NODE_ENV === "production";
6+
const baseCookieAttributes = [ "Path=/", "SameSite=Strict" ];
7+
if ( isProduction ) {
8+
baseCookieAttributes.push( "Secure" );
9+
}
10+
const expiredCookieAttributes = [ ...baseCookieAttributes, "Max-Age=0" ];
11+
const EXPIRED_TOKEN_COOKIE = `token=; HttpOnly; ${ expiredCookieAttributes.join( "; " ) }`;
12+
const EXPIRED_AUTH_STATE_COOKIE = `auth_state=; ${ expiredCookieAttributes.join( "; " ) }`;
13+
14+
function appendSetCookie( res, cookies ) {
15+
const existing = res.getHeader( "Set-Cookie" );
16+
17+
if ( !existing ) {
18+
res.setHeader( "Set-Cookie", cookies );
19+
return;
20+
}
21+
22+
if ( Array.isArray( existing ) ) {
23+
res.setHeader( "Set-Cookie", [ ...existing, ...cookies ] );
24+
return;
25+
}
26+
27+
res.setHeader( "Set-Cookie", [ existing, ...cookies ] );
28+
}
29+
30+
function clearAuthCookies( res ) {
31+
appendSetCookie( res, [ EXPIRED_TOKEN_COOKIE, EXPIRED_AUTH_STATE_COOKIE ] );
32+
}
33+
34+
function respondUnauthorized( res, message ) {
35+
clearAuthCookies( res );
36+
res.status( 401 ).json( { error: message } );
37+
return null;
38+
}
39+
540
function extractBearerToken( headerValue ) {
641
if ( !headerValue || typeof headerValue !== "string" ) {
742
return null;
@@ -64,8 +99,7 @@ export async function requireUser(
6499
const token = getTokenFromRequest( req );
65100

66101
if ( !token && !middlewareUser ) {
67-
res.status( 401 ).json( { error: "Unauthorized: No token provided" } );
68-
return null;
102+
return respondUnauthorized( res, "Unauthorized: No token provided" );
69103
}
70104

71105
let decoded = middlewareUser;
@@ -75,19 +109,16 @@ export async function requireUser(
75109
decoded = jwt.verify( token, process.env.JWT_SECRET );
76110
} catch ( error ) {
77111
console.error( "Invalid token:", error );
78-
res.status( 401 ).json( { error: "Unauthorized: Invalid token" } );
79-
return null;
112+
return respondUnauthorized( res, "Unauthorized: Invalid token" );
80113
}
81114
}
82115

83116
if ( !decoded ) {
84-
res.status( 401 ).json( { error: "Unauthorized: Invalid token payload" } );
85-
return null;
117+
return respondUnauthorized( res, "Unauthorized: Invalid token payload" );
86118
}
87119

88120
if ( !decoded?.userId || !decoded?.username ) {
89-
res.status( 401 ).json( { error: "Unauthorized: Invalid token payload" } );
90-
return null;
121+
return respondUnauthorized( res, "Unauthorized: Invalid token payload" );
91122
}
92123

93124
req.user = decoded;
@@ -104,8 +135,7 @@ export async function requireUser(
104135
.findOne( { _id: new ObjectId( decoded.userId ) } );
105136

106137
if ( !user ) {
107-
res.status( 401 ).json( { error: "Unauthorized: User not found" } );
108-
return null;
138+
return respondUnauthorized( res, "Unauthorized: User not found" );
109139
}
110140

111141
return { token, decoded, user };

pages/yugioh/sets/[letter]/[setName].js

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import fs from "fs/promises";
22
import path from "path";
3-
import { useEffect, useState, useMemo, useCallback, Suspense } from "react";
3+
import { useEffect, useState, useMemo, useCallback } from "react";
44
import { useRouter } from "next/router";
55
import Head from "next/head";
66
import Link from "next/link";
7+
import dynamic from "next/dynamic";
78
import { Filter, Grid, List } from "lucide-react";
89
import Breadcrumb from "@/components/Navigation/Breadcrumb";
910
import CardFilter from "@/components/Yugioh/CardFilter";
1011
import FilterPanel from "@/components/Yugioh/FilterPanel";
1112
import YugiohSearchBar from "@/components/Yugioh/YugiohSearchBar";
12-
import YugiohCardDataTable from "@/components/Yugioh/YugiohCardDataTable";
1313
import YugiohPagination from "@/components/Yugioh/YugiohPagination";
1414
import Notification from "@/components/Notification";
1515
import { fetchCardData as fetchAllCardData } from "@/utils/api";
1616
import { SpeedInsights } from "@vercel/speed-insights/next";
1717
import { buildCollectionKey, buildCollectionMap } from "@/utils/collectionUtils.js";
1818
import { readAuthStateFromCookie, subscribeToAuthState, dispatchAuthStateChange } from "@/utils/authState";
1919

20+
const YugiohCardDataTable = dynamic(
21+
() => import( "@/components/Yugioh/YugiohCardDataTable" ),
22+
{
23+
ssr: false,
24+
loading: () => (
25+
<div className="py-10 text-center text-white/70">Loading...</div>
26+
),
27+
}
28+
);
29+
2030
const CARD_SETS_FILE_PATH = path.join(
2131
process.cwd(),
2232
"public",
@@ -1526,7 +1536,7 @@ const CardsInSetPage = ( { initialSetName = "", setNameId = null, letter = "" }
15261536
const activeVariant = cardItem.activeVariant || null;
15271537
const overlayLabel = activeVariant ? buildVariantLabel( activeVariant ) : currentRarityLabel;
15281538
const cardContainerClasses = [
1529-
"relative min-h-[24rem] w-full max-w-[350px] mx-auto object-cover overflow-hidden rounded-xl border border-white/10 bg-black/40 shadow-lg transition duration-200 group-hover:border-indigo-400/60 dark:border-white/20 dark:bg-gray-900/60",
1539+
"relative min-h-[24rem] w-full max-w-[350px] mx-auto object-cover overflow-hidden rounded-sm border border-white/10 bg-black/40 shadow-lg transition duration-200 group-hover:border-indigo-400/60 dark:border-white/20 dark:bg-gray-900/60",
15301540
isSelected ? "ring-2 ring-indigo-400/70" : "",
15311541
].filter( Boolean ).join( " " );
15321542
const isFlipped = Boolean( flippedGridCards[ selectionKey ] );
@@ -1613,7 +1623,7 @@ const CardsInSetPage = ( { initialSetName = "", setNameId = null, letter = "" }
16131623
<div className="size-full max-w-fit mx-auto -inset-1">
16141624
{ hasImage ? (
16151625
<img
1616-
className="object-scale-down sm:object-cover object-center w-full mx-auto aspect-square h-full"
1626+
className="object-scale-down sm:object-cover object-center w-full mx-auto aspect-1 h-full"
16171627
src={ imageSrc }
16181628
alt={ `Card Image - ${ cardItem.productName }` }
16191629
loading="lazy"
@@ -2065,17 +2075,15 @@ const CardsInSetPage = ( { initialSetName = "", setNameId = null, letter = "" }
20652075
</>
20662076
) : (
20672077
<div className="overflow-hidden rounded-sm border border-white/10 bg-black/40 p-4 shadow-2xl">
2068-
<Suspense fallback={ <div className="py-10 text-center text-white/70">Loading...</div> }>
2069-
<YugiohCardDataTable
2070-
matchedCardData={ matchedCardData }
2071-
selectedRowIds={ selectedRowIds }
2072-
setSelectedRowIds={ setSelectedRowIds }
2073-
collectionMap={ collectionLookup }
2074-
onRarityChange={ handleRarityOverrideChange }
2075-
autoRarityOptionValue={ AUTO_RARITY_OPTION }
2076-
isAuthenticated={ isAuthenticated }
2077-
/>
2078-
</Suspense>
2078+
<YugiohCardDataTable
2079+
matchedCardData={ matchedCardData }
2080+
selectedRowIds={ selectedRowIds }
2081+
setSelectedRowIds={ setSelectedRowIds }
2082+
collectionMap={ collectionLookup }
2083+
onRarityChange={ handleRarityOverrideChange }
2084+
autoRarityOptionValue={ AUTO_RARITY_OPTION }
2085+
isAuthenticated={ isAuthenticated }
2086+
/>
20792087
</div>
20802088
) }
20812089
{ isAuthenticated &&

styles/globals.css

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ img:hover
131131
.navLink:focus-visible
132132
{
133133
outline: 2px solid rgba(99, 102, 241, 0.45);
134-
outline-offset: 4px;
134+
outline-offset: 2px;
135135
}
136136

137137
.navLink:hover,
@@ -214,7 +214,7 @@ img:hover
214214
line-height: 1.32;
215215
color: inherit;
216216
letter-spacing: 0.04em;
217-
filter: drop-shadow(0 0 6px rgba(99, 102, 241, 0.12));
217+
filter: drop-shadow(0 0 2px rgba(99, 102, 241, 0.12));
218218
overflow-wrap: anywhere;
219219
transition: color var(--transition-slow), filter 320ms ease;
220220
}
@@ -279,15 +279,15 @@ img:hover
279279
.navLink.navLink-active .navLink-labelText
280280
{
281281
color: var(--text-secondary);
282-
filter: drop-shadow(0 0 14px rgba(99, 102, 241, 0.4));
282+
filter: drop-shadow(0 0 5px rgba(99, 102, 241, 0.4));
283283
}
284284

285285
.navLink:hover .navLink-labelText::before,
286286
.navLink:focus-visible .navLink-labelText::before,
287287
.navLink.navLink-active .navLink-labelText::before
288288
{
289289
-webkit-text-stroke-color: rgba(148, 163, 184, 0.85);
290-
filter: drop-shadow(0 0 22px rgba(99, 102, 241, 0.5));
290+
filter: drop-shadow(0 0 2px rgba(99, 102, 241, 0.5));
291291
opacity: 1;
292292
}
293293

@@ -296,7 +296,7 @@ img:hover
296296
.navLink.navLink-active .navLink-labelText::after
297297
{
298298
-webkit-text-stroke-color: rgba(99, 102, 241, 0.48);
299-
filter: blur(22px);
299+
filter: blur(2px);
300300
opacity: 0.95;
301301
}
302302

@@ -428,4 +428,4 @@ img:hover
428428
box-shadow: var(--shadow-soft);
429429
backdrop-filter: saturate(140%) blur(18px);
430430
}
431-
}
431+
}

styles/gridcards.css

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
.card
1212
{
1313
width: min(var(--card-width), 100%);
14-
height: 95%;
14+
height: 100%;
1515
position: relative;
16-
display: inline-flex;
16+
display: flex;
1717
flex-direction: column;
18-
justify-content: center;
19-
align-items: center;
18+
justify-content: top;
19+
align-items: top;
2020
perspective: 550px;
21-
margin: 0.5rem;
22-
padding: 0.75rem;
21+
margin: 2%;
22+
padding: 1%;
2323
}
2424

2525
.cover-image

styles/hovercards.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
position: absolute;
3030
inset: 0;
3131
backface-visibility: hidden;
32-
border-radius: 30%;
32+
border-radius: 2%;
3333
overflow: hidden;
3434
display: flex;
3535
flex-direction: column;

0 commit comments

Comments
 (0)