mirror of
https://github.com/wahyd4/heygo.git
synced 2026-08-09 05:15:52 +10:00
- POST /api/auth/dev-login creates/finds user, promotes to admin, sets session cookie - POST /api/auth/dev-logout clears session cookie - Dev login page at #/dev-login in the SPA - Fix: getSessionCookieAttributes omits Secure over HTTP (local dev fix) - Guarded: only works when PUBLIC_HOST=localhost, returns 403 otherwise - Justfile: dev-admin and dev-logout commands - README: local admin login instructions
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
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<Response | null> {
|
|
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<DevUserRow>();
|
|
|
|
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('');
|
|
}
|