148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
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<SystemConfigData> {
|
|
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<T>(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;
|
|
}
|