diff --git a/frontend/db/index.ts b/frontend/db/index.ts index 5e76ca23..931d67d8 100644 --- a/frontend/db/index.ts +++ b/frontend/db/index.ts @@ -1,33 +1,36 @@ import { neon } from '@neondatabase/serverless'; -import * as dotenv from 'dotenv'; import { drizzle as drizzleNeon } from 'drizzle-orm/neon-http'; import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'; import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core'; import { Pool } from 'pg'; +import { readServerEnv } from '@/lib/env/server-env'; + import * as schema from './schema'; -dotenv.config(); type AppDatabase = PgDatabase; -const APP_ENV = process.env.APP_ENV?.trim().toLowerCase(); +const APP_ENV = readServerEnv('APP_ENV')?.toLowerCase(); + +const DATABASE_URL = readServerEnv('DATABASE_URL'); +const DATABASE_URL_LOCAL = readServerEnv('DATABASE_URL_LOCAL'); +const IS_LOCAL_ENV = APP_ENV === 'local'; + +const STRICT_LOCAL_DB_GUARD = readServerEnv('SHOP_STRICT_LOCAL_DB') === '1'; +const REQUIRED_LOCAL_DB_URL = readServerEnv('SHOP_REQUIRED_DATABASE_URL_LOCAL'); if (process.env.NODE_ENV !== 'test') { console.log('[db] runtime env check', { - APP_ENV: process.env.APP_ENV ?? '', - has_DATABASE_URL: Boolean(process.env.DATABASE_URL?.trim()), - has_DATABASE_URL_LOCAL: Boolean(process.env.DATABASE_URL_LOCAL?.trim()), - NETLIFY: process.env.NETLIFY ?? '', - CONTEXT: process.env.CONTEXT ?? '', + APP_ENV: APP_ENV ?? '', + has_DATABASE_URL: Boolean(DATABASE_URL), + has_DATABASE_URL_LOCAL: Boolean(DATABASE_URL_LOCAL), + NETLIFY: readServerEnv('NETLIFY') ?? '', + CONTEXT: readServerEnv('CONTEXT') ?? '', NODE_ENV: process.env.NODE_ENV ?? '', }); } -const IS_LOCAL_ENV = APP_ENV === 'local'; - -const STRICT_LOCAL_DB_GUARD = process.env.SHOP_STRICT_LOCAL_DB === '1'; -const REQUIRED_LOCAL_DB_URL = process.env.SHOP_REQUIRED_DATABASE_URL_LOCAL; if (STRICT_LOCAL_DB_GUARD) { if (!IS_LOCAL_ENV) { @@ -36,13 +39,13 @@ if (STRICT_LOCAL_DB_GUARD) { ); } - if (!process.env.DATABASE_URL_LOCAL?.trim()) { + if (!DATABASE_URL_LOCAL) { throw new Error( '[db] SHOP_STRICT_LOCAL_DB=1 requires DATABASE_URL_LOCAL to be set' ); } - if (process.env.DATABASE_URL?.trim()) { + if (DATABASE_URL) { throw new Error( '[db] SHOP_STRICT_LOCAL_DB=1 forbids DATABASE_URL during shop-local tests' ); @@ -50,7 +53,7 @@ if (STRICT_LOCAL_DB_GUARD) { if ( REQUIRED_LOCAL_DB_URL && - process.env.DATABASE_URL_LOCAL !== REQUIRED_LOCAL_DB_URL + DATABASE_URL_LOCAL !== REQUIRED_LOCAL_DB_URL ) { throw new Error( '[db] SHOP_STRICT_LOCAL_DB=1 requires DATABASE_URL_LOCAL to match SHOP_REQUIRED_DATABASE_URL_LOCAL exactly' @@ -60,38 +63,29 @@ if (STRICT_LOCAL_DB_GUARD) { let db: AppDatabase; -if (IS_LOCAL_ENV) { - const url = process.env.DATABASE_URL_LOCAL?.trim(); +if (DATABASE_URL) { + const sql = neon(DATABASE_URL); + db = drizzleNeon(sql, { schema }); - if (!url) { + if (process.env.NODE_ENV !== 'test') { + console.log('[db] using production database (neon http)'); + } +} else if (IS_LOCAL_ENV) { + if (!DATABASE_URL_LOCAL) { throw new Error('[db] APP_ENV=local requires DATABASE_URL_LOCAL to be set'); } - const pool = new Pool({ - connectionString: url, - }); - + const pool = new Pool({ connectionString: DATABASE_URL_LOCAL }); db = drizzlePg(pool, { schema }); if (process.env.NODE_ENV !== 'test') { console.log('[db] using local PostgreSQL (pg)'); } } else { - const url = process.env.DATABASE_URL?.trim(); - - if (!url) { - throw new Error( - `[db] APP_ENV=${APP_ENV} requires DATABASE_URL to be set` - ); - } - - const sql = neon(url); - - db = drizzleNeon(sql, { schema }); - - if (process.env.NODE_ENV !== 'test') { - console.log('[db] using production database (neon http)'); - } + throw new Error( + `[db] no usable database configuration found (APP_ENV=${APP_ENV ?? 'undefined'})` + ); } + export { db }; diff --git a/frontend/lib/env/auth.ts b/frontend/lib/env/auth.ts index 9b79c9f4..5f131eea 100644 --- a/frontend/lib/env/auth.ts +++ b/frontend/lib/env/auth.ts @@ -1,25 +1,31 @@ -import * as dotenv from 'dotenv'; -dotenv.config(); +import { readServerEnv } from './server-env'; type AppEnv = 'local' | 'develop' | 'production'; -const validAppEnvs = ['local', 'develop', 'production']; -const rawAppEnv = process.env.APP_ENV; +const validAppEnvs: AppEnv[] = ['local', 'develop', 'production']; -if (!rawAppEnv) { - throw new Error('APP_ENV is not defined'); -} +const rawAppEnv = readServerEnv('APP_ENV')?.toLowerCase(); +const context = readServerEnv('CONTEXT')?.toLowerCase(); + +const inferredAppEnv: AppEnv | undefined = + context === 'production' + ? 'production' + : context + ? 'develop' + : undefined; + +const resolvedAppEnv = (rawAppEnv ?? inferredAppEnv) as AppEnv | undefined; -if (!validAppEnvs.includes(rawAppEnv as AppEnv)) { +if (!resolvedAppEnv || !validAppEnvs.includes(resolvedAppEnv)) { throw new Error( - `Invalid APP_ENV: ${rawAppEnv}. Must be one of: ${validAppEnvs.join(', ')}` + `Invalid APP_ENV: ${rawAppEnv ?? ''}. Must be one of: ${validAppEnvs.join(', ')}` ); } -const APP_ENV = rawAppEnv as AppEnv; +const APP_ENV: AppEnv = resolvedAppEnv; function requireEnv(name: string): string { - const value = process.env[name]; + const value = readServerEnv(name); if (!value) { throw new Error(`Missing env var: ${name}`); } @@ -29,16 +35,24 @@ function requireEnv(name: string): string { export const authEnv = { appEnv: APP_ENV, - google: { - clientId: requireEnv('GOOGLE_CLIENT_ID'), - clientSecret: requireEnv('GOOGLE_CLIENT_SECRET'), - redirectUri: - APP_ENV === 'local' - ? requireEnv('GOOGLE_CLIENT_REDIRECT_URI_LOCAL') - : APP_ENV === 'develop' - ? requireEnv('GOOGLE_CLIENT_REDIRECT_URI_DEVELOP') - : requireEnv('GOOGLE_CLIENT_REDIRECT_URI_PROD'), - }, + google: + APP_ENV === 'local' + ? { + clientId: requireEnv('GOOGLE_CLIENT_ID_LOCAL'), + clientSecret: requireEnv('GOOGLE_CLIENT_SECRET_LOCAL'), + redirectUri: requireEnv('GOOGLE_CLIENT_REDIRECT_URI_LOCAL'), + } + : APP_ENV === 'develop' + ? { + clientId: requireEnv('GOOGLE_CLIENT_ID_DEVELOP'), + clientSecret: requireEnv('GOOGLE_CLIENT_SECRET_DEVELOP'), + redirectUri: requireEnv('GOOGLE_CLIENT_REDIRECT_URI_DEVELOP'), + } + : { + clientId: requireEnv('GOOGLE_CLIENT_ID_PROD'), + clientSecret: requireEnv('GOOGLE_CLIENT_SECRET_PROD'), + redirectUri: requireEnv('GOOGLE_CLIENT_REDIRECT_URI_PROD'), + }, github: APP_ENV === 'local' diff --git a/frontend/lib/env/server-env.ts b/frontend/lib/env/server-env.ts new file mode 100644 index 00000000..a1466f16 --- /dev/null +++ b/frontend/lib/env/server-env.ts @@ -0,0 +1,17 @@ +import 'server-only'; + +type NetlifyEnv = { + get?: (key: string) => string | undefined; +}; + +function readFromNetlifyEnv(key: string): string | undefined { + const maybeNetlify = (globalThis as { Netlify?: { env?: NetlifyEnv } }).Netlify; + const value = maybeNetlify?.env?.get?.(key); + return typeof value === 'string' && value.trim() ? value.trim() : undefined; +} + +export function readServerEnv(key: string): string | undefined { + const fromProcess = process.env[key]?.trim(); + if (fromProcess) return fromProcess; + return readFromNetlifyEnv(key); +} diff --git a/frontend/proxy.ts b/frontend/proxy.ts index dae0ce43..6db449b6 100644 --- a/frontend/proxy.ts +++ b/frontend/proxy.ts @@ -8,10 +8,10 @@ import { routing } from './i18n/routing'; const AUTH_COOKIE_NAME = 'auth_session'; -const AUTH_SECRET = process.env.AUTH_SECRET; -if (!AUTH_SECRET) { - throw new Error('AUTH_SECRET is not defined'); -} +// const AUTH_SECRET = process.env.AUTH_SECRET; +// if (!AUTH_SECRET) { +// throw new Error('AUTH_SECRET is not defined'); +// } function decodeAuthToken(token: string): AuthTokenPayload | null { const parts = token.split('.'); diff --git a/frontend/scripts/generate-env.sh b/frontend/scripts/generate-env.sh deleted file mode 100644 index 953d8216..00000000 --- a/frontend/scripts/generate-env.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# Generate .env from .env.example allowlist using current environment. -# Only variables listed in .env.example are included — no platform internals leak. -grep '^[A-Z]' .env.example | cut -d= -f1 | while read -r var; do - val="${!var}" - [ -n "$val" ] && printf '%s=%s\n' "$var" "$val" -done > .env diff --git a/netlify.toml b/netlify.toml index e071879d..12f80de1 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,6 @@ [build] base = "frontend" - command = "bash scripts/generate-env.sh && npm ci --include=optional && npm run build" + command = "npm ci --include=optional && npm run build" publish = ".next" [build.environment]