From f03e7aaf193f6cb73fd69bf28bfe5030cad2f800 Mon Sep 17 00:00:00 2001 From: belisards Date: Sun, 3 May 2026 16:19:14 -0300 Subject: [PATCH] feat(auth): add token verification and cookie helpers --- lib/auth/cookies.ts | 39 ++++++++++++++++++++++++++++++++++ lib/auth/tokens.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 lib/auth/cookies.ts create mode 100644 lib/auth/tokens.ts diff --git a/lib/auth/cookies.ts b/lib/auth/cookies.ts new file mode 100644 index 0000000..c31aaf5 --- /dev/null +++ b/lib/auth/cookies.ts @@ -0,0 +1,39 @@ +export const ADM_COOKIE = 'adm_token'; +export const USR_COOKIE = 'usr_token'; + +export function isSecureCookie(request: { headers: { get(name: string): string | null }; url: string }): boolean { + if (process.env.COOKIE_SECURE !== undefined) { + return process.env.COOKIE_SECURE === 'true'; + } + if (process.env.NODE_ENV === 'production') { + return ( + request.headers.get('x-forwarded-proto') === 'https' || + request.url.startsWith('https://') + ); + } + return false; +} + +export function buildCookie(name: string, value: string, secure: boolean, maxAgeSeconds: number): string { + const parts = [ + `${name}=${value}`, + 'Path=/', + 'HttpOnly', + 'SameSite=Lax', + `Max-Age=${maxAgeSeconds}`, + ]; + if (secure) parts.push('Secure'); + return parts.join('; '); +} + +export function buildClearCookie(name: string, secure: boolean): string { + const parts = [ + `${name}=`, + 'Path=/', + 'HttpOnly', + 'SameSite=Lax', + 'Max-Age=0', + ]; + if (secure) parts.push('Secure'); + return parts.join('; '); +} diff --git a/lib/auth/tokens.ts b/lib/auth/tokens.ts new file mode 100644 index 0000000..b6a887f --- /dev/null +++ b/lib/auth/tokens.ts @@ -0,0 +1,51 @@ +import { NextRequest } from 'next/server'; +import { eq } from 'drizzle-orm'; +import crypto from 'crypto'; +import { db, guests, type Guest } from '@/lib/db'; +import { ADM_COOKIE, USR_COOKIE } from './cookies'; + +function constantTimeEquals(a: string, b: string): boolean { + const ab = Buffer.from(a); + const bb = Buffer.from(b); + if (ab.length !== bb.length) return false; + return crypto.timingSafeEqual(ab, bb); +} + +export function getAdminToken(): string { + const t = process.env.ADMIN_TOKEN; + if (!t || t.length < 16) { + throw new Error('ADMIN_TOKEN env var must be set and at least 16 chars'); + } + return t; +} + +export function verifyAdminToken(req: NextRequest): boolean { + const cookie = req.cookies.get(ADM_COOKIE)?.value; + if (!cookie) return false; + try { + return constantTimeEquals(cookie, getAdminToken()); + } catch { + return false; + } +} + +export async function getGuestFromRequest(req: NextRequest): Promise { + const cookie = req.cookies.get(USR_COOKIE)?.value; + if (!cookie) return null; + const rows = await db.select().from(guests).where(eq(guests.id, cookie)).limit(1); + return rows[0] ?? null; +} + +export function isAdminTokenValue(token: string): boolean { + try { + return constantTimeEquals(token, getAdminToken()); + } catch { + return false; + } +} + +export async function isGuestTokenValue(token: string): Promise { + if (!token) return null; + const rows = await db.select().from(guests).where(eq(guests.id, token)).limit(1); + return rows[0] ?? null; +}