diff --git a/.env b/.env index a00df10b..28be54ac 100644 --- a/.env +++ b/.env @@ -15,7 +15,6 @@ UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js # GlitchTip (Sentry protocol) SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1 -NEXT_PUBLIC_SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@klz-cables.com/errors/1 # Redis Cache REDIS_URL= diff --git a/instrumentation.ts b/instrumentation.ts index 148b21d4..f0cc2640 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,8 +1,19 @@ import * as Sentry from '@sentry/nextjs'; +import { getServerAppServices } from '@/lib/services/create-services.server'; -// Next.js will call this on boot for the active runtime. -// We dynamically import the correct Sentry config file. +/** + * Next.js will call this on boot for the active runtime. + * + * NEXT_RUNTIME is an environment variable automatically set by Next.js: + * - 'nodejs' when running in the standard Node.js runtime + * - 'edge' when running in the Edge runtime (e.g. Middleware, Edge API Routes) + */ export async function register() { + // Initialize server services on boot + if (process.env.NEXT_RUNTIME === 'nodejs') { + getServerAppServices(); + } + if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./sentry.server.config'); } diff --git a/lib/config.ts b/lib/config.ts index 5830b735..76c9c381 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -2,64 +2,76 @@ * Centralized configuration management for the application. * This file defines the schema and provides a type-safe way to access environment variables. */ +import dotenv from 'dotenv'; +import path from 'path'; + +// Load .env file in development or if not already loaded +if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { + dotenv.config({ path: path.resolve(process.cwd(), '.env') }); +} + +const getEnv = (key: string, defaultValue?: string): string | undefined => { + if (typeof process === 'undefined') return defaultValue; + return process.env[key] || defaultValue; +}; export const config = { - env: process.env.NODE_ENV || 'development', - isProduction: process.env.NODE_ENV === 'production', - isDevelopment: process.env.NODE_ENV === 'development', - isTest: process.env.NODE_ENV === 'test', + env: getEnv('NODE_ENV', 'development'), + isProduction: getEnv('NODE_ENV') === 'production', + isDevelopment: getEnv('NODE_ENV') === 'development', + isTest: getEnv('NODE_ENV') === 'test', - baseUrl: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000', + baseUrl: getEnv('NEXT_PUBLIC_BASE_URL', 'http://localhost:3000'), analytics: { umami: { - websiteId: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, - scriptUrl: process.env.UMAMI_SCRIPT_URL || 'https://analytics.infra.mintel.me/script.js', + websiteId: getEnv('NEXT_PUBLIC_UMAMI_WEBSITE_ID'), + scriptUrl: getEnv('UMAMI_SCRIPT_URL', 'https://analytics.infra.mintel.me/script.js'), // The proxied path used in the frontend proxyPath: '/stats/script.js', - enabled: Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID), + enabled: Boolean(getEnv('NEXT_PUBLIC_UMAMI_WEBSITE_ID')), }, }, errors: { glitchtip: { // Use SENTRY_DSN for both server and client (proxied) - dsn: process.env.SENTRY_DSN, + dsn: getEnv('SENTRY_DSN'), // The proxied origin used in the frontend proxyPath: '/errors', - enabled: Boolean(process.env.SENTRY_DSN), + enabled: Boolean(getEnv('SENTRY_DSN')), }, }, cache: { redis: { - url: process.env.REDIS_URL, - keyPrefix: process.env.REDIS_KEY_PREFIX || 'klz:', - enabled: Boolean(process.env.REDIS_URL), + url: getEnv('REDIS_URL'), + keyPrefix: getEnv('REDIS_KEY_PREFIX', 'klz:'), + enabled: Boolean(getEnv('REDIS_URL')), }, }, logging: { - level: process.env.LOG_LEVEL || 'info', + level: getEnv('LOG_LEVEL', 'info'), }, mail: { - host: process.env.MAIL_HOST, - port: parseInt(process.env.MAIL_PORT || '587', 10), - user: process.env.MAIL_USERNAME, - pass: process.env.MAIL_PASSWORD, - from: process.env.MAIL_FROM, - recipients: process.env.MAIL_RECIPIENTS?.split(',') || [], + host: getEnv('MAIL_HOST'), + port: parseInt(getEnv('MAIL_PORT', '587')!, 10), + user: getEnv('MAIL_USERNAME'), + pass: getEnv('MAIL_PASSWORD'), + from: getEnv('MAIL_FROM'), + recipients: getEnv('MAIL_RECIPIENTS', '')?.split(',').filter(Boolean) || [], }, woocommerce: { - url: process.env.WOOCOMMERCE_URL, - consumerKey: process.env.WOOCOMMERCE_CONSUMER_KEY, - consumerSecret: process.env.WOOCOMMERCE_CONSUMER_SECRET, + url: getEnv('WOOCOMMERCE_URL'), + consumerKey: getEnv('WOOCOMMERCE_CONSUMER_KEY'), + consumerSecret: getEnv('WOOCOMMERCE_CONSUMER_SECRET'), }, wordpress: { - appPassword: process.env.WORDPRESS_APP_PASSWORD, + appPassword: getEnv('WORDPRESS_APP_PASSWORD'), }, } as const; diff --git a/lib/mail/mailer.ts b/lib/mail/mailer.ts index 7bf85e75..f0c5f7b3 100644 --- a/lib/mail/mailer.ts +++ b/lib/mail/mailer.ts @@ -2,14 +2,15 @@ import nodemailer from "nodemailer"; import { render } from "@react-email/components"; import { ReactElement } from "react"; import { getServerAppServices } from "@/lib/services/create-services.server"; +import { config } from "../config"; const transporter = nodemailer.createTransport({ - host: process.env.MAIL_HOST, - port: Number(process.env.MAIL_PORT), - secure: Number(process.env.MAIL_PORT) === 465, + host: config.mail.host, + port: config.mail.port, + secure: config.mail.port === 465, auth: { - user: process.env.MAIL_USERNAME, - pass: process.env.MAIL_PASSWORD, + user: config.mail.user, + pass: config.mail.pass, }, }); @@ -22,10 +23,10 @@ interface SendEmailOptions { export async function sendEmail({ to, subject, template }: SendEmailOptions) { const html = await render(template); - const recipients = to || process.env.MAIL_RECIPIENTS?.split(",") || []; + const recipients = to || config.mail.recipients; const mailOptions = { - from: process.env.MAIL_FROM, + from: config.mail.from, to: recipients, subject, html, diff --git a/lib/services/create-services.ts b/lib/services/create-services.ts index b41c281d..f3982bfc 100644 --- a/lib/services/create-services.ts +++ b/lib/services/create-services.ts @@ -5,6 +5,7 @@ import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporti import { NoopErrorReportingService } from './errors/noop-error-reporting-service'; import { NoopLoggerService } from './logging/noop-logger-service'; import { PinoLoggerService } from './logging/pino-logger-service'; +import { config, getMaskedConfig } from '../config'; /** * Singleton instance of AppServices. @@ -74,48 +75,28 @@ export function getAppServices(): AppServices { ? new PinoLoggerService('server') : new NoopLoggerService(); - // Log environment variables (safely masked) - const envLog = { - // Mask sensitive values - show only last 4 characters or '***' for empty - NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID - ? `***${process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID.slice(-4)}` - : 'not set', - UMAMI_SCRIPT_URL: process.env.UMAMI_SCRIPT_URL ?? 'not set', - NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN - ? `***${process.env.NEXT_PUBLIC_SENTRY_DSN.slice(-4)}` - : 'not set', - SENTRY_DSN: process.env.SENTRY_DSN - ? `***${process.env.SENTRY_DSN.slice(-4)}` - : 'not set', - // Safe to show - no sensitive data - NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL ?? 'not set', - NODE_ENV: process.env.NODE_ENV ?? 'not set', - }; - // Log initialization if (typeof window === 'undefined') { // Server-side logger.info('Initializing server application services', { - environment: envLog, + environment: getMaskedConfig(), timestamp: new Date().toISOString(), }); } else { // Client-side logger.info('Initializing client application services', { - environment: envLog, + environment: getMaskedConfig(), timestamp: new Date().toISOString(), }); } // Determine which services to enable based on environment variables - const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID); - const sentryClientEnabled = Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN); - const sentryServerEnabled = Boolean(process.env.SENTRY_DSN); + const umamiEnabled = config.analytics.umami.enabled; + const sentryEnabled = config.errors.glitchtip.enabled; logger.info('Service configuration', { umamiEnabled, - sentryClientEnabled, - sentryServerEnabled, + sentryEnabled, isServer: typeof window === 'undefined', }); @@ -135,20 +116,12 @@ export function getAppServices(): AppServices { } // Create error reporting service (GlitchTip/Sentry or no-op) - // Server-side and client-side have separate DSNs - const errors = - typeof window === 'undefined' - ? sentryServerEnabled - ? new GlitchtipErrorReportingService({ enabled: true }) - : new NoopErrorReportingService() - : sentryClientEnabled - ? new GlitchtipErrorReportingService({ enabled: true }) - : new NoopErrorReportingService(); + const errors = sentryEnabled + ? new GlitchtipErrorReportingService({ enabled: true }) + : new NoopErrorReportingService(); - if (typeof window === 'undefined' && sentryServerEnabled) { - logger.info('GlitchTip error reporting service initialized (server)'); - } else if (typeof window !== 'undefined' && sentryClientEnabled) { - logger.info('GlitchTip error reporting service initialized (client)'); + if (sentryEnabled) { + logger.info(`GlitchTip error reporting service initialized (${typeof window === 'undefined' ? 'server' : 'client'})`); } else { logger.info('Noop error reporting service initialized (error reporting disabled)'); } @@ -161,7 +134,7 @@ export function getAppServices(): AppServices { logger.info('Pino logger service initialized', { name: typeof window === 'undefined' ? 'server' : 'client', - level: process.env.LOG_LEVEL ?? 'info', + level: config.logging.level, }); // Create and cache the singleton