/** * Application Configuration * Centralized configuration management */ import { readFileSync, existsSync } from 'fs' import { dirname, join } from 'path' import { fileURLToPath } from 'url' import { log } from '../middleware/logger.mjs' const _configDir = dirname(fileURLToPath(import.meta.url)) const _repoRoot = join(_configDir, '..', '..') /** Default dev API port from repo root `mailflow.dev.port.json` (avoids clashes with other apps on 3000). */ function readDevPortFromFile() { try { const f = join(_repoRoot, 'mailflow.dev.port.json') if (!existsSync(f)) return 3030 const j = JSON.parse(readFileSync(f, 'utf8')) const p = parseInt(String(j.port), 10) return Number.isFinite(p) && p > 0 ? p : 3030 } catch { return 3030 } } const nodeEnv = process.env.NODE_ENV || 'development' const listenPort = process.env.PORT ? parseInt(process.env.PORT, 10) : nodeEnv === 'production' ? 3000 : readDevPortFromFile() const defaultLocalBase = `http://localhost:${listenPort}` /** * Environment configuration */ export const config = { // Server port: listenPort, nodeEnv, isDev: nodeEnv !== 'production', isProd: nodeEnv === 'production', // URLs baseUrl: process.env.BASE_URL || defaultLocalBase, frontendUrl: process.env.FRONTEND_URL || 'http://localhost:5173', // Appwrite appwrite: { endpoint: process.env.APPWRITE_ENDPOINT, projectId: process.env.APPWRITE_PROJECT_ID, apiKey: process.env.APPWRITE_API_KEY, databaseId: process.env.APPWRITE_DATABASE_ID, }, // Stripe stripe: { secretKey: process.env.STRIPE_SECRET_KEY, webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, prices: { basic: process.env.STRIPE_PRICE_BASIC || 'price_basic_monthly', pro: process.env.STRIPE_PRICE_PRO || 'price_pro_monthly', business: process.env.STRIPE_PRICE_BUSINESS || 'price_business_monthly', }, }, // Google OAuth google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, redirectUri: process.env.GOOGLE_REDIRECT_URI || `${defaultLocalBase}/api/oauth/gmail/callback`, }, // Microsoft OAuth microsoft: { clientId: process.env.MICROSOFT_CLIENT_ID, clientSecret: process.env.MICROSOFT_CLIENT_SECRET, redirectUri: process.env.MICROSOFT_REDIRECT_URI || `${defaultLocalBase}/api/oauth/outlook/callback`, }, // Mistral AI mistral: { apiKey: process.env.MISTRAL_API_KEY, }, // Rate Limiting rateLimit: { windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000', 10), max: parseInt(process.env.RATE_LIMIT_MAX || '100', 10), }, // CORS (Dev: localhost + 127.0.0.1 für Vite, Browser ruft API oft direkt auf 127.0.0.1:PORT) cors: { origin: process.env.NODE_ENV === 'production' ? process.env.CORS_ORIGIN || process.env.FRONTEND_URL || 'http://localhost:5173' : [ process.env.CORS_ORIGIN, process.env.FRONTEND_URL, 'http://localhost:5173', 'http://127.0.0.1:5173', ] .filter(Boolean) .filter((o, i, a) => a.indexOf(o) === i), credentials: true, }, // Free Tier Limits freeTier: { emailsPerMonth: parseInt(process.env.FREE_TIER_EMAILS_PER_MONTH || '500', 10), emailAccounts: 1, autoSchedule: false, // manual only }, /** Highest product tier (admin comped plan, PLANS key in stripe.mjs). Optional env: TOP_SUBSCRIPTION_PLAN */ topSubscriptionPlan: (process.env.TOP_SUBSCRIPTION_PLAN || 'business').trim().toLowerCase(), // Admin: comma-separated list of emails with admin rights (e.g. support). // support@webklar.com is always included; env adds more. adminEmails: (() => { const fromEnv = (process.env.ADMIN_EMAILS || '') .split(',') .map((e) => e.trim().toLowerCase()) .filter(Boolean) return [...new Set(['support@webklar.com', ...fromEnv])] })(), // Gitea Webhook (Deployment) — trim: trailing newlines in .env break HMAC/Bearer match gitea: (() => { const secret = (process.env.GITEA_WEBHOOK_SECRET || '').trim() const auth = (process.env.GITEA_WEBHOOK_AUTH_TOKEN || '').trim() return { webhookSecret: secret, webhookAuthToken: auth || secret, } })(), /** HMAC secret for Gmail/Outlook OAuth state (recommended in production) */ oauthStateSecret: process.env.OAUTH_STATE_SECRET || '', } /** * Required environment variables */ const requiredVars = [ 'APPWRITE_ENDPOINT', 'APPWRITE_PROJECT_ID', 'APPWRITE_API_KEY', 'APPWRITE_DATABASE_ID', 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', ] /** * Optional but recommended variables */ const recommendedVars = [ 'MISTRAL_API_KEY', 'GOOGLE_CLIENT_ID', 'MICROSOFT_CLIENT_ID', ] /** * Validate configuration */ export function validateConfig() { const missing = [] const warnings = [] // Check required variables for (const varName of requiredVars) { if (!process.env[varName]) { missing.push(varName) } } if (missing.length > 0) { log.error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`) process.exit(1) } // Check recommended variables for (const varName of recommendedVars) { if (!process.env[varName]) { warnings.push(varName) } } if (warnings.length > 0) { log.warn(`Optionale Variablen fehlen: ${warnings.join(', ')}`) } log.success('Konfiguration validiert') return true } /** * Feature flags based on available config */ export const features = { gmail: () => Boolean(config.google.clientId && config.google.clientSecret), outlook: () => Boolean(config.microsoft.clientId && config.microsoft.clientSecret), ai: () => Boolean(config.mistral.apiKey), } /** * Check if an email has admin rights (support, etc.) */ export function isAdmin(email) { if (!email || typeof email !== 'string') return false return config.adminEmails.includes(email.trim().toLowerCase()) } export default config