From c14a6a61e45a4fdda59f2b2125ffb9ab763e8dd5 Mon Sep 17 00:00:00 2001 From: Lesia Soloviova Date: Sun, 29 Mar 2026 19:54:49 +0100 Subject: [PATCH 1/2] fix(env): read critical runtime flags via readServerEnv for Netlify SSR - switch admin feature flag resolution in locale layout to readServerEnv - update admin guard to use readServerEnv(ENABLE_ADMIN_API) - update shop status token secret read to readServerEnv - extend generated runtime fallback allowlist with: ENABLE_ADMIN_API, NEXT_PUBLIC_ENABLE_ADMIN, SHOP_STATUS_TOKEN_SECRET - preserve Vercel-safe runtime-env.generated.ts stub import flow --- frontend/app/[locale]/layout.tsx | 5 +++-- frontend/lib/auth/admin.ts | 3 ++- frontend/lib/env/server-env.ts | 3 +++ frontend/lib/shop/status-token.ts | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/app/[locale]/layout.tsx b/frontend/app/[locale]/layout.tsx index 3cd94956..fbd4dbae 100644 --- a/frontend/app/[locale]/layout.tsx +++ b/frontend/app/[locale]/layout.tsx @@ -13,6 +13,7 @@ import { ThemeProvider } from '@/components/theme/ThemeProvider'; import { getCachedBlogCategories } from '@/db/queries/blog/blog-categories'; import { AuthProvider } from '@/hooks/useAuth'; import { locales } from '@/i18n/config'; +import { readServerEnv } from '@/lib/env/server-env'; export default async function LocaleLayout({ children, @@ -32,8 +33,8 @@ export default async function LocaleLayout({ const enableAdmin = ( - process.env.ENABLE_ADMIN_API ?? - process.env.NEXT_PUBLIC_ENABLE_ADMIN ?? + readServerEnv('ENABLE_ADMIN_API') ?? + readServerEnv('NEXT_PUBLIC_ENABLE_ADMIN') ?? '' ).toLowerCase() === 'true'; diff --git a/frontend/lib/auth/admin.ts b/frontend/lib/auth/admin.ts index b5682545..afd7ffe2 100644 --- a/frontend/lib/auth/admin.ts +++ b/frontend/lib/auth/admin.ts @@ -1,6 +1,7 @@ import 'server-only'; import type { NextRequest } from 'next/server'; +import { readServerEnv } from '@/lib/env/server-env'; import { getCurrentUser } from '@/lib/auth'; @@ -31,7 +32,7 @@ export class AdminForbiddenError extends Error { export function assertAdminApiEnabled(): void { if ( process.env.NODE_ENV === 'production' && - process.env.ENABLE_ADMIN_API !== 'true' + readServerEnv('ENABLE_ADMIN_API') !== 'true' ) { throw new AdminApiDisabledError(); } diff --git a/frontend/lib/env/server-env.ts b/frontend/lib/env/server-env.ts index 07ebdd97..1f33a13d 100644 --- a/frontend/lib/env/server-env.ts +++ b/frontend/lib/env/server-env.ts @@ -41,6 +41,9 @@ const GENERATED_FALLBACK_KEYS = new Set([ 'GITHUB_CLIENT_ID_DEVELOP', 'GITHUB_CLIENT_SECRET_DEVELOP', 'GITHUB_CLIENT_REDIRECT_URI_DEVELOP', + 'ENABLE_ADMIN_API', + 'NEXT_PUBLIC_ENABLE_ADMIN', + 'SHOP_STATUS_TOKEN_SECRET', ]); diff --git a/frontend/lib/shop/status-token.ts b/frontend/lib/shop/status-token.ts index 4fcf6968..7ad760ef 100644 --- a/frontend/lib/shop/status-token.ts +++ b/frontend/lib/shop/status-token.ts @@ -1,4 +1,5 @@ import crypto from 'node:crypto'; +import { readServerEnv } from '@/lib/env/server-env'; export const STATUS_TOKEN_SCOPES = [ 'status_lite', @@ -27,7 +28,7 @@ type TokenPayload = { const DEFAULT_TTL_SECONDS = 45 * 60; function getSecret(): string { - const raw = process.env.SHOP_STATUS_TOKEN_SECRET ?? ''; + const raw = readServerEnv('SHOP_STATUS_TOKEN_SECRET') ?? ''; const trimmed = raw.trim(); if (!trimmed) { throw new Error('SHOP_STATUS_TOKEN_SECRET is not configured'); From 9cc3a536992361806f9742151a02e2ad882e7422 Mon Sep 17 00:00:00 2001 From: Lesia Soloviova Date: Sun, 29 Mar 2026 20:21:14 +0100 Subject: [PATCH 2/2] fix(env): migrate origin/email env reads to readServerEnv and extend fallback keys --- frontend/lib/auth/admin.ts | 14 +++++++++----- frontend/lib/email/sendPasswordResetEmail.ts | 4 +++- frontend/lib/email/sendVerificationEmail.ts | 4 +++- frontend/lib/email/transporter.ts | 6 ++++-- frontend/lib/env/server-env.ts | 5 +++++ frontend/lib/security/origin.ts | 6 ++++-- frontend/lib/shop/status-token.ts | 1 + 7 files changed, 29 insertions(+), 11 deletions(-) diff --git a/frontend/lib/auth/admin.ts b/frontend/lib/auth/admin.ts index afd7ffe2..2d119b5c 100644 --- a/frontend/lib/auth/admin.ts +++ b/frontend/lib/auth/admin.ts @@ -1,9 +1,9 @@ import 'server-only'; import type { NextRequest } from 'next/server'; -import { readServerEnv } from '@/lib/env/server-env'; import { getCurrentUser } from '@/lib/auth'; +import { readServerEnv } from '@/lib/env/server-env'; export class AdminApiDisabledError extends Error { code = 'ADMIN_API_DISABLED' as const; @@ -30,10 +30,14 @@ export class AdminForbiddenError extends Error { } export function assertAdminApiEnabled(): void { - if ( - process.env.NODE_ENV === 'production' && - readServerEnv('ENABLE_ADMIN_API') !== 'true' - ) { + const enabled = + ( + readServerEnv('ENABLE_ADMIN_API') ?? + readServerEnv('NEXT_PUBLIC_ENABLE_ADMIN') ?? + '' + ).toLowerCase() === 'true'; + + if (process.env.NODE_ENV === 'production' && !enabled) { throw new AdminApiDisabledError(); } } diff --git a/frontend/lib/email/sendPasswordResetEmail.ts b/frontend/lib/email/sendPasswordResetEmail.ts index 36138b71..03b2135d 100644 --- a/frontend/lib/email/sendPasswordResetEmail.ts +++ b/frontend/lib/email/sendPasswordResetEmail.ts @@ -1,3 +1,5 @@ +import { readServerEnv } from '@/lib/env/server-env'; + import { resetPasswordTemplate } from './templates/reset-password'; import { mailer } from './transporter'; @@ -7,7 +9,7 @@ type Params = { }; export async function sendPasswordResetEmail({ to, resetUrl }: Params) { - const from = process.env.EMAIL_FROM; + const from = readServerEnv('EMAIL_FROM'); if (!from) { throw new Error('EMAIL_FROM is not configured'); diff --git a/frontend/lib/email/sendVerificationEmail.ts b/frontend/lib/email/sendVerificationEmail.ts index f867ee81..1c592525 100644 --- a/frontend/lib/email/sendVerificationEmail.ts +++ b/frontend/lib/email/sendVerificationEmail.ts @@ -1,3 +1,5 @@ +import { readServerEnv } from '@/lib/env/server-env'; + import { verifyEmailTemplate } from './templates/verify-email'; import { mailer } from './transporter'; @@ -7,7 +9,7 @@ type Params = { }; export async function sendVerificationEmail({ to, verifyUrl }: Params) { - const from = process.env.EMAIL_FROM; + const from = readServerEnv('EMAIL_FROM'); if (!from) { throw new Error('EMAIL_FROM is not configured'); diff --git a/frontend/lib/email/transporter.ts b/frontend/lib/email/transporter.ts index 2de3c0e1..e14f09fd 100644 --- a/frontend/lib/email/transporter.ts +++ b/frontend/lib/email/transporter.ts @@ -1,7 +1,9 @@ import nodemailer from 'nodemailer'; -const user = process.env.GMAIL_USER; -const pass = process.env.GMAIL_APP_PASSWORD; +import { readServerEnv } from '@/lib/env/server-env'; + +const user = readServerEnv('GMAIL_USER'); +const pass = readServerEnv('GMAIL_APP_PASSWORD'); if (!user || !pass) { throw new Error('Missing Gmail SMTP credentials'); diff --git a/frontend/lib/env/server-env.ts b/frontend/lib/env/server-env.ts index 1f33a13d..ec92b94d 100644 --- a/frontend/lib/env/server-env.ts +++ b/frontend/lib/env/server-env.ts @@ -44,6 +44,11 @@ const GENERATED_FALLBACK_KEYS = new Set([ 'ENABLE_ADMIN_API', 'NEXT_PUBLIC_ENABLE_ADMIN', 'SHOP_STATUS_TOKEN_SECRET', + 'APP_ORIGIN', + 'APP_ADDITIONAL_ORIGINS', + 'GMAIL_USER', + 'GMAIL_APP_PASSWORD', + 'EMAIL_FROM', ]); diff --git a/frontend/lib/security/origin.ts b/frontend/lib/security/origin.ts index ab147b79..11110407 100644 --- a/frontend/lib/security/origin.ts +++ b/frontend/lib/security/origin.ts @@ -1,5 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; +import { readServerEnv } from '@/lib/env/server-env'; + const LOCALHOST_ORIGIN = 'http://localhost:3000'; function buildErrorResponse( @@ -49,13 +51,13 @@ export function normalizeOrigin(input: string): string { export function getAllowedOrigins(): string[] { const allowed = new Set(); - const appOrigin = (process.env.APP_ORIGIN ?? '').trim(); + const appOrigin = (readServerEnv('APP_ORIGIN') ?? '').trim(); if (appOrigin) { const normalized = normalizeOrigin(appOrigin); if (normalized) allowed.add(normalized); } - const additionalRaw = (process.env.APP_ADDITIONAL_ORIGINS ?? '').trim(); + const additionalRaw = (readServerEnv('APP_ADDITIONAL_ORIGINS') ?? '').trim(); if (additionalRaw) { for (const entry of additionalRaw.split(',')) { const candidate = entry.trim(); diff --git a/frontend/lib/shop/status-token.ts b/frontend/lib/shop/status-token.ts index 7ad760ef..c5b33058 100644 --- a/frontend/lib/shop/status-token.ts +++ b/frontend/lib/shop/status-token.ts @@ -1,4 +1,5 @@ import crypto from 'node:crypto'; + import { readServerEnv } from '@/lib/env/server-env'; export const STATUS_TOKEN_SCOPES = [