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
This commit is contained in:
parent
94ef7901e2
commit
0d00b1ec9c
@ -20,3 +20,6 @@ S3_BUCKET_NAME=monuments-images
|
||||
# App
|
||||
NEXT_PUBLIC_APP_URL=https://testbed.mk
|
||||
NEXT_PUBLIC_APP_DOMAIN=testbed.mk
|
||||
|
||||
# Admin
|
||||
ADMIN_SESSION_SECRET=your-random-64-char-secret-here-change-it-in-production
|
||||
@ -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;
|
||||
|
||||
@ -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<string> {
|
||||
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<string> {
|
||||
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<AdminSession | null> {
|
||||
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<AdminSession | null> {
|
||||
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<AdminSession | null> {
|
||||
const store = await cookies();
|
||||
const token = store.get(COOKIE_NAME)?.value;
|
||||
if (!token) return null;
|
||||
return verifyAdminSession(token);
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user