From 6c898fe1f031f6ee3d30714127c2527ca420ec36 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sat, 20 Jun 2026 20:06:10 +1000 Subject: [PATCH] update dev env --- README.md | 15 ++++++++++ tests/session.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++++- worker/auth.ts | 28 +++++++++++++++++- worker/env.ts | 1 + wrangler.dev.jsonc | 3 +- wrangler.jsonc | 3 +- wrangler.prod.jsonc | 3 +- 7 files changed, 115 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0d8a5e3..bff2b26 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,21 @@ The Worker reads `PUBLIC_HOST`, `PRIVATE_HOST`, `APP_BASE_URL`, and `COOKIE_DOMAIN` from env vars (set in each wrangler config), so the same code serves every environment without hardcoded hostnames. +Admin users are configured with `ADMIN_EMAILS` in the matching Wrangler config: + +- `wrangler.dev.jsonc` for `dev.heygo.cc` +- `wrangler.prod.jsonc` for `heygo.cc` +- `wrangler.jsonc` for local development + +Use a comma, semicolon, space, or newline separated list, for example: + +```jsonc +"ADMIN_EMAILS": "owner@example.com, ops@example.com" +``` + +When a signed-in user's email matches this list, the app treats that user as +`admin` even if the stored database role is still `user`. + ### Prerequisites - A Cloudflare account with the `heygo.cc` zone added. diff --git a/tests/session.test.ts b/tests/session.test.ts index bde11e7..9383799 100644 --- a/tests/session.test.ts +++ b/tests/session.test.ts @@ -3,7 +3,9 @@ import { AUTH_SESSION_COOKIE_NAME, AuthError, getCurrentUser, + getConfiguredAdminEmails, hashSessionToken, + isConfiguredAdminEmail, parseCookieHeader, requireAdmin, requireUser, @@ -64,7 +66,7 @@ class FakeSessionStatement { } } -function makeEnv(rows: SessionRow[] = []): { env: Env; db: FakeSessionD1 } { +function makeEnv(rows: SessionRow[] = [], overrides: Partial = {}): { env: Env; db: FakeSessionD1 } { const db = new FakeSessionD1(rows); return { env: { @@ -73,6 +75,7 @@ function makeEnv(rows: SessionRow[] = []): { env: Env; db: FakeSessionD1 } { PRIVATE_HOST: 'my.heygo.cc', APP_BASE_URL: 'https://heygo.cc', COOKIE_DOMAIN: '.heygo.cc', + ...overrides, }, db, }; @@ -129,6 +132,19 @@ describe('hashSessionToken', () => { }); }); +describe('configured admin emails', () => { + it('parses comma, semicolon, and whitespace separated admin emails case-insensitively', () => { + const emails = getConfiguredAdminEmails({ + ADMIN_EMAILS: ' Boss@Heygo.cc, owner@example.com; ops@example.com\n', + }); + + expect([...emails]).toEqual(['boss@heygo.cc', 'owner@example.com', 'ops@example.com']); + expect(isConfiguredAdminEmail('boss@heygo.cc', { ADMIN_EMAILS: 'Boss@Heygo.cc' })).toBe(true); + expect(isConfiguredAdminEmail('BOSS@HEYGO.CC', { ADMIN_EMAILS: 'boss@heygo.cc' })).toBe(true); + expect(isConfiguredAdminEmail('person@heygo.cc', { ADMIN_EMAILS: 'boss@heygo.cc' })).toBe(false); + }); +}); + describe('getCurrentUser', () => { it('returns null when there is no Cookie header', async () => { const { env } = makeEnv(); @@ -176,6 +192,30 @@ describe('getCurrentUser', () => { }); }); + it('treats a configured admin email as admin even when the database role is user', async () => { + const token = 'configured-admin-token'; + const hash = await hashSessionToken(token); + const { env } = makeEnv( + [ + { + id: 'user_configured_admin', + email: 'Boss@Heygo.cc', + name: 'Boss', + image_url: null, + role: 'user', + expires_at: futureIso(), + session_token_hash: hash, + }, + ], + { ADMIN_EMAILS: 'boss@heygo.cc' }, + ); + + const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; + const user = await getCurrentUser(makeRequest(cookie), env); + + expect(user?.role).toBe('admin'); + }); + it('returns null for an expired session', async () => { const token = 'expired-session-token'; const hash = await hashSessionToken(token); @@ -272,4 +312,29 @@ describe('requireAdmin', () => { expect(user.id).toBe('user_5'); expect(user.role).toBe('admin'); }); + + it('allows a configured admin email through requireAdmin', async () => { + const token = 'configured-admin-require-token'; + const hash = await hashSessionToken(token); + const { env } = makeEnv( + [ + { + id: 'user_6', + email: 'owner@heygo.cc', + name: 'Owner', + image_url: null, + role: 'user', + expires_at: futureIso(), + session_token_hash: hash, + }, + ], + { ADMIN_EMAILS: 'owner@heygo.cc' }, + ); + + const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; + const user = await requireAdmin(makeRequest(cookie), env); + + expect(user.id).toBe('user_6'); + expect(user.role).toBe('admin'); + }); }); diff --git a/worker/auth.ts b/worker/auth.ts index 3151b30..ff16a17 100644 --- a/worker/auth.ts +++ b/worker/auth.ts @@ -24,6 +24,7 @@ export interface AuthSession { } const ENABLED_PROVIDER_SET = new Set(AUTH_ENABLED_PROVIDERS); +const ADMIN_EMAIL_SPLIT_PATTERN = /[\s,;]+/; export function isAuthProviderEnabled(provider: string): provider is EnabledAuthProvider { return ENABLED_PROVIDER_SET.has(provider); @@ -96,6 +97,31 @@ export async function hashSessionToken(token: string): Promise { return bufferToHex(digest); } +export function getConfiguredAdminEmails(env: Pick): Set { + const raw = env.ADMIN_EMAILS?.trim(); + if (!raw) { + return new Set(); + } + + return new Set( + raw + .split(ADMIN_EMAIL_SPLIT_PATTERN) + .map((email) => email.trim().toLowerCase()) + .filter(Boolean), + ); +} + +export function isConfiguredAdminEmail( + email: string | null | undefined, + env: Pick, +): boolean { + if (!email) { + return false; + } + + return getConfiguredAdminEmails(env).has(email.trim().toLowerCase()); +} + function bufferToHex(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let hex = ''; @@ -148,7 +174,7 @@ export async function getCurrentUser(request: Request, env: Env): Promise