import crypto from "node:crypto"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/lib/auth/server"; import { db, apiKeys, users, eq } from "@epicure/db"; import { applyRateLimit } from "@/lib/rate-limit"; export async function requireSession() { const session = await auth.api.getSession({ headers: await headers() }); if (!session) { return { session: null, response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }) }; } return { session, response: null }; } export async function requireAdmin() { const { session, response } = await requireSession(); if (response) return { session: null, response }; // Don't trust session.user.role — it comes from a 5-minute cookieCache // (see lib/auth/server.ts), so a just-demoted admin would keep access for // up to 5 minutes. Query the current role directly. const [dbUser] = await db .select({ role: users.role }) .from(users) .where(eq(users.id, session!.user.id)) .limit(1); if (dbUser?.role !== "admin") { return { session: null, response: NextResponse.json({ error: "Forbidden" }, { status: 403 }) }; } return { session, response: null }; } type SessionLike = { user: { id: string; email: string; name: string; tier: string; role?: string; image?: string | null; }; }; type RateLimitOpts = { limit: number; windowSeconds: number }; export async function requireSessionOrApiKey( req: NextRequest, opts?: { rateLimit?: RateLimitOpts } ): Promise<{ session: SessionLike; response: null } | { session: null; response: NextResponse }> { // 1. Try Bearer API key const authHeader = req.headers.get("authorization"); if (authHeader?.startsWith("Bearer ")) { const rawKey = authHeader.slice(7).trim(); if (rawKey.startsWith("ek_")) { const keyHash = crypto.createHash("sha256").update(rawKey).digest("hex"); const [keyRow] = await db .select({ id: apiKeys.id, userId: apiKeys.userId }) .from(apiKeys) .where(eq(apiKeys.keyHash, keyHash)) .limit(1); if (keyRow) { // Update lastUsedAt asynchronously — don't block response void db .update(apiKeys) .set({ lastUsedAt: new Date() }) .where(eq(apiKeys.id, keyRow.id)) .catch((err) => console.error("[api-auth] failed to update apiKeys.lastUsedAt", err)); const [user] = await db .select({ id: users.id, email: users.email, name: users.name, tier: users.tier, role: users.role, }) .from(users) .where(eq(users.id, keyRow.userId)) .limit(1); if (user) { // Rate limit per API key (not per user — a user's other keys shouldn't // share this bucket). if (opts?.rateLimit) { const { limit, windowSeconds } = opts.rateLimit; const rateLimitResponse = await applyRateLimit( `rl:api:key:${keyRow.id}`, limit, windowSeconds ); if (rateLimitResponse) { return { session: null, response: rateLimitResponse }; } } return { session: { user: { ...user, image: null } }, response: null, }; } } return { session: null, response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }), }; } } // 2. Fall back to session cookie const result = await requireSession(); if (result.response) return result; if (opts?.rateLimit) { const { limit, windowSeconds } = opts.rateLimit; const rateLimitResponse = await applyRateLimit( `rl:api:session:${result.session!.user.id}`, limit, windowSeconds ); if (rateLimitResponse) { return { session: null, response: rateLimitResponse }; } } return result; }