125 lines
4.7 KiB
TypeScript
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;
|
|
}
|