Files
gutscheintracker/lib/integration-settings.ts
T

125 lines
4.7 KiB
TypeScript

import { getDb } from "./db";
import { canEncrypt, decryptSecret, encryptSecret } from "./settings-crypto";
export const INTEGRATION_SECTIONS = ["smtp", "telegram", "logo"] as const;
export type IntegrationSection = (typeof INTEGRATION_SECTIONS)[number];
const SECRET_KEYS = new Set(["smtp_pass", "telegram_bot_token", "logo_dev_secret_key"]);
const ENV_FALLBACK: Record<string, string> = {
smtp_host: "SMTP_HOST",
smtp_port: "SMTP_PORT",
smtp_user: "SMTP_USER",
smtp_pass: "SMTP_PASS",
mail_from: "MAIL_FROM",
telegram_bot_token: "TELEGRAM_BOT_TOKEN",
logo_dev_secret_key: "LOGO_DEV_SECRET_KEY",
logo_dev_publishable_key: "LOGO_DEV_PUBLISHABLE_KEY",
};
export type SmtpSettings = { host: string; port: number; user: string; pass: string; from: string };
export type TelegramSettings = { botToken: string };
export type LogoSettings = { secretKey: string; publishableKey: string };
async function readStored(key: string): Promise<string | null> {
const db = getDb();
const row = await db.appSetting.findUnique({ where: { key } });
if (!row) return null;
if (SECRET_KEYS.has(key)) {
try {
return decryptSecret(row.value);
} catch {
return null;
}
}
return row.value;
}
async function resolveValue(key: string): Promise<string> {
const stored = await readStored(key);
if (stored !== null && stored !== "") return stored;
return process.env[ENV_FALLBACK[key] ?? ""] ?? "";
}
/** Volle SMTP-Konfiguration: UI-Werte zuerst, sonst .env-Rückfall. */
export async function getSmtpSettings(): Promise<SmtpSettings> {
const [host, port, user, pass, from] = await Promise.all([
resolveValue("smtp_host"),
resolveValue("smtp_port"),
resolveValue("smtp_user"),
resolveValue("smtp_pass"),
resolveValue("mail_from"),
]);
return { host, port: Number(port) || 587, user, pass, from };
}
export async function getTelegramSettings(): Promise<TelegramSettings> {
return { botToken: await resolveValue("telegram_bot_token") };
}
export async function getLogoSettings(): Promise<LogoSettings> {
const [secretKey, publishableKey] = await Promise.all([
resolveValue("logo_dev_secret_key"),
resolveValue("logo_dev_publishable_key"),
]);
return { secretKey, publishableKey };
}
/** Status für die UI: Klartextwerte plus je Geheimnis nur ob gesetzt, nie der Wert. */
export async function getIntegrationStatus() {
const [smtp, telegram, logo] = await Promise.all([getSmtpSettings(), getTelegramSettings(), getLogoSettings()]);
return {
smtp: { host: smtp.host, port: smtp.port, user: smtp.user, from: smtp.from, passGesetzt: smtp.pass !== "" },
telegram: { botTokenGesetzt: telegram.botToken !== "" },
logo: { secretGesetzt: logo.secretKey !== "", publishableKey: logo.publishableKey },
verschluesselungBereit: canEncrypt(),
};
}
const SECTION_FIELDS: Record<IntegrationSection, string[]> = {
smtp: ["smtp_host", "smtp_port", "smtp_user", "smtp_pass", "mail_from"],
telegram: ["telegram_bot_token"],
logo: ["logo_dev_secret_key", "logo_dev_publishable_key"],
};
export function validateIntegrationSection(section: string, values: Record<string, unknown>): string[] {
if (!(INTEGRATION_SECTIONS as readonly string[]).includes(section)) return ["section"];
const errs: string[] = [];
if (section === "smtp") {
if (typeof values.smtp_port !== "undefined" && values.smtp_port !== "" && !Number.isInteger(Number(values.smtp_port))) {
errs.push("smtp_port");
}
if (typeof values.smtp_host === "string" && values.smtp_host.trim() !== "" && values.smtp_host.trim().length > 255) {
errs.push("smtp_host");
}
}
for (const key of SECTION_FIELDS[section as IntegrationSection]) {
const v = values[key];
if (typeof v !== "undefined" && typeof v !== "string") errs.push(key);
if (typeof v === "string" && v.length > 2000) errs.push(key);
}
return errs;
}
/**
* Speichert einen Bereich. Leere Geheimnis-Felder lassen den bestehenden Wert
* unverändert (UI sendet dann ""), alle anderen Felder werden übernommen
* ("" löscht einen Klartextwert). Gibt die gespeicherten Schlüssel zurück.
*/
export async function saveIntegrationSection(section: IntegrationSection, values: Record<string, string>): Promise<string[]> {
const db = getDb();
const saved: string[] = [];
for (const key of SECTION_FIELDS[section]) {
if (!(key in values)) continue;
const v = values[key];
if (SECRET_KEYS.has(key)) {
if (v === "") continue;
await db.appSetting.upsert({ where: { key }, create: { key, value: encryptSecret(v) }, update: { value: encryptSecret(v) } });
} else {
await db.appSetting.upsert({ where: { key }, create: { key, value: v }, update: { value: v } });
}
saved.push(key);
}
return saved;
}