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..2d119b5c 100644 --- a/frontend/lib/auth/admin.ts +++ b/frontend/lib/auth/admin.ts @@ -3,6 +3,7 @@ import 'server-only'; import type { NextRequest } from 'next/server'; import { getCurrentUser } from '@/lib/auth'; +import { readServerEnv } from '@/lib/env/server-env'; export class AdminApiDisabledError extends Error { code = 'ADMIN_API_DISABLED' as const; @@ -29,10 +30,14 @@ export class AdminForbiddenError extends Error { } export function assertAdminApiEnabled(): void { - if ( - process.env.NODE_ENV === 'production' && - process.env.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 07ebdd97..ec92b94d 100644 --- a/frontend/lib/env/server-env.ts +++ b/frontend/lib/env/server-env.ts @@ -41,6 +41,14 @@ 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', + '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 4fcf6968..c5b33058 100644 --- a/frontend/lib/shop/status-token.ts +++ b/frontend/lib/shop/status-token.ts @@ -1,5 +1,7 @@ import crypto from 'node:crypto'; +import { readServerEnv } from '@/lib/env/server-env'; + export const STATUS_TOKEN_SCOPES = [ 'status_lite', 'order_payment_init', @@ -27,7 +29,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');