import { AUTH_SESSION_COOKIE_NAME, getSessionCookieAttributes, hashSessionToken, } from '../auth'; import type { Env } from '../env'; // Guard: only allow when running locally. The dev login bypasses OAuth and // auto-promotes the user to admin, so it must never be reachable from a // deployed environment. function isLocalDev(env: Env): boolean { return env.PUBLIC_HOST === 'localhost' || env.APP_BASE_URL.includes('localhost'); } const FIND_USER_BY_EMAIL = `SELECT id, email, name, role FROM users WHERE email = ? LIMIT 1`; const CREATE_USER = `INSERT INTO users (id, email, name, image_url, role) VALUES (?, ?, ?, NULL, ?)`; const UPDATE_USER_ROLE = `UPDATE users SET role = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`; const CREATE_SESSION = `INSERT INTO sessions (id, user_id, session_token_hash, expires_at) VALUES (?, ?, ?, ?)`; // Session expires in 30 days const SESSION_TTL_DAYS = 30; interface DevLoginBody { email: string; } interface DevUserRow { id: string; email: string; name: string | null; role: string; } export async function handleDevAuth(request: Request, env: Env): Promise { const url = new URL(request.url); if (url.pathname === '/api/auth/dev-logout') { // Guard: reject in non-local environments if (!isLocalDev(env)) { return Response.json( { error: 'Dev logout is only available in local development' }, { status: 403 }, ); } // Clear the session cookie by setting it with an expiry in the past. const cookieAttrs = getSessionCookieAttributes(request.url, env.COOKIE_DOMAIN); const expired = new Date(0).toUTCString(); const cookieValue = `${AUTH_SESSION_COOKIE_NAME}=; Expires=${expired}; ${cookieAttrs.join('; ')}`; return Response.json({ ok: true }, { headers: { 'Set-Cookie': cookieValue } }); } // Only handle /api/auth/dev-login if (url.pathname !== '/api/auth/dev-login') { return null; } // Guard: reject in non-local environments if (!isLocalDev(env)) { return Response.json( { error: 'Dev login is only available in local development' }, { status: 403 }, ); } if (request.method !== 'POST') { return Response.json({ error: 'Method not allowed' }, { status: 405 }); } let body: DevLoginBody; try { body = (await request.json()) as DevLoginBody; } catch { return Response.json({ error: 'Invalid JSON body' }, { status: 400 }); } if (!body.email || !body.email.includes('@')) { return Response.json({ error: 'Valid email is required' }, { status: 400 }); } // Find or create user let user = await env.DB.prepare(FIND_USER_BY_EMAIL) .bind(body.email) .first(); if (!user) { const userId = generateId(); const name = body.email.split('@')[0]; await env.DB.prepare(CREATE_USER).bind(userId, body.email, name, 'admin').run(); user = { id: userId, email: body.email, name, role: 'admin' }; } else { // Promote to admin if not already if (user.role !== 'admin') { await env.DB.prepare(UPDATE_USER_ROLE).bind('admin', user.id).run(); } } // Create session const sessionToken = generateSessionToken(); const tokenHash = await hashSessionToken(sessionToken); const sessionId = generateId(); const expiresAt = new Date( Date.now() + SESSION_TTL_DAYS * 24 * 60 * 60 * 1000, ).toISOString(); await env.DB.prepare(CREATE_SESSION).bind(sessionId, user.id, tokenHash, expiresAt).run(); // Build Set-Cookie header const cookieAttrs = getSessionCookieAttributes(request.url, env.COOKIE_DOMAIN); const cookieValue = `${AUTH_SESSION_COOKIE_NAME}=${sessionToken}; ${cookieAttrs.join('; ')}`; return Response.json( { ok: true, user: { id: user.id, email: user.email, name: user.name, role: 'admin' }, }, { headers: { 'Set-Cookie': cookieValue }, }, ); } function generateId(): string { return crypto.randomUUID(); } function generateSessionToken(): string { // 32-byte random token as hex const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); }