import net from "node:net"; import tls from "node:tls"; import { db, setSetting } from "./db.mjs"; import { requirePermission } from "./auth.mjs"; import { json, readBody } from "./http.mjs"; import { permissions } from "./permissions.mjs"; function getSettingValue(key, fallback = "") { return db.prepare("SELECT value FROM system_settings WHERE key = ?").get(key)?.value ?? fallback; } function testSmtpConnection({ host, port }) { return new Promise((resolve, reject) => { const socketFactory = Number(port) === 465 ? tls.connect : net.connect; const socket = socketFactory({ host, port: Number(port), servername: host, timeout: 10000, }); let buffer = ""; const finish = (result) => { socket.removeAllListeners(); socket.end(); resolve(result); }; socket.on("connect", () => { if (Number(port) === 465) { socket.write("EHLO vitepress-cms.local\r\n"); } }); socket.on("secureConnect", () => { socket.write("EHLO vitepress-cms.local\r\n"); }); socket.on("data", (chunk) => { buffer += chunk.toString("utf-8"); if (buffer.startsWith("220")) { socket.write("EHLO vitepress-cms.local\r\n"); } if (buffer.includes("\n250 ") || buffer.includes("\n250-") || buffer.startsWith("250")) { socket.write("QUIT\r\n"); finish({ ok: true, greeting: buffer.split(/\r?\n/).filter(Boolean).slice(0, 3).join(" / "), }); } }); socket.on("timeout", () => { socket.destroy(); reject(new Error("SMTP connection timed out")); }); socket.on("error", reject); }); } function publicSetting(row) { return { key: row.key, value: row.encrypted ? "" : row.value, encrypted: Boolean(row.encrypted), updatedAt: row.updated_at, }; } async function handleGetSettings(request, response) { if (!requirePermission(request, response, permissions.settingsManage)) return; const rows = db .prepare("SELECT key, value, encrypted, updated_at FROM system_settings ORDER BY key") .all(); json(response, 200, { settings: rows.map(publicSetting) }); } async function handleSaveSettings(request, response) { if (!requirePermission(request, response, permissions.settingsManage)) return; const body = JSON.parse(await readBody(request) || "{}"); const settings = Array.isArray(body.settings) ? body.settings : []; settings.forEach((setting) => { if (!setting.key) return; const existing = db.prepare("SELECT encrypted FROM system_settings WHERE key = ?").get(String(setting.key)); const shouldKeepExistingSecret = Boolean(existing?.encrypted) && Boolean(setting.encrypted) && !String(setting.value ?? ""); if (shouldKeepExistingSecret) return; setSetting(String(setting.key), String(setting.value ?? ""), Boolean(setting.encrypted)); }); json(response, 200, { ok: true }); } async function handleTestSmtp(request, response) { if (!requirePermission(request, response, permissions.settingsManage)) return; const host = getSettingValue("smtp.host"); const port = getSettingValue("smtp.port", "587"); if (!host) { json(response, 400, { error: "SMTP host is required" }); return; } const result = await testSmtpConnection({ host, port }); json(response, 200, result); } export async function handleSettingsApi(request, response, url) { try { if (url.pathname === "/api/admin/settings" && request.method === "GET") { await handleGetSettings(request, response); return true; } if (url.pathname === "/api/admin/settings" && request.method === "PUT") { await handleSaveSettings(request, response); return true; } if (url.pathname === "/api/admin/settings/test-smtp" && request.method === "POST") { await handleTestSmtp(request, response); return true; } return false; } catch (error) { json(response, 500, { error: error instanceof Error ? error.message : "Unknown settings API error", }); return true; } }