import { adminProcedure, createTRPCRouter } from "../trpc.js"; import { isAiConfigured } from "../ai-client.js"; import { DEFAULT_SUMMARY_PROMPT } from "./resource.js"; import { testSmtpConnection } from "../lib/email.js"; import { createAuditEntry } from "../lib/audit.js"; import { getRuntimeSecretStatuses, RUNTIME_SECRET_FIELDS, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js"; import { buildSystemSettingsViewModel, buildSettingsUpdatePayload, sanitizeSettingsAuditSnapshot, settingsUpdateInputSchema, testRuntimeAiConnection, } from "./settings-support.js"; export const settingsRouter = createTRPCRouter({ getSystemSettings: adminProcedure.query(async ({ ctx }) => { const settings = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" }, }); const runtimeSettings = resolveSystemSettingsRuntime(settings); const runtimeSecrets = getRuntimeSecretStatuses(settings); return buildSystemSettingsViewModel({ settings, runtimeSettings, runtimeSecrets, defaultSummaryPrompt: DEFAULT_SUMMARY_PROMPT, }); }), updateSystemSettings: adminProcedure .input(settingsUpdateInputSchema) .mutation(async ({ ctx, input }) => { const { data, ignoredSecretFields } = buildSettingsUpdatePayload(input); if (Object.keys(data).length === 0) { return { ok: true, ignoredSecretFields, secretStorageMode: "environment-only" as const, }; } // Fetch current settings for before-snapshot const before = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } }); await ctx.db.systemSettings.upsert({ where: { id: "singleton" }, create: { id: "singleton", ...data }, update: data, }); const sanitizedBefore = before ? sanitizeSettingsAuditSnapshot(before as unknown as Record) : undefined; const sanitizedAfter = sanitizeSettingsAuditSnapshot(data); void createAuditEntry({ db: ctx.db, entityType: "SystemSettings", entityId: "singleton", entityName: "System Settings", action: before ? "UPDATE" : "CREATE", userId: ctx.dbUser?.id, ...(sanitizedBefore !== undefined ? { before: sanitizedBefore } : {}), after: sanitizedAfter, source: "ui", }); return { ok: true, ignoredSecretFields, secretStorageMode: "environment-only" as const, }; }), clearStoredRuntimeSecrets: adminProcedure.mutation(async ({ ctx }) => { const existing = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" }, select: Object.fromEntries( RUNTIME_SECRET_FIELDS.map((field) => [field, true]), ) as Record<(typeof RUNTIME_SECRET_FIELDS)[number], true>, }); const clearedFields = RUNTIME_SECRET_FIELDS.filter((field) => !!existing?.[field]); if (clearedFields.length === 0) { return { ok: true, clearedFields: [] as string[] }; } await ctx.db.systemSettings.update({ where: { id: "singleton" }, data: Object.fromEntries(clearedFields.map((field) => [field, null])), }); void createAuditEntry({ db: ctx.db, entityType: "SystemSettings", entityId: "singleton", entityName: "Runtime Secrets", action: "UPDATE", userId: ctx.dbUser?.id, after: { clearedFields }, source: "ui", summary: `Cleared ${clearedFields.length} legacy runtime secret field(s) from database storage`, }); return { ok: true, clearedFields }; }), testAiConnection: adminProcedure.mutation(async ({ ctx }) => { const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({ where: { id: "singleton" }, })); return testRuntimeAiConnection(settings); }), testSmtpConnection: adminProcedure.mutation(async ({ ctx }) => { const result = await testSmtpConnection(); void createAuditEntry({ db: ctx.db, entityType: "SystemSettings", entityId: "singleton", entityName: "SMTP Connection Test", action: "UPDATE", userId: ctx.dbUser?.id, after: { testResult: result.ok ? "success" : "failed" }, source: "ui", summary: result.ok ? "SMTP connection test succeeded" : "SMTP connection test failed", }); return result; }), testGeminiConnection: adminProcedure.mutation(async ({ ctx }) => { const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({ where: { id: "singleton" }, select: { geminiApiKey: true, geminiModel: true }, })); if (!settings?.geminiApiKey) { return { ok: false, error: "Gemini API key is not configured." }; } try { const { generateGeminiImage } = await import("../gemini-client.js"); const model = settings.geminiModel ?? "gemini-2.5-flash-image"; // Generate a tiny test image with a simple prompt const dataUrl = await generateGeminiImage( settings.geminiApiKey, "A simple blue circle on white background, minimal, 256x256", model, ); const hasImage = dataUrl.startsWith("data:image/"); void createAuditEntry({ db: ctx.db, entityType: "SystemSettings", entityId: "singleton", entityName: "Gemini Connection Test", action: "UPDATE", userId: ctx.dbUser?.id, after: { testResult: hasImage ? "success" : "failed" }, source: "ui", summary: hasImage ? "Gemini image generation test succeeded" : "Gemini test returned no image", }); return { ok: hasImage, model, preview: hasImage ? dataUrl.slice(0, 100) + "..." : undefined }; } catch (err) { const { parseGeminiError } = await import("../gemini-client.js"); return { ok: false, error: parseGeminiError(err) }; } }), getAiConfigured: adminProcedure.query(async ({ ctx }) => { const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({ where: { id: "singleton" }, select: { aiProvider: true, azureOpenAiEndpoint: true, azureOpenAiDeployment: true, azureOpenAiApiKey: true, }, })); return { configured: isAiConfigured(settings) }; }), });