update dev env

This commit is contained in:
2026-06-20 20:06:10 +10:00
parent 73ca8ac321
commit 6c898fe1f0
7 changed files with 115 additions and 5 deletions
+15
View File
@@ -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 `COOKIE_DOMAIN` from env vars (set in each wrangler config), so the same code
serves every environment without hardcoded hostnames. 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 ### Prerequisites
- A Cloudflare account with the `heygo.cc` zone added. - A Cloudflare account with the `heygo.cc` zone added.
+66 -1
View File
@@ -3,7 +3,9 @@ import {
AUTH_SESSION_COOKIE_NAME, AUTH_SESSION_COOKIE_NAME,
AuthError, AuthError,
getCurrentUser, getCurrentUser,
getConfiguredAdminEmails,
hashSessionToken, hashSessionToken,
isConfiguredAdminEmail,
parseCookieHeader, parseCookieHeader,
requireAdmin, requireAdmin,
requireUser, requireUser,
@@ -64,7 +66,7 @@ class FakeSessionStatement {
} }
} }
function makeEnv(rows: SessionRow[] = []): { env: Env; db: FakeSessionD1 } { function makeEnv(rows: SessionRow[] = [], overrides: Partial<Env> = {}): { env: Env; db: FakeSessionD1 } {
const db = new FakeSessionD1(rows); const db = new FakeSessionD1(rows);
return { return {
env: { env: {
@@ -73,6 +75,7 @@ function makeEnv(rows: SessionRow[] = []): { env: Env; db: FakeSessionD1 } {
PRIVATE_HOST: 'my.heygo.cc', PRIVATE_HOST: 'my.heygo.cc',
APP_BASE_URL: 'https://heygo.cc', APP_BASE_URL: 'https://heygo.cc',
COOKIE_DOMAIN: '.heygo.cc', COOKIE_DOMAIN: '.heygo.cc',
...overrides,
}, },
db, 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', () => { describe('getCurrentUser', () => {
it('returns null when there is no Cookie header', async () => { it('returns null when there is no Cookie header', async () => {
const { env } = makeEnv(); 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 () => { it('returns null for an expired session', async () => {
const token = 'expired-session-token'; const token = 'expired-session-token';
const hash = await hashSessionToken(token); const hash = await hashSessionToken(token);
@@ -272,4 +312,29 @@ describe('requireAdmin', () => {
expect(user.id).toBe('user_5'); expect(user.id).toBe('user_5');
expect(user.role).toBe('admin'); 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');
});
}); });
+27 -1
View File
@@ -24,6 +24,7 @@ export interface AuthSession {
} }
const ENABLED_PROVIDER_SET = new Set<string>(AUTH_ENABLED_PROVIDERS); const ENABLED_PROVIDER_SET = new Set<string>(AUTH_ENABLED_PROVIDERS);
const ADMIN_EMAIL_SPLIT_PATTERN = /[\s,;]+/;
export function isAuthProviderEnabled(provider: string): provider is EnabledAuthProvider { export function isAuthProviderEnabled(provider: string): provider is EnabledAuthProvider {
return ENABLED_PROVIDER_SET.has(provider); return ENABLED_PROVIDER_SET.has(provider);
@@ -96,6 +97,31 @@ export async function hashSessionToken(token: string): Promise<string> {
return bufferToHex(digest); return bufferToHex(digest);
} }
export function getConfiguredAdminEmails(env: Pick<Env, 'ADMIN_EMAILS'>): Set<string> {
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<Env, 'ADMIN_EMAILS'>,
): boolean {
if (!email) {
return false;
}
return getConfiguredAdminEmails(env).has(email.trim().toLowerCase());
}
function bufferToHex(buffer: ArrayBuffer): string { function bufferToHex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
let hex = ''; let hex = '';
@@ -148,7 +174,7 @@ export async function getCurrentUser(request: Request, env: Env): Promise<AuthUs
email: row.email, email: row.email,
name: row.name, name: row.name,
imageUrl: row.image_url, imageUrl: row.image_url,
role: row.role, role: isConfiguredAdminEmail(row.email, env) ? 'admin' : row.role,
}; };
} }
+1
View File
@@ -6,4 +6,5 @@ export interface Env {
PRIVATE_HOST: string; PRIVATE_HOST: string;
APP_BASE_URL: string; APP_BASE_URL: string;
COOKIE_DOMAIN: string; COOKIE_DOMAIN: string;
ADMIN_EMAILS?: string;
} }
+2 -1
View File
@@ -23,7 +23,8 @@
"PUBLIC_HOST": "dev.heygo.cc", "PUBLIC_HOST": "dev.heygo.cc",
"PRIVATE_HOST": "my.dev.heygo.cc", "PRIVATE_HOST": "my.dev.heygo.cc",
"APP_BASE_URL": "https://dev.heygo.cc", "APP_BASE_URL": "https://dev.heygo.cc",
"COOKIE_DOMAIN": ".heygo.cc" "COOKIE_DOMAIN": ".heygo.cc",
"ADMIN_EMAILS": "wahyd4@gmail.com"
}, },
"d1_databases": [ "d1_databases": [
{ {
+2 -1
View File
@@ -13,7 +13,8 @@
"PUBLIC_HOST": "*", "PUBLIC_HOST": "*",
"PRIVATE_HOST": "*", "PRIVATE_HOST": "*",
"APP_BASE_URL": "http://localhost:8787", "APP_BASE_URL": "http://localhost:8787",
"COOKIE_DOMAIN": "" "COOKIE_DOMAIN": "",
"ADMIN_EMAILS": "wahyd4@gmail.com"
}, },
"d1_databases": [ "d1_databases": [
{ {
+2 -1
View File
@@ -13,7 +13,8 @@
"PUBLIC_HOST": "heygo.cc", "PUBLIC_HOST": "heygo.cc",
"PRIVATE_HOST": "my.heygo.cc", "PRIVATE_HOST": "my.heygo.cc",
"APP_BASE_URL": "https://heygo.cc", "APP_BASE_URL": "https://heygo.cc",
"COOKIE_DOMAIN": ".heygo.cc" "COOKIE_DOMAIN": ".heygo.cc",
"ADMIN_EMAILS": "wahyd4@gmail.com"
}, },
"d1_databases": [ "d1_databases": [
{ {