feat: add dev-only login for local admin access without OAuth

- 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
This commit is contained in:
Hermes Agent
2026-06-20 14:55:28 +10:00
parent a5b0953cc0
commit fd647bcc2e
9 changed files with 687 additions and 7 deletions
+21
View File
@@ -19,6 +19,27 @@ just dev # start Worker on http://localhost:8787
Open `http://localhost:8787` in your browser — the SPA and API are both served by the Worker.
### Sign in as admin locally
OAuth isn't available in local dev. Use the dev-only login instead:
1. Start the worker: `just dev`
2. Open `http://localhost:8787/#/dev-login` in your browser
3. Enter your email (pre-filled with `wahyd4@gmail.com`) and click "Sign in as admin"
Or via CLI:
```bash
just dev-admin # creates admin user + session
just dev-admin other@example.com # different email
just dev-logout # clears the session cookie
```
This endpoint is guarded: it only works when `PUBLIC_HOST=localhost` (or `APP_BASE_URL`
contains `localhost`). In dev/prod deployments it returns 403. The dev login bypasses
OAuth, finds-or-creates the user, promotes them to admin, and sets a `heygo_session`
cookie — exactly what a real OAuth callback would do, minus the provider.
### Just commands
Run `just` (no args) to list all recipes. The most common ones:
+20
View File
@@ -46,6 +46,26 @@ smoke: build migrate-local
echo "--- Root (SPA) ---"; curl -sI http://localhost:{{port}}/ | head -1; \
kill $$SERVER_PID 2>/dev/null
# ── Local dev auth ─────────────────────────────────────────
# Create admin user and sign in locally (prints URL to open in browser)
# Requires the worker to be running: `just dev`
# Usage: just dev-admin wahyd4@gmail.com
dev-admin email="wahyd4@gmail.com":
@echo "=== Creating admin user via dev login API ==="
@curl -s -X POST http://localhost:{{port}}/api/auth/dev-login \
-H 'content-type: application/json' \
-d '{"email":"{{email}}"}'
@echo
@echo "Or open http://localhost:{{port}}/#/dev-login in your browser"
# Sign out locally (clears session cookie)
# Requires the worker to be running: `just dev`
dev-logout:
@curl -s -X POST http://localhost:{{port}}/api/auth/dev-logout
@echo
@echo "Signed out."
# ── Local D1 helpers ───────────────────────────────────────
# Run an arbitrary SQL query against local D1 (quote the SQL)
+5 -2
View File
@@ -3,18 +3,20 @@ import LoginPage from './routes/LoginPage';
import PrivateLinksPage from './routes/PrivateLinksPage';
import PublicLinksPage from './routes/PublicLinksPage';
import AdminReviewPage from './routes/AdminReviewPage';
import DevLoginPage from './routes/DevLoginPage';
type RouteId = 'private' | 'public' | 'admin' | 'login';
type RouteId = 'private' | 'public' | 'admin' | 'login' | 'dev-login';
const ROUTES: readonly { id: RouteId; label: string }[] = [
{ id: 'private', label: 'Private Links' },
{ id: 'public', label: 'Public Directory' },
{ id: 'admin', label: 'Admin Review' },
{ id: 'login', label: 'Login' },
{ id: 'dev-login', label: 'Dev Login' },
];
function readRouteFromHash(): RouteId {
const match = window.location.hash.match(/^#\/(private|public|admin|login)/);
const match = window.location.hash.match(/^#\/(private|public|admin|login|dev-login)/);
return (match?.[1] as RouteId) ?? 'private';
}
@@ -58,6 +60,7 @@ export default function App() {
{route === 'public' ? <PublicLinksPage /> : null}
{route === 'admin' ? <AdminReviewPage /> : null}
{route === 'login' ? <LoginPage /> : null}
{route === 'dev-login' ? <DevLoginPage /> : null}
</main>
<footer className="app-footer">
+62
View File
@@ -0,0 +1,62 @@
import { useState } from 'react';
export default function DevLoginPage() {
const [email, setEmail] = useState('wahyd4@gmail.com');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [error, setError] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus('loading');
setError('');
try {
const res = await fetch('/api/auth/dev-login', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email }),
});
if (!res.ok) {
const data = (await res.json().catch(() => ({ error: 'Login failed' }))) as {
error?: string;
};
throw new Error(data.error || 'Login failed');
}
setStatus('success');
// Redirect to private links page after short delay
setTimeout(() => {
window.location.hash = '/private';
}, 500);
} catch (err) {
setStatus('error');
setError(err instanceof Error ? err.message : 'Login failed');
}
}
return (
<section className="panel login-panel">
<h1>Dev Login (local only)</h1>
<p className="muted">
This login bypasses OAuth and is only available when running locally
(PUBLIC_HOST=localhost). The user is automatically promoted to admin.
</p>
<form onSubmit={handleSubmit} className="dev-login-form">
<label>
Email
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</label>
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Signing in…' : 'Sign in as admin'}
</button>
{status === 'success' && <p className="success-msg"> Signed in! Redirecting</p>}
{status === 'error' && <p className="error-msg"> {error}</p>}
</form>
</section>
);
}
+10 -3
View File
@@ -25,17 +25,24 @@ describe('session cookie policy', () => {
it('uses the configured cookie domain when one is supplied', () => {
expect(getSessionCookieAttributes('https://my.heygo.cc/app', '.heygo.cc')).toEqual([
'HttpOnly',
'Secure',
'SameSite=Lax',
'Path=/',
'Secure',
'Domain=.heygo.cc',
]);
});
it('omits Secure over HTTP (local dev) even when a domain is set', () => {
expect(getSessionCookieAttributes('http://localhost:8787/app', '')).toEqual([
'HttpOnly',
'SameSite=Lax',
'Path=/',
]);
});
it('omits Domain when the cookie domain env var is empty (localhost development)', () => {
expect(getSessionCookieAttributes('http://localhost:5173/app', '')).toEqual([
'HttpOnly',
'Secure',
'SameSite=Lax',
'Path=/',
]);
@@ -44,9 +51,9 @@ describe('session cookie policy', () => {
it('honors a custom cookie domain for non-heygo hosts', () => {
expect(getSessionCookieAttributes('https://my.example.com/app', '.example.com')).toEqual([
'HttpOnly',
'Secure',
'SameSite=Lax',
'Path=/',
'Secure',
'Domain=.example.com',
]);
});
+424
View File
@@ -0,0 +1,424 @@
import { describe, expect, it } from 'vitest';
import worker from '../worker/index';
import { AUTH_SESSION_COOKIE_NAME, hashSessionToken } from '../worker/auth';
import type { Env } from '../worker/env';
type UserRole = 'user' | 'admin';
type UserRow = {
id: string;
email: string;
name: string | null;
role: UserRole;
};
type SessionRow = {
id: string;
user_id: string;
session_token_hash: string;
expires_at: string;
};
type RunCall = {
sql: string;
params: unknown[];
};
type FirstCall = {
sql: string;
params: unknown[];
};
class FakeDevAuthD1 {
readonly users: UserRow[] = [];
readonly sessions: SessionRow[] = [];
readonly runCalls: RunCall[] = [];
readonly firstCalls: FirstCall[] = [];
prepare(sql: string): FakeDevAuthStatement {
return new FakeDevAuthStatement(this, sql);
}
findUserByEmail(email: string): UserRow | null {
return this.users.find((u) => u.email === email) ?? null;
}
findSessionByHash(hash: string): SessionRow | null {
return this.sessions.find((s) => s.session_token_hash === hash) ?? null;
}
}
class FakeDevAuthStatement {
private params: unknown[] = [];
constructor(
private readonly db: FakeDevAuthD1,
private readonly sql: string,
) {}
bind(...params: unknown[]): this {
this.params = params;
return this;
}
async first<T>(): Promise<T | null> {
this.db.firstCalls.push({ sql: this.sql, params: this.params });
if (this.sql.includes('FROM users') && this.sql.includes('email = ?')) {
const row = this.db.findUserByEmail(String(this.params[0]));
if (!row) {
return null;
}
return { id: row.id, email: row.email, name: row.name, role: row.role } as T;
}
return null;
}
async run(): Promise<D1Result> {
this.db.runCalls.push({ sql: this.sql, params: this.params });
if (this.sql.startsWith('INSERT INTO users')) {
const [id, email, name, role] = this.params;
this.db.users.push({
id: String(id),
email: String(email),
name: name == null ? null : String(name),
role: String(role) as UserRole,
});
}
if (this.sql.startsWith('UPDATE users SET role')) {
const [role, id] = this.params;
const row = this.db.users.find((u) => u.id === String(id));
if (row) {
row.role = String(role) as UserRole;
}
}
if (this.sql.startsWith('INSERT INTO sessions')) {
const [id, userId, tokenHash, expiresAt] = this.params;
this.db.sessions.push({
id: String(id),
user_id: String(userId),
session_token_hash: String(tokenHash),
expires_at: String(expiresAt),
});
}
return { success: true, meta: { changes: 1 } } as unknown as D1Result;
}
}
class FakeExecutionContext {
waitUntil(): void {}
passThroughOnException(): void {}
}
type DevLoginResponse = {
ok: boolean;
user: { id: string; email: string; name: string | null; role: string };
};
type DevErrorResponse = { error: string };
type EnvOverrides = Partial<Env>;
function makeEnv(db: FakeDevAuthD1, overrides: EnvOverrides = {}): Env {
return {
DB: db as unknown as D1Database,
PUBLIC_HOST: 'localhost',
PRIVATE_HOST: 'localhost',
APP_BASE_URL: 'http://localhost:8787',
COOKIE_DOMAIN: '',
...overrides,
} as Env;
}
function prodEnv(overrides: EnvOverrides = {}): Env {
return {
DB: {} as D1Database,
PUBLIC_HOST: 'heygo.cc',
PRIVATE_HOST: 'my.heygo.cc',
APP_BASE_URL: 'https://heygo.cc',
COOKIE_DOMAIN: '.heygo.cc',
...overrides,
} as Env;
}
async function fetchWorker(
path: string,
env: Env,
init: RequestInit = {},
): Promise<Response> {
const url = `http://localhost:8787${path}`;
const request = new Request(url, init);
const ctx = new FakeExecutionContext();
return worker.fetch(
request as unknown as Parameters<typeof worker.fetch>[0],
env as unknown as Parameters<typeof worker.fetch>[1],
ctx as unknown as Parameters<typeof worker.fetch>[2],
);
}
function jsonPost(path: string, body: unknown): RequestInit {
return {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
};
}
describe('dev auth endpoint', () => {
it('creates a new admin user and sets a session cookie on POST /api/auth/dev-login', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'wahyd4@gmail.com' }),
);
expect(response.status).toBe(200);
const setCookie = response.headers.get('set-cookie');
expect(setCookie).not.toBeNull();
expect(setCookie).toContain(`${AUTH_SESSION_COOKIE_NAME}=`);
expect(setCookie).toContain('HttpOnly');
expect(setCookie).toContain('Path=/');
// Local dev is HTTP, so Secure must NOT be present
expect(setCookie).not.toContain('Secure');
const data = (await response.json()) as DevLoginResponse;
expect(data).toMatchObject({
ok: true,
user: { email: 'wahyd4@gmail.com', role: 'admin' },
});
// A user was created with admin role
expect(db.users).toHaveLength(1);
expect(db.users[0].email).toBe('wahyd4@gmail.com');
expect(db.users[0].role).toBe('admin');
// A session was created
expect(db.sessions).toHaveLength(1);
const createdSession = db.sessions[0];
expect(createdSession.user_id).toBe(db.users[0].id);
expect(createdSession.session_token_hash).not.toBe('');
// expires_at is a valid future ISO date
const expiresAt = Date.parse(createdSession.expires_at);
expect(Number.isNaN(expiresAt)).toBe(false);
expect(expiresAt).toBeGreaterThan(Date.now());
});
it('reuses an existing user and promotes a non-admin to admin', async () => {
const db = new FakeDevAuthD1();
db.users.push({
id: 'user_existing',
email: 'wahyd4@gmail.com',
name: 'wahyd4',
role: 'user',
});
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'wahyd4@gmail.com' }),
);
expect(response.status).toBe(200);
const data = (await response.json()) as DevLoginResponse;
expect(data.user.role).toBe('admin');
expect(data.user.id).toBe('user_existing');
// No new user inserted
expect(db.users).toHaveLength(1);
// Role promoted to admin via UPDATE
expect(db.users[0].role).toBe('admin');
const updateCall = db.runCalls.find((c) => c.sql.startsWith('UPDATE users SET role'));
expect(updateCall).toBeDefined();
expect(updateCall?.params).toEqual(['admin', 'user_existing']);
// Session was still created
expect(db.sessions).toHaveLength(1);
});
it('does not issue an UPDATE when the existing user is already admin', async () => {
const db = new FakeDevAuthD1();
db.users.push({
id: 'user_admin',
email: 'wahyd4@gmail.com',
name: 'wahyd4',
role: 'admin',
});
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'wahyd4@gmail.com' }),
);
expect(response.status).toBe(200);
expect(db.runCalls.find((c) => c.sql.startsWith('UPDATE users SET role'))).toBeUndefined();
expect(db.sessions).toHaveLength(1);
});
it('rejects GET /api/auth/dev-login with 405', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker('/api/auth/dev-login', env, { method: 'GET' });
expect(response.status).toBe(405);
const data = (await response.json()) as DevErrorResponse;
expect(data.error).toMatch(/method/i);
// No DB writes happened
expect(db.runCalls).toHaveLength(0);
});
it('rejects an invalid email with 400', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'not-an-email' }),
);
expect(response.status).toBe(400);
const data = (await response.json()) as DevErrorResponse;
expect(data.error).toMatch(/email/i);
expect(db.users).toHaveLength(0);
expect(db.sessions).toHaveLength(0);
});
it('rejects a missing email with 400', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', {}),
);
expect(response.status).toBe(400);
expect(db.users).toHaveLength(0);
});
it('rejects an invalid JSON body with 400', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker('/api/auth/dev-login', env, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: 'not-json',
});
expect(response.status).toBe(400);
const data = (await response.json()) as DevErrorResponse;
expect(data.error).toMatch(/json/i);
});
it('is guarded: returns 403 when PUBLIC_HOST is not localhost', async () => {
const db = new FakeDevAuthD1();
const env = prodEnv({ DB: db as unknown as D1Database });
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'wahyd4@gmail.com' }),
);
expect(response.status).toBe(403);
const data = (await response.json()) as DevErrorResponse;
expect(data.error).toMatch(/local/i);
// No user or session created
expect(db.users).toHaveLength(0);
expect(db.sessions).toHaveLength(0);
});
it('is guarded: returns 403 even when APP_BASE_URL has no localhost', async () => {
const db = new FakeDevAuthD1();
const env = prodEnv({ DB: db as unknown as D1Database, PUBLIC_HOST: 'dev.heygo.cc' });
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'wahyd4@gmail.com' }),
);
expect(response.status).toBe(403);
expect(db.users).toHaveLength(0);
});
it('POST /api/auth/dev-logout clears the session cookie with a past expiry', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-logout',
env,
{ method: 'POST' },
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ ok: true });
const setCookie = response.headers.get('set-cookie');
expect(setCookie).not.toBeNull();
expect(setCookie).toContain(`${AUTH_SESSION_COOKIE_NAME}=;`);
// Must clear the cookie via an expiry in the past
expect(setCookie).toContain('Expires=');
// Should NOT contain Secure (HTTP local dev)
expect(setCookie).not.toContain('Secure');
});
it('dev-logout is guarded: returns 403 when not local dev', async () => {
const env = prodEnv();
const response = await fetchWorker('/api/auth/dev-logout', env, { method: 'POST' });
expect(response.status).toBe(403);
});
it('returns a valid session that getCurrentUser can later resolve', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker(
'/api/auth/dev-login',
env,
jsonPost('/api/auth/dev-login', { email: 'wahyd4@gmail.com' }),
);
expect(response.status).toBe(200);
const setCookie = response.headers.get('set-cookie');
expect(setCookie).not.toBeNull();
// Extract the raw token value from the Set-Cookie header
const tokenMatch = setCookie!.match(new RegExp(`${AUTH_SESSION_COOKIE_NAME}=([^;]+)`));
expect(tokenMatch).not.toBeNull();
const token = tokenMatch![1];
// The stored hash must match a SHA-256 of the token we issued
const expectedHash = await hashSessionToken(token);
const session = db.findSessionByHash(expectedHash);
expect(session).not.toBeNull();
expect(session?.user_id).toBe(db.users[0].id);
});
it('falls through (404) for unrelated /api/ paths', async () => {
const db = new FakeDevAuthD1();
const env = makeEnv(db);
const response = await fetchWorker('/api/some-other-path', env, { method: 'GET' });
expect(response.status).toBe(404);
// No DB writes for dev auth
expect(db.runCalls).toHaveLength(0);
});
});
+8 -2
View File
@@ -36,8 +36,14 @@ export function getSessionCookieAttributes(
// The request URL is retained so callers can derive a host-specific domain in
// the future; the shared Domain attribute now comes from the COOKIE_DOMAIN
// env var so each deployment (dev/prod/local) controls its own scope.
void requestUrl;
const attributes = ['HttpOnly', 'Secure', 'SameSite=Lax', 'Path=/'];
const url = typeof requestUrl === 'string' ? new URL(requestUrl) : requestUrl;
const attributes = ['HttpOnly', 'SameSite=Lax', 'Path=/'];
// Only set Secure over HTTPS. Local dev (wrangler dev) runs on HTTP;
// a Secure cookie would be silently dropped by the browser.
if (url.protocol === 'https:') {
attributes.push('Secure');
}
if (cookieDomain) {
attributes.push(`Domain=${cookieDomain}`);
+5
View File
@@ -1,5 +1,6 @@
import type { Env } from './env';
import { withPrivateNoStoreHeaders } from './lib/responses';
import { handleDevAuth } from './routes/api.dev-auth';
import { handleLinksApi } from './routes/api.links';
import { handlePromotionsApi } from './routes/api.promotions';
import { handlePrivateShortlink, isHeygoPrivateHost } from './routes/private-redirect';
@@ -34,6 +35,10 @@ export default {
}
if (url.pathname.startsWith('/api/')) {
const devAuthResponse = await handleDevAuth(request, env);
if (devAuthResponse) {
return withPrivateHostNoStoreHeaders(url, devAuthResponse, env.PRIVATE_HOST);
}
const apiResponse = await handleLinksApi(request, env);
if (apiResponse) {
return withPrivateHostNoStoreHeaders(url, apiResponse, env.PRIVATE_HOST);
+132
View File
@@ -0,0 +1,132 @@
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('');
}