From 2f3e756f5b7edb072ce79dc97e23d981c8355ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20P=C3=A1scoa?= Date: Mon, 12 Oct 2026 09:23:15 +0100 Subject: [PATCH] feat: add local development auth mode --- src/env.ts | 2 ++ src/server/auth.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/env.ts b/src/env.ts index 017f2b93b..92a75d3e7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -31,6 +31,7 @@ export const env = createEnv({ DISABLE_EMAIL_SIGNUP: z.boolean(), INVITE_ONLY: z.boolean(), PREVIEW_ACCESS_AUTH: z.boolean().default(false), + LOCAL_DEV_AUTH: z.boolean().default(false), FROM_EMAIL: z.string().optional(), EMAIL_SERVER_HOST: z.string().optional(), EMAIL_SERVER_PORT: z.string().optional(), @@ -101,6 +102,7 @@ export const env = createEnv({ DISABLE_EMAIL_SIGNUP: parseEnvBoolean(process.env.DISABLE_EMAIL_SIGNUP), INVITE_ONLY: parseEnvBoolean(process.env.INVITE_ONLY), PREVIEW_ACCESS_AUTH: parseEnvBoolean(process.env.PREVIEW_ACCESS_AUTH, false), + LOCAL_DEV_AUTH: parseEnvBoolean(process.env.LOCAL_DEV_AUTH, false), FROM_EMAIL: process.env.FROM_EMAIL, EMAIL_SERVER_HOST: process.env.EMAIL_SERVER_HOST, EMAIL_SERVER_PORT: process.env.EMAIL_SERVER_PORT, diff --git a/src/server/auth.ts b/src/server/auth.ts index cc6a9fc1e..0b75df2b0 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -99,6 +99,45 @@ const toHeaders = (raw: Headers | Record) return headers; }; +const isLocalDevHost = (host: string | null): boolean => { + if (!host) return false; + + let hostname: string; + try { + hostname = new URL(`http://${host}`).hostname; + } catch { + return false; + } + + if ( + 'localhost' === hostname || + hostname.endsWith('.localhost') || + '[::1]' === hostname + ) { + return true; + } + + const octets = hostname.split('.').map(Number); + if ( + 4 !== octets.length || + octets.some((octet) => !Number.isInteger(octet) || 0 > octet || 255 < octet) + ) { + return false; + } + + const first = octets[0]; + const second = octets[1]; + if (undefined === first || undefined === second) return false; + + return ( + 10 === first || + 127 === first || + (192 === first && 168 === second) || + (172 === first && 16 <= second && 31 >= second) || + (169 === first && 254 === second) + ); +}; + const loadSessionUser = async (userId: number): Promise => { const user = await db.query.users.findFirst({ where: eq(users.id, userId) }); if (!user) { @@ -123,6 +162,21 @@ export const getSessionFromHeaders = async ( raw: Headers | Record, ): Promise => { const headers = toHeaders(raw); + // Limit the opt-in demo identity to localhost/private-network requests. + if (env.LOCAL_DEV_AUTH && isLocalDevHost(headers.get('host'))) { + const email = 'local-dev@splitpro.test'; + await db + .insert(users) + .values({ email, name: 'Local Developer', emailVerified: true }) + .onConflictDoNothing(); + + const localDevUser = await db.query.users.findFirst({ where: eq(users.email, email) }); + if (!localDevUser) return null; + const user = await loadSessionUser(localDevUser.id); + if (!user) return null; + return { user, expires: new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() }; + } + if (env.PREVIEW_ACCESS_AUTH) { const email = headers.get(PREVIEW_ACCESS_EMAIL_HEADER)?.trim().toLowerCase(); if (!email) return null;