From 0d00b1ec9c07b842bfc47a58b5e15b5a07c61897 Mon Sep 17 00:00:00 2001 From: dimitar Date: Wed, 29 Jul 2026 19:07:43 +0200 Subject: [PATCH] fix(build): use Web Crypto API for Edge Runtime compatibility + final build verification - Rewrite admin-session.ts to use Web Crypto API (crypto.subtle) instead of Node.js crypto module, ensuring compatibility with Edge Runtime in middleware - Add ADMIN_SESSION_SECRET to .env.example - Build passes with zero warnings --- .env.example | 5 ++- src/app/api/admin/login/route.ts | 4 +-- src/lib/admin-session.ts | 53 ++++++++++++++++++++------------ src/middleware.ts | 2 +- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index 0a7c50d..4f7821c 100644 --- a/.env.example +++ b/.env.example @@ -19,4 +19,7 @@ S3_BUCKET_NAME=monuments-images # App NEXT_PUBLIC_APP_URL=https://testbed.mk -NEXT_PUBLIC_APP_DOMAIN=testbed.mk \ No newline at end of file +NEXT_PUBLIC_APP_DOMAIN=testbed.mk + +# Admin +ADMIN_SESSION_SECRET=your-random-64-char-secret-here-change-it-in-production \ No newline at end of file diff --git a/src/app/api/admin/login/route.ts b/src/app/api/admin/login/route.ts index eb7f070..89c36eb 100644 --- a/src/app/api/admin/login/route.ts +++ b/src/app/api/admin/login/route.ts @@ -14,7 +14,7 @@ export async function POST(req: NextRequest) { } if (username === SUPER_USERNAME && password === SUPER_PASSWORD) { - const session = createAdminSession({ username: SUPER_USERNAME, role: "SUPER_ADMIN" }); + const session = await createAdminSession({ username: SUPER_USERNAME, role: "SUPER_ADMIN" }); const res = NextResponse.json({ success: true }); res.cookies.set(cookieOptions(session)); return res; @@ -30,7 +30,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Невалидно корисничко име или лозинка" }, { status: 401 }); } - const session = createAdminSession({ username: admin.username, role: admin.role }); + const session = await createAdminSession({ username: admin.username, role: admin.role }); const res = NextResponse.json({ success: true }); res.cookies.set(cookieOptions(session)); return res; diff --git a/src/lib/admin-session.ts b/src/lib/admin-session.ts index 07d2e72..0d2b7a9 100644 --- a/src/lib/admin-session.ts +++ b/src/lib/admin-session.ts @@ -1,5 +1,4 @@ import { cookies } from "next/headers"; -import { createHmac, timingSafeEqual } from "crypto"; const COOKIE_NAME = "admin_session"; const SEP = "."; @@ -10,10 +9,17 @@ function getSecret(): string { return secret; } -function sign(payload: string): string { - const hmac = createHmac("sha256", getSecret()); - hmac.update(payload); - return hmac.digest("base64url"); +async function sign(payload: string): Promise { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(getSecret()), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(payload)); + return btoa(String.fromCharCode(...new Uint8Array(sig))); } export interface AdminSession { @@ -21,37 +27,37 @@ export interface AdminSession { role: "SUPER_ADMIN" | "ADMIN"; } -export function createAdminSession(data: AdminSession): string { - const payload = Buffer.from(JSON.stringify(data)).toString("base64url"); - return payload + SEP + sign(payload); +export async function createAdminSession(data: AdminSession): Promise { + const payload = btoa(JSON.stringify(data)); + return payload + SEP + (await sign(payload)); } -export function verifyAdminSession(token: string): AdminSession | null { +export async function verifyAdminSession(token: string): Promise { const sepIdx = token.lastIndexOf(SEP); if (sepIdx === -1) return null; const payload = token.slice(0, sepIdx); const sig = token.slice(sepIdx + 1); - const expectedSig = sign(payload); - const sigBuf = Buffer.from(sig); - const expectedBuf = Buffer.from(expectedSig); - if (sigBuf.length !== expectedBuf.length) return null; + const expectedSig = await sign(payload); + if (sig.length !== expectedSig.length) return null; try { - if (!timingSafeEqual(sigBuf, expectedBuf)) return null; + if (!constantTimeEqual(sig, expectedSig)) return null; } catch { return null; } try { - return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8")); + return JSON.parse(atob(payload)); } catch { return null; } } -export async function getAdminSession(): Promise { - const store = await cookies(); - const token = store.get(COOKIE_NAME)?.value; - if (!token) return null; - return verifyAdminSession(token); +function constantTimeEqual(a: string, b: string): boolean { + if (a.length !== b.length) return false; + let result = 0; + for (let i = 0; i < a.length; i++) { + result |= a.charCodeAt(i) ^ b.charCodeAt(i); + } + return result === 0; } export const COOKIE_NAME_ADMIN = COOKIE_NAME; @@ -66,3 +72,10 @@ export function cookieOptions(value: string) { path: "/admin", }; } + +export async function getAdminSession(): Promise { + const store = await cookies(); + const token = store.get(COOKIE_NAME)?.value; + if (!token) return null; + return verifyAdminSession(token); +} diff --git a/src/middleware.ts b/src/middleware.ts index 2c64e73..9545857 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -36,7 +36,7 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { } const token = req.cookies.get(COOKIE_NAME_ADMIN)?.value; - if (!token || !verifyAdminSession(token)) { + if (!token || !(await verifyAdminSession(token))) { if (req.nextUrl.pathname.startsWith("/api/")) { return NextResponse.json({ error: "Неавторизирано" }, { status: 401 }); }