import { promises as fs } from "fs"; import path from "path"; import { PrismaClient } from "@prisma/client"; export type SystemConfigData = { app?: { initialized?: boolean }; db?: { databaseUrl?: string }; auth?: { jwtSecret?: string }; email?: { enabled?: boolean; provider?: "smtp"; fromAddress?: string; from?: string; smtp?: { host?: string; port?: number; user?: string; pass?: string }; }; stripe?: { enabled?: boolean; secretKey?: string; webhookSecret?: string; publishableKey?: string; }; oauth?: { google?: { enabled?: boolean; clientId?: string; clientSecret?: string }; github?: { enabled?: boolean; clientId?: string; clientSecret?: string }; facebook?: { enabled?: boolean; clientId?: string; clientSecret?: string }; discord?: { enabled?: boolean; clientId?: string; clientSecret?: string }; }; googleAuth?: { enabled?: boolean; clientId?: string; clientSecret?: string; }; googleCalendar?: { enabled?: boolean; serviceAccountEmail?: string; serviceAccountKey?: string; calendarId?: string; }; }; const DATA_DIR = process.env.CONFIG_DIR || path.join(process.cwd(), "data"); const FILE_PATH = path.join(DATA_DIR, "system-config.json"); const defaultConfig: SystemConfigData = { app: { initialized: false }, db: { databaseUrl: process.env.DATABASE_URL }, auth: { jwtSecret: process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET }, email: { enabled: Boolean(process.env.EMAIL_FROM), provider: (process.env.EMAIL_PROVIDER as any) || undefined, fromAddress: process.env.EMAIL_FROM, from: process.env.EMAIL_FROM, smtp: { host: process.env.SMTP_HOST, port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : undefined, user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }, stripe: { enabled: Boolean(process.env.STRIPE_SECRET_KEY), secretKey: process.env.STRIPE_SECRET_KEY, webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, }, oauth: { google: { enabled: Boolean(process.env.GOOGLE_CLIENT_ID), clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, github: { enabled: Boolean(process.env.GITHUB_CLIENT_ID), clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }, facebook: { enabled: Boolean(process.env.FACEBOOK_CLIENT_ID), clientId: process.env.FACEBOOK_CLIENT_ID, clientSecret: process.env.FACEBOOK_CLIENT_SECRET, }, discord: { enabled: Boolean(process.env.DISCORD_CLIENT_ID), clientId: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, }, }, googleAuth: { enabled: Boolean(process.env.GOOGLE_CLIENT_ID), clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }, }; async function ensureDir() { await fs.mkdir(DATA_DIR, { recursive: true }); } export async function loadSystemConfig(): Promise { await ensureDir(); try { const raw = await fs.readFile(FILE_PATH, "utf-8"); const fileCfg = JSON.parse(raw) as SystemConfigData; return deepMerge(defaultConfig, fileCfg); } catch { // create initial file config await fs.writeFile(FILE_PATH, JSON.stringify(defaultConfig, null, 2), "utf-8"); return defaultConfig; } } export async function saveSystemConfig(partial: SystemConfigData, prisma?: PrismaClient | null) { const current = await loadSystemConfig(); const merged = deepMerge(current, partial); await ensureDir(); await fs.writeFile(FILE_PATH, JSON.stringify(merged, null, 2), "utf-8"); // Also persist to DB if available (best-effort) if (prisma) { try { await prisma.systemConfig.upsert({ where: { id: 1 }, update: { data: merged as any }, create: { id: 1, data: merged as any }, }); } catch { // ignore } } return merged; } function isObject(v: any) { return v && typeof v === "object" && !Array.isArray(v); } function deepMerge(base: T, patch: any): T { if (!isObject(base) || !isObject(patch)) return (patch ?? base) as T; const out: any = { ...base }; for (const k of Object.keys(patch)) { const bv = (base as any)[k]; const pv = patch[k]; out[k] = isObject(bv) && isObject(pv) ? deepMerge(bv, pv) : pv; } return out; }