env
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m36s

This commit is contained in:
2026-01-27 00:30:12 +01:00
parent 3cab376cd1
commit 5c71e9a064
7 changed files with 131 additions and 46 deletions

View File

@@ -50,6 +50,7 @@ export default function AnalyticsProvider() {
src="/stats/script.js"
data-website-id={websiteId}
strategy="afterInteractive"
data-domains="klz-cables.com"
defer
/>
);

102
lib/config.ts Normal file
View File

@@ -0,0 +1,102 @@
/**
* Centralized configuration management for the application.
* This file defines the schema and provides a type-safe way to access environment variables.
*/
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',
baseUrl: process.env.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',
// The proxied path used in the frontend
proxyPath: '/stats/script.js',
enabled: Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID),
},
},
errors: {
glitchtip: {
// Server-side DSN (direct to GlitchTip)
dsn: process.env.SENTRY_DSN,
// Client-side DSN (proxied through Next.js)
publicDsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// The proxied origin used in the frontend
proxyPath: '/errors',
enabled: Boolean(process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN),
},
},
cache: {
redis: {
url: process.env.REDIS_URL,
keyPrefix: process.env.REDIS_KEY_PREFIX || 'klz:',
enabled: Boolean(process.env.REDIS_URL),
},
},
logging: {
level: process.env.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(',') || [],
},
woocommerce: {
url: process.env.WOOCOMMERCE_URL,
consumerKey: process.env.WOOCOMMERCE_CONSUMER_KEY,
consumerSecret: process.env.WOOCOMMERCE_CONSUMER_SECRET,
},
wordpress: {
appPassword: process.env.WORDPRESS_APP_PASSWORD,
},
} as const;
/**
* Helper to get a masked version of the config for logging.
*/
export function getMaskedConfig() {
const mask = (val: string | undefined) => (val ? `***${val.slice(-4)}` : 'not set');
return {
env: config.env,
baseUrl: config.baseUrl,
analytics: {
umami: {
websiteId: mask(config.analytics.umami.websiteId),
scriptUrl: config.analytics.umami.scriptUrl,
enabled: config.analytics.umami.enabled,
},
},
errors: {
glitchtip: {
dsn: mask(config.errors.glitchtip.dsn),
publicDsn: mask(config.errors.glitchtip.publicDsn),
enabled: config.errors.glitchtip.enabled,
},
},
cache: {
redis: {
url: mask(config.cache.redis.url),
keyPrefix: config.cache.redis.keyPrefix,
enabled: config.cache.redis.enabled,
},
},
logging: {
level: config.logging.level,
},
};
}

View File

@@ -75,8 +75,9 @@ export class UmamiAnalyticsService implements AnalyticsService {
// Server-side tracking via proxy
if (typeof window === 'undefined') {
const { getServerAppServices } = require('../create-services.server');
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
const umamiUrl = process.env.UMAMI_SCRIPT_URL?.replace('/script.js', '') || 'https://analytics.infra.mintel.me';
const { config } = require('../../config');
const websiteId = config.analytics.umami.websiteId;
const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', '');
if (!websiteId) return;
@@ -123,8 +124,9 @@ export class UmamiAnalyticsService implements AnalyticsService {
// Server-side tracking via proxy
if (typeof window === 'undefined') {
const { getServerAppServices } = require('../create-services.server');
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
const umamiUrl = process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL?.replace('/script.js', '') || 'https://analytics.infra.mintel.me';
const { config } = require('../../config');
const websiteId = config.analytics.umami.websiteId;
const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', '');
if (!websiteId || !url) return;

View File

@@ -6,6 +6,7 @@ import { RedisCacheService } from './cache/redis-cache-service';
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
import { PinoLoggerService } from './logging/pino-logger-service';
import { config, getMaskedConfig } from '../config';
let singleton: AppServices | undefined;
@@ -15,71 +16,47 @@ export function getServerAppServices(): AppServices {
// Create logger first to log initialization
const logger = new PinoLoggerService('server');
// 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',
SENTRY_DSN: process.env.SENTRY_DSN
? `***${process.env.SENTRY_DSN.slice(-4)}`
: 'not set',
REDIS_URL: process.env.REDIS_URL
? `***${process.env.REDIS_URL.slice(-4)}`
: 'not set',
REDIS_KEY_PREFIX: process.env.REDIS_KEY_PREFIX ?? 'klz:',
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
NODE_ENV: process.env.NODE_ENV ?? 'not set',
// Safe to show - no sensitive data
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL ?? 'not set',
};
logger.info('Initializing server application services', {
environment: envLog,
environment: getMaskedConfig(),
timestamp: new Date().toISOString(),
});
const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
const sentryEnabled = Boolean(process.env.SENTRY_DSN);
logger.info('Service configuration', {
umamiEnabled,
sentryEnabled,
redisEnabled: Boolean(process.env.REDIS_URL),
umamiEnabled: config.analytics.umami.enabled,
sentryEnabled: config.errors.glitchtip.enabled,
redisEnabled: config.cache.redis.enabled,
});
const analytics = umamiEnabled
const analytics = config.analytics.umami.enabled
? new UmamiAnalyticsService({ enabled: true })
: new NoopAnalyticsService();
if (umamiEnabled) {
if (config.analytics.umami.enabled) {
logger.info('Umami analytics service initialized');
} else {
logger.info('Noop analytics service initialized (analytics disabled)');
}
const errors = sentryEnabled
const errors = config.errors.glitchtip.enabled
? new GlitchtipErrorReportingService({ enabled: true })
: new NoopErrorReportingService();
if (sentryEnabled) {
if (config.errors.glitchtip.enabled) {
logger.info('GlitchTip error reporting service initialized');
} else {
logger.info('Noop error reporting service initialized (error reporting disabled)');
}
const redisUrl = process.env.REDIS_URL;
const cache = redisUrl
const cache = config.cache.redis.enabled && config.cache.redis.url
? new RedisCacheService({
url: redisUrl,
keyPrefix: process.env.REDIS_KEY_PREFIX ?? 'klz:',
url: config.cache.redis.url,
keyPrefix: config.cache.redis.keyPrefix,
})
: new MemoryCacheService();
if (redisUrl) {
if (config.cache.redis.enabled) {
logger.info('Redis cache service initialized', {
keyPrefix: process.env.REDIS_KEY_PREFIX ?? 'klz:'
keyPrefix: config.cache.redis.keyPrefix
});
} else {
logger.info('Memory cache service initialized (Redis not configured)');
@@ -87,7 +64,7 @@ export function getServerAppServices(): AppServices {
logger.info('Pino logger service initialized', {
name: 'server',
level: process.env.LOG_LEVEL ?? 'info',
level: config.logging.level,
});
singleton = new AppServices(analytics, errors, cache, logger);

View File

@@ -1,5 +1,6 @@
import pino, { Logger as PinoLogger } from 'pino';
import type { LoggerService } from './logger-service';
import { config } from '../../config';
export class PinoLoggerService implements LoggerService {
private readonly logger: PinoLogger;
@@ -10,9 +11,9 @@ export class PinoLoggerService implements LoggerService {
} else {
this.logger = pino({
name: name || 'app',
level: process.env.LOG_LEVEL || 'info',
level: config.logging.level,
transport:
process.env.NODE_ENV !== 'production'
!config.isProduction
? {
target: 'pino-pretty',
options: {

View File

@@ -344,7 +344,9 @@ const nextConfig = {
},
async rewrites() {
const umamiUrl = (process.env.UMAMI_SCRIPT_URL || 'https://analytics.infra.mintel.me').replace('/script.js', '');
const glitchtipUrl = process.env.SENTRY_DSN ? new URL(process.env.SENTRY_DSN).origin : 'https://errors.infra.mintel.me';
const glitchtipUrl = process.env.SENTRY_DSN
? new URL(process.env.SENTRY_DSN).origin
: 'https://errors.infra.mintel.me';
return [
{

File diff suppressed because one or more lines are too long