import { NextRequest } from 'next/server'; import { cookies } from 'next/headers'; import prisma from '@/lib/prisma'; export const USER_SESSION_COOKIE_NAME = 'afro_session'; export const DEFAULT_USER_SESSION_DURATION = 7 * 24 * 60 * 60; // 7 days in seconds export interface UserSessionPayload { userId: string; email: string; role: string; exp: number; iat: number; } export const USER_SAFE_SELECT = { id: true, name: true, email: true, role: true, avatar: true, bio: true, location: true, phone: true, countryId: true, isSuspended: true, suspensionReason: true, createdAt: true, updatedAt: true, emailVerified: true, }; function getJwtSecret(): string { const secret = process.env.JWT_SECRET || process.env.NEXTAUTH_SECRET || process.env.ADMIN_JWT_SECRET; if (!secret) { if (process.env.NODE_ENV === 'production') { throw new Error('CRITICAL: JWT_SECRET or NEXTAUTH_SECRET environment variable must be set in production.'); } return 'dev_only_afrohub_user_jwt_secret_key_change_in_prod'; } return secret; } function base64UrlEncode(str: string): string { return Buffer.from(str, 'utf-8').toString('base64url'); } function base64UrlDecode(str: string): string { return Buffer.from(str, 'base64url').toString('utf-8'); } async function getCryptoKey(): Promise { const enc = new TextEncoder(); return await crypto.subtle.importKey( 'raw', enc.encode(getJwtSecret()), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify'] ); } export async function signUserToken( payload: Omit, expiresInSeconds: number = DEFAULT_USER_SESSION_DURATION ): Promise { const now = Math.floor(Date.now() / 1000); const fullPayload: UserSessionPayload = { ...payload, iat: now, exp: now + expiresInSeconds, }; const header = { alg: 'HS256', typ: 'JWT' }; const headerB64 = base64UrlEncode(JSON.stringify(header)); const payloadB64 = base64UrlEncode(JSON.stringify(fullPayload)); const dataToSign = `${headerB64}.${payloadB64}`; const key = await getCryptoKey(); const signatureBuffer = await crypto.subtle.sign( 'HMAC', key, new TextEncoder().encode(dataToSign) ); const signatureB64 = Buffer.from(signatureBuffer).toString('base64url'); return `${dataToSign}.${signatureB64}`; } export async function verifyUserToken(token?: string | null): Promise { if (!token || typeof token !== 'string') return null; const parts = token.split('.'); if (parts.length !== 3) return null; const [headerB64, payloadB64, signatureB64] = parts; const dataToSign = `${headerB64}.${payloadB64}`; try { const key = await getCryptoKey(); const signatureBytes = Buffer.from(signatureB64, 'base64url'); const isValid = await crypto.subtle.verify( 'HMAC', key, signatureBytes, new TextEncoder().encode(dataToSign) ); if (!isValid) return null; const payloadJson = base64UrlDecode(payloadB64); const payload = JSON.parse(payloadJson) as UserSessionPayload; const now = Math.floor(Date.now() / 1000); if (!payload.exp || payload.exp < now || !payload.userId) { return null; } return payload; } catch { return null; } } export async function setUserSessionCookie( token: string, maxAgeSeconds: number = DEFAULT_USER_SESSION_DURATION ): Promise { const cookieStore = await cookies(); cookieStore.set(USER_SESSION_COOKIE_NAME, token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: maxAgeSeconds, }); } export async function clearUserSessionCookie(): Promise { const cookieStore = await cookies(); cookieStore.set(USER_SESSION_COOKIE_NAME, '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 0, }); } export async function getAuthUser(request?: NextRequest) { try { let token: string | undefined; if (request) { token = request.cookies.get(USER_SESSION_COOKIE_NAME)?.value; } if (!token) { const cookieStore = await cookies(); token = cookieStore.get(USER_SESSION_COOKIE_NAME)?.value; } const payload = await verifyUserToken(token); if (!payload?.userId) return null; const user = await prisma.user.findUnique({ where: { id: payload.userId }, select: { id: true, email: true, name: true, role: true, emailVerified: true, isSuspended: true, deletedAt: true, } }); if (!user || user.deletedAt) { return null; } return user; } catch (error) { console.error('Error fetching auth user:', error); return null; } } export async function isAdmin(request?: NextRequest) { const user = await getAuthUser(request); return user?.role === 'ADMIN' && !user.isSuspended; }