feat(auth): add token verification and cookie helpers
This commit is contained in:
39
lib/auth/cookies.ts
Normal file
39
lib/auth/cookies.ts
Normal file
@@ -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('; ');
|
||||
}
|
||||
51
lib/auth/tokens.ts
Normal file
51
lib/auth/tokens.ts
Normal file
@@ -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<Guest | null> {
|
||||
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<Guest | null> {
|
||||
if (!token) return null;
|
||||
const rows = await db.select().from(guests).where(eq(guests.id, token)).limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user