refactor(runtime): prefer env-backed secrets at runtime
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc.js";
|
||||
import { createAiClient, isAiConfigured, parseAiError } from "../ai-client.js";
|
||||
import { isAiConfigured, parseAiError, sanitizeDiagnosticError } from "../ai-client.js";
|
||||
import { DEFAULT_SUMMARY_PROMPT } from "./resource.js";
|
||||
import { VALUE_SCORE_WEIGHTS } from "@capakraken/shared";
|
||||
import { testSmtpConnection } from "../lib/email.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
|
||||
/** Fields that must never appear in audit log values */
|
||||
const SENSITIVE_FIELDS = new Set([
|
||||
@@ -20,6 +22,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
const settings = await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
});
|
||||
const runtimeSettings = resolveSystemSettingsRuntime(settings);
|
||||
|
||||
const defaultWeights = {
|
||||
skillDepth: VALUE_SCORE_WEIGHTS.SKILL_DEPTH,
|
||||
@@ -38,7 +41,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
aiTemperature: settings?.aiTemperature ?? 1,
|
||||
aiSummaryPrompt: settings?.aiSummaryPrompt ?? null,
|
||||
defaultSummaryPrompt: DEFAULT_SUMMARY_PROMPT,
|
||||
hasApiKey: !!settings?.azureOpenAiApiKey,
|
||||
hasApiKey: !!runtimeSettings.azureOpenAiApiKey,
|
||||
scoreWeights: (settings?.scoreWeights as unknown as typeof defaultWeights) ?? defaultWeights,
|
||||
scoreVisibleRoles: (settings?.scoreVisibleRoles as unknown as string[]) ?? ["ADMIN", "MANAGER"],
|
||||
// SMTP
|
||||
@@ -47,7 +50,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
smtpUser: settings?.smtpUser ?? null,
|
||||
smtpFrom: settings?.smtpFrom ?? null,
|
||||
smtpTls: settings?.smtpTls ?? true,
|
||||
hasSmtpPassword: !!settings?.smtpPassword,
|
||||
hasSmtpPassword: !!runtimeSettings.smtpPassword,
|
||||
// Global anonymization
|
||||
anonymizationEnabled: settings?.anonymizationEnabled ?? false,
|
||||
anonymizationDomain: settings?.anonymizationDomain ?? "superhartmut.de",
|
||||
@@ -55,10 +58,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
// DALL-E
|
||||
azureDalleDeployment: settings?.azureDalleDeployment ?? null,
|
||||
azureDalleEndpoint: settings?.azureDalleEndpoint ?? null,
|
||||
hasDalleApiKey: !!settings?.azureDalleApiKey,
|
||||
hasDalleApiKey: !!runtimeSettings.azureDalleApiKey,
|
||||
// Gemini
|
||||
geminiModel: settings?.geminiModel ?? "gemini-2.5-flash-image",
|
||||
hasGeminiApiKey: !!settings?.geminiApiKey,
|
||||
hasGeminiApiKey: !!runtimeSettings.geminiApiKey,
|
||||
// Image provider
|
||||
imageProvider: settings?.imageProvider ?? "dalle",
|
||||
// Vacation defaults
|
||||
@@ -216,9 +219,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testAiConnection: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const settings = await ctx.db.systemSettings.findUnique({
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
});
|
||||
}));
|
||||
|
||||
if (!isAiConfigured(settings)) {
|
||||
const provider = settings?.aiProvider ?? "openai";
|
||||
@@ -228,21 +231,21 @@ export const settingsRouter = createTRPCRouter({
|
||||
return { ok: false, error: "Missing required fields: model name and API key are required." };
|
||||
}
|
||||
|
||||
const provider = settings!.aiProvider ?? "openai";
|
||||
const apiKey = settings!.azureOpenAiApiKey!;
|
||||
const provider = settings.aiProvider ?? "openai";
|
||||
const apiKey = settings.azureOpenAiApiKey!;
|
||||
|
||||
let url: string;
|
||||
let headers: Record<string, string>;
|
||||
|
||||
if (provider === "azure") {
|
||||
const endpoint = settings!.azureOpenAiEndpoint!.replace(/\/$/, "");
|
||||
const deployment = settings!.azureOpenAiDeployment!;
|
||||
const apiVersion = settings!.azureApiVersion ?? "2025-01-01-preview";
|
||||
const endpoint = settings.azureOpenAiEndpoint!.replace(/\/$/, "");
|
||||
const deployment = settings.azureOpenAiDeployment!;
|
||||
const apiVersion = settings.azureApiVersion ?? "2025-01-01-preview";
|
||||
url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;
|
||||
headers = { "Content-Type": "application/json", "api-key": apiKey };
|
||||
} else {
|
||||
// Standard OpenAI API — deployment field holds the model name (e.g. "gpt-4o")
|
||||
const model = settings!.azureOpenAiDeployment ?? "gpt-4o-mini";
|
||||
const model = settings.azureOpenAiDeployment ?? "gpt-4o-mini";
|
||||
url = "https://api.openai.com/v1/chat/completions";
|
||||
headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` };
|
||||
// Override body to include model field for OpenAI
|
||||
@@ -257,17 +260,24 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
});
|
||||
const body = await resp.text();
|
||||
if (resp.ok) return { ok: true, raw: null };
|
||||
if (resp.ok) return { ok: true };
|
||||
let msg = body;
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { error?: { message?: string } };
|
||||
if (parsed.error?.message) msg = parsed.error.message;
|
||||
} catch { /* keep raw */ }
|
||||
const raw = `HTTP ${resp.status}: ${msg}`;
|
||||
return { ok: false, error: parseAiError(new Error(raw)), raw };
|
||||
logger.warn(
|
||||
{ provider, diagnostic: sanitizeDiagnosticError(raw) },
|
||||
"AI connection test failed",
|
||||
);
|
||||
return { ok: false, error: parseAiError(new Error(raw)) };
|
||||
} catch (err) {
|
||||
const raw = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, error: parseAiError(err), raw };
|
||||
logger.warn(
|
||||
{ provider, diagnostic: sanitizeDiagnosticError(err) },
|
||||
"AI connection test failed",
|
||||
);
|
||||
return { ok: false, error: parseAiError(err) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,9 +293,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
const body = await resp.text();
|
||||
|
||||
if (resp.ok) {
|
||||
return { ok: true, raw: null };
|
||||
}
|
||||
if (resp.ok) return { ok: true };
|
||||
|
||||
let azureMessage = body;
|
||||
try {
|
||||
@@ -294,10 +302,17 @@ export const settingsRouter = createTRPCRouter({
|
||||
} catch { /* leave as raw text */ }
|
||||
|
||||
const raw = `HTTP ${resp.status}: ${azureMessage}`;
|
||||
return { ok: false, error: parseAiError(new Error(raw)), raw };
|
||||
logger.warn(
|
||||
{ provider, diagnostic: sanitizeDiagnosticError(raw) },
|
||||
"AI connection test failed",
|
||||
);
|
||||
return { ok: false, error: parseAiError(new Error(raw)) };
|
||||
} catch (err) {
|
||||
const raw = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, error: parseAiError(err), raw };
|
||||
logger.warn(
|
||||
{ provider, diagnostic: sanitizeDiagnosticError(err) },
|
||||
"AI connection test failed",
|
||||
);
|
||||
return { ok: false, error: parseAiError(err) };
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -320,10 +335,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
testGeminiConnection: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const settings = await ctx.db.systemSettings.findUnique({
|
||||
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." };
|
||||
@@ -362,7 +377,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getAiConfigured: adminProcedure.query(async ({ ctx }) => {
|
||||
const settings = await ctx.db.systemSettings.findUnique({
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
aiProvider: true,
|
||||
@@ -370,7 +385,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
azureOpenAiDeployment: true,
|
||||
azureOpenAiApiKey: true,
|
||||
},
|
||||
});
|
||||
}));
|
||||
return { configured: isAiConfigured(settings) };
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user