refactor(api): extract settings response support
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildSettingsUpdatePayload,
|
||||||
|
buildSystemSettingsViewModel,
|
||||||
|
getDefaultScoreWeights,
|
||||||
|
sanitizeSettingsAuditSnapshot,
|
||||||
|
} from "../router/settings-support.js";
|
||||||
|
|
||||||
|
describe("settings support", () => {
|
||||||
|
it("builds the admin-facing system settings view model with defaults", () => {
|
||||||
|
const runtimeSecrets = {
|
||||||
|
azureOpenAiApiKey: {
|
||||||
|
configured: true,
|
||||||
|
activeSource: "environment" as const,
|
||||||
|
hasStoredValue: true,
|
||||||
|
envVarNames: ["OPENAI_API_KEY"],
|
||||||
|
},
|
||||||
|
azureDalleApiKey: {
|
||||||
|
configured: false,
|
||||||
|
activeSource: "none" as const,
|
||||||
|
hasStoredValue: false,
|
||||||
|
envVarNames: ["AZURE_DALLE_API_KEY"],
|
||||||
|
},
|
||||||
|
geminiApiKey: {
|
||||||
|
configured: true,
|
||||||
|
activeSource: "database" as const,
|
||||||
|
hasStoredValue: true,
|
||||||
|
envVarNames: ["GEMINI_API_KEY"],
|
||||||
|
},
|
||||||
|
smtpPassword: {
|
||||||
|
configured: false,
|
||||||
|
activeSource: "none" as const,
|
||||||
|
hasStoredValue: false,
|
||||||
|
envVarNames: ["SMTP_PASSWORD"],
|
||||||
|
},
|
||||||
|
anonymizationSeed: {
|
||||||
|
configured: false,
|
||||||
|
activeSource: "none" as const,
|
||||||
|
hasStoredValue: false,
|
||||||
|
envVarNames: ["ANONYMIZATION_SEED"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(buildSystemSettingsViewModel({
|
||||||
|
settings: {
|
||||||
|
aiProvider: "azure",
|
||||||
|
azureOpenAiEndpoint: "https://example.openai.azure.com",
|
||||||
|
scoreVisibleRoles: ["ADMIN"],
|
||||||
|
smtpTls: false,
|
||||||
|
vacationDefaultDays: 30,
|
||||||
|
},
|
||||||
|
runtimeSettings: {
|
||||||
|
azureOpenAiApiKey: "secret",
|
||||||
|
smtpPassword: null,
|
||||||
|
azureDalleApiKey: null,
|
||||||
|
geminiApiKey: "gemini-secret",
|
||||||
|
},
|
||||||
|
runtimeSecrets,
|
||||||
|
defaultSummaryPrompt: "Summarize this resource.",
|
||||||
|
})).toEqual({
|
||||||
|
aiProvider: "azure",
|
||||||
|
azureOpenAiEndpoint: "https://example.openai.azure.com",
|
||||||
|
azureOpenAiDeployment: null,
|
||||||
|
azureApiVersion: "2025-01-01-preview",
|
||||||
|
aiMaxCompletionTokens: 300,
|
||||||
|
aiTemperature: 1,
|
||||||
|
aiSummaryPrompt: null,
|
||||||
|
defaultSummaryPrompt: "Summarize this resource.",
|
||||||
|
hasApiKey: true,
|
||||||
|
runtimeSecrets,
|
||||||
|
legacyStoredSecretFields: ["azureOpenAiApiKey", "geminiApiKey"],
|
||||||
|
scoreWeights: getDefaultScoreWeights(),
|
||||||
|
scoreVisibleRoles: ["ADMIN"],
|
||||||
|
smtpHost: null,
|
||||||
|
smtpPort: 587,
|
||||||
|
smtpUser: null,
|
||||||
|
smtpFrom: null,
|
||||||
|
smtpTls: false,
|
||||||
|
hasSmtpPassword: false,
|
||||||
|
anonymizationEnabled: false,
|
||||||
|
anonymizationDomain: "superhartmut.de",
|
||||||
|
anonymizationMode: "global",
|
||||||
|
azureDalleDeployment: null,
|
||||||
|
azureDalleEndpoint: null,
|
||||||
|
hasDalleApiKey: false,
|
||||||
|
geminiModel: "gemini-2.5-flash-image",
|
||||||
|
hasGeminiApiKey: true,
|
||||||
|
imageProvider: "dalle",
|
||||||
|
vacationDefaultDays: 30,
|
||||||
|
timelineUndoMaxSteps: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds sparse updates and strips secret persistence", () => {
|
||||||
|
expect(buildSettingsUpdatePayload({
|
||||||
|
aiProvider: "openai",
|
||||||
|
azureOpenAiApiKey: "secret",
|
||||||
|
smtpPassword: "smtp-secret",
|
||||||
|
aiSummaryPrompt: "",
|
||||||
|
imageProvider: "gemini",
|
||||||
|
})).toEqual({
|
||||||
|
data: {
|
||||||
|
aiProvider: "openai",
|
||||||
|
aiSummaryPrompt: null,
|
||||||
|
imageProvider: "gemini",
|
||||||
|
},
|
||||||
|
ignoredSecretFields: ["azureOpenAiApiKey", "smtpPassword"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes sensitive fields in audit snapshots", () => {
|
||||||
|
expect(sanitizeSettingsAuditSnapshot({
|
||||||
|
azureOpenAiApiKey: "secret",
|
||||||
|
smtpPassword: null,
|
||||||
|
aiProvider: "azure",
|
||||||
|
})).toEqual({
|
||||||
|
azureOpenAiApiKey: "***",
|
||||||
|
smtpPassword: null,
|
||||||
|
aiProvider: "azure",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,10 @@ import {
|
|||||||
parseAiError,
|
parseAiError,
|
||||||
sanitizeDiagnosticError,
|
sanitizeDiagnosticError,
|
||||||
} from "../ai-client.js";
|
} from "../ai-client.js";
|
||||||
|
import {
|
||||||
|
RUNTIME_SECRET_FIELDS,
|
||||||
|
} from "../lib/system-settings-runtime.js";
|
||||||
|
import type { RuntimeSecretField, RuntimeSecretStatus } from "../lib/system-settings-runtime.js";
|
||||||
import { logger } from "../lib/logger.js";
|
import { logger } from "../lib/logger.js";
|
||||||
|
|
||||||
/** Fields that must never appear in audit log values */
|
/** Fields that must never appear in audit log values */
|
||||||
@@ -75,6 +79,80 @@ export function getDefaultScoreWeights() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildSystemSettingsViewModel(input: {
|
||||||
|
settings: {
|
||||||
|
aiProvider?: string | null;
|
||||||
|
azureOpenAiEndpoint?: string | null;
|
||||||
|
azureOpenAiDeployment?: string | null;
|
||||||
|
azureApiVersion?: string | null;
|
||||||
|
aiMaxCompletionTokens?: number | null;
|
||||||
|
aiTemperature?: number | null;
|
||||||
|
aiSummaryPrompt?: string | null;
|
||||||
|
scoreWeights?: unknown;
|
||||||
|
scoreVisibleRoles?: unknown;
|
||||||
|
smtpHost?: string | null;
|
||||||
|
smtpPort?: number | null;
|
||||||
|
smtpUser?: string | null;
|
||||||
|
smtpFrom?: string | null;
|
||||||
|
smtpTls?: boolean | null;
|
||||||
|
anonymizationEnabled?: boolean | null;
|
||||||
|
anonymizationDomain?: string | null;
|
||||||
|
anonymizationMode?: string | null;
|
||||||
|
azureDalleDeployment?: string | null;
|
||||||
|
azureDalleEndpoint?: string | null;
|
||||||
|
geminiModel?: string | null;
|
||||||
|
imageProvider?: string | null;
|
||||||
|
vacationDefaultDays?: number | null;
|
||||||
|
timelineUndoMaxSteps?: number | null;
|
||||||
|
} | null | undefined;
|
||||||
|
runtimeSettings: {
|
||||||
|
azureOpenAiApiKey?: string | null;
|
||||||
|
smtpPassword?: string | null;
|
||||||
|
azureDalleApiKey?: string | null;
|
||||||
|
geminiApiKey?: string | null;
|
||||||
|
} | null | undefined;
|
||||||
|
runtimeSecrets: Record<RuntimeSecretField, RuntimeSecretStatus>;
|
||||||
|
defaultSummaryPrompt: string;
|
||||||
|
}) {
|
||||||
|
const defaultWeights = getDefaultScoreWeights();
|
||||||
|
const settings = input.settings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
aiProvider: settings?.aiProvider ?? "openai",
|
||||||
|
azureOpenAiEndpoint: settings?.azureOpenAiEndpoint ?? null,
|
||||||
|
azureOpenAiDeployment: settings?.azureOpenAiDeployment ?? null,
|
||||||
|
azureApiVersion: settings?.azureApiVersion ?? "2025-01-01-preview",
|
||||||
|
aiMaxCompletionTokens: settings?.aiMaxCompletionTokens ?? 300,
|
||||||
|
aiTemperature: settings?.aiTemperature ?? 1,
|
||||||
|
aiSummaryPrompt: settings?.aiSummaryPrompt ?? null,
|
||||||
|
defaultSummaryPrompt: input.defaultSummaryPrompt,
|
||||||
|
hasApiKey: !!input.runtimeSettings?.azureOpenAiApiKey,
|
||||||
|
runtimeSecrets: input.runtimeSecrets,
|
||||||
|
legacyStoredSecretFields: RUNTIME_SECRET_FIELDS.filter(
|
||||||
|
(field) => input.runtimeSecrets[field].hasStoredValue,
|
||||||
|
),
|
||||||
|
scoreWeights: (settings?.scoreWeights as typeof defaultWeights | null | undefined) ?? defaultWeights,
|
||||||
|
scoreVisibleRoles: (settings?.scoreVisibleRoles as string[] | null | undefined) ?? ["ADMIN", "MANAGER"],
|
||||||
|
smtpHost: settings?.smtpHost ?? null,
|
||||||
|
smtpPort: settings?.smtpPort ?? 587,
|
||||||
|
smtpUser: settings?.smtpUser ?? null,
|
||||||
|
smtpFrom: settings?.smtpFrom ?? null,
|
||||||
|
smtpTls: settings?.smtpTls ?? true,
|
||||||
|
hasSmtpPassword: !!input.runtimeSettings?.smtpPassword,
|
||||||
|
anonymizationEnabled: settings?.anonymizationEnabled ?? false,
|
||||||
|
anonymizationDomain: settings?.anonymizationDomain ?? "superhartmut.de",
|
||||||
|
anonymizationMode: settings?.anonymizationMode ?? "global",
|
||||||
|
azureDalleDeployment: settings?.azureDalleDeployment ?? null,
|
||||||
|
azureDalleEndpoint: settings?.azureDalleEndpoint ?? null,
|
||||||
|
hasDalleApiKey: !!input.runtimeSettings?.azureDalleApiKey,
|
||||||
|
geminiModel: settings?.geminiModel ?? "gemini-2.5-flash-image",
|
||||||
|
hasGeminiApiKey: !!input.runtimeSettings?.geminiApiKey,
|
||||||
|
imageProvider: settings?.imageProvider ?? "dalle",
|
||||||
|
vacationDefaultDays: settings?.vacationDefaultDays ?? 28,
|
||||||
|
timelineUndoMaxSteps: settings?.timelineUndoMaxSteps ?? 50,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSettingsUpdatePayload(input: SettingsUpdateInput): {
|
export function buildSettingsUpdatePayload(input: SettingsUpdateInput): {
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
ignoredSecretFields: string[];
|
ignoredSecretFields: string[];
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { testSmtpConnection } from "../lib/email.js";
|
|||||||
import { createAuditEntry } from "../lib/audit.js";
|
import { createAuditEntry } from "../lib/audit.js";
|
||||||
import { getRuntimeSecretStatuses, RUNTIME_SECRET_FIELDS, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
import { getRuntimeSecretStatuses, RUNTIME_SECRET_FIELDS, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||||
import {
|
import {
|
||||||
|
buildSystemSettingsViewModel,
|
||||||
buildSettingsUpdatePayload,
|
buildSettingsUpdatePayload,
|
||||||
getDefaultScoreWeights,
|
|
||||||
sanitizeSettingsAuditSnapshot,
|
sanitizeSettingsAuditSnapshot,
|
||||||
settingsUpdateInputSchema,
|
settingsUpdateInputSchema,
|
||||||
testRuntimeAiConnection,
|
testRuntimeAiConnection,
|
||||||
@@ -19,51 +19,12 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
const runtimeSettings = resolveSystemSettingsRuntime(settings);
|
const runtimeSettings = resolveSystemSettingsRuntime(settings);
|
||||||
const runtimeSecrets = getRuntimeSecretStatuses(settings);
|
const runtimeSecrets = getRuntimeSecretStatuses(settings);
|
||||||
const legacyStoredSecretFields = RUNTIME_SECRET_FIELDS.filter(
|
return buildSystemSettingsViewModel({
|
||||||
(field) => runtimeSecrets[field].hasStoredValue,
|
settings,
|
||||||
);
|
runtimeSettings,
|
||||||
|
|
||||||
const defaultWeights = getDefaultScoreWeights();
|
|
||||||
|
|
||||||
return {
|
|
||||||
aiProvider: settings?.aiProvider ?? "openai",
|
|
||||||
azureOpenAiEndpoint: settings?.azureOpenAiEndpoint ?? null,
|
|
||||||
azureOpenAiDeployment: settings?.azureOpenAiDeployment ?? null,
|
|
||||||
azureApiVersion: settings?.azureApiVersion ?? "2025-01-01-preview",
|
|
||||||
aiMaxCompletionTokens: settings?.aiMaxCompletionTokens ?? 300,
|
|
||||||
aiTemperature: settings?.aiTemperature ?? 1,
|
|
||||||
aiSummaryPrompt: settings?.aiSummaryPrompt ?? null,
|
|
||||||
defaultSummaryPrompt: DEFAULT_SUMMARY_PROMPT,
|
|
||||||
hasApiKey: !!runtimeSettings.azureOpenAiApiKey,
|
|
||||||
runtimeSecrets,
|
runtimeSecrets,
|
||||||
legacyStoredSecretFields,
|
defaultSummaryPrompt: DEFAULT_SUMMARY_PROMPT,
|
||||||
scoreWeights: (settings?.scoreWeights as unknown as typeof defaultWeights) ?? defaultWeights,
|
});
|
||||||
scoreVisibleRoles: (settings?.scoreVisibleRoles as unknown as string[]) ?? ["ADMIN", "MANAGER"],
|
|
||||||
// SMTP
|
|
||||||
smtpHost: settings?.smtpHost ?? null,
|
|
||||||
smtpPort: settings?.smtpPort ?? 587,
|
|
||||||
smtpUser: settings?.smtpUser ?? null,
|
|
||||||
smtpFrom: settings?.smtpFrom ?? null,
|
|
||||||
smtpTls: settings?.smtpTls ?? true,
|
|
||||||
hasSmtpPassword: !!runtimeSettings.smtpPassword,
|
|
||||||
// Global anonymization
|
|
||||||
anonymizationEnabled: settings?.anonymizationEnabled ?? false,
|
|
||||||
anonymizationDomain: settings?.anonymizationDomain ?? "superhartmut.de",
|
|
||||||
anonymizationMode: settings?.anonymizationMode ?? "global",
|
|
||||||
// DALL-E
|
|
||||||
azureDalleDeployment: settings?.azureDalleDeployment ?? null,
|
|
||||||
azureDalleEndpoint: settings?.azureDalleEndpoint ?? null,
|
|
||||||
hasDalleApiKey: !!runtimeSettings.azureDalleApiKey,
|
|
||||||
// Gemini
|
|
||||||
geminiModel: settings?.geminiModel ?? "gemini-2.5-flash-image",
|
|
||||||
hasGeminiApiKey: !!runtimeSettings.geminiApiKey,
|
|
||||||
// Image provider
|
|
||||||
imageProvider: settings?.imageProvider ?? "dalle",
|
|
||||||
// Vacation defaults
|
|
||||||
vacationDefaultDays: settings?.vacationDefaultDays ?? 28,
|
|
||||||
// Timeline
|
|
||||||
timelineUndoMaxSteps: settings?.timelineUndoMaxSteps ?? 50,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateSystemSettings: adminProcedure
|
updateSystemSettings: adminProcedure
|
||||||
|
|||||||
Reference in New Issue
Block a user