refactor(api): extract settings procedures
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
createAuditEntry,
|
||||
testSmtpConnection,
|
||||
generateGeminiImage,
|
||||
parseGeminiError,
|
||||
} = vi.hoisted(() => ({
|
||||
createAuditEntry: vi.fn(),
|
||||
testSmtpConnection: vi.fn(),
|
||||
generateGeminiImage: vi.fn(),
|
||||
parseGeminiError: vi.fn((error: unknown) => `parsed:${String(error)}`),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/audit.js", () => ({
|
||||
createAuditEntry,
|
||||
}));
|
||||
|
||||
vi.mock("../lib/email.js", () => ({
|
||||
testSmtpConnection,
|
||||
}));
|
||||
|
||||
vi.mock("../gemini-client.js", () => ({
|
||||
generateGeminiImage,
|
||||
parseGeminiError,
|
||||
}));
|
||||
|
||||
import {
|
||||
clearStoredRuntimeSecrets,
|
||||
getAiConfiguredStatus,
|
||||
getSystemSettingsView,
|
||||
testSettingsGeminiConnection,
|
||||
testSettingsSmtpConnection,
|
||||
updateSystemSettings,
|
||||
} from "../router/settings-procedure-support.js";
|
||||
|
||||
function createAdminContext(db: Record<string, unknown>) {
|
||||
return {
|
||||
db: db as never,
|
||||
dbUser: {
|
||||
id: "admin_1",
|
||||
systemRole: SystemRole.ADMIN,
|
||||
permissionOverrides: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("settings procedure support", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("builds the system settings view model from db and runtime state", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
vi.stubEnv("SMTP_PASSWORD", "env-smtp-password");
|
||||
|
||||
const result = await getSystemSettingsView(createAdminContext({
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiApiKey: null,
|
||||
smtpPassword: null,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
expect(result.hasApiKey).toBe(true);
|
||||
expect(result.hasSmtpPassword).toBe(true);
|
||||
expect(result.aiProvider).toBe("openai");
|
||||
expect(result.defaultSummaryPrompt).toBeTypeOf("string");
|
||||
});
|
||||
|
||||
it("updates settings without persisting incoming secret fields and writes an audit entry", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "singleton",
|
||||
aiProvider: "azure",
|
||||
azureOpenAiApiKey: "secret",
|
||||
});
|
||||
const upsert = vi.fn().mockResolvedValue({});
|
||||
|
||||
const result = await updateSystemSettings(createAdminContext({
|
||||
systemSettings: {
|
||||
findUnique,
|
||||
upsert,
|
||||
},
|
||||
}), {
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
azureOpenAiApiKey: "should-be-ignored",
|
||||
smtpPassword: "also-ignored",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
ignoredSecretFields: ["azureOpenAiApiKey", "smtpPassword"],
|
||||
secretStorageMode: "environment-only",
|
||||
});
|
||||
expect(upsert).toHaveBeenCalledWith({
|
||||
where: { id: "singleton" },
|
||||
create: {
|
||||
id: "singleton",
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
},
|
||||
update: {
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
},
|
||||
});
|
||||
expect(createAuditEntry).toHaveBeenCalledWith(expect.objectContaining({
|
||||
entityType: "SystemSettings",
|
||||
action: "UPDATE",
|
||||
after: expect.objectContaining({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it("clears only legacy runtime secrets that are still stored", async () => {
|
||||
const update = vi.fn().mockResolvedValue({});
|
||||
|
||||
const result = await clearStoredRuntimeSecrets(createAdminContext({
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
azureOpenAiApiKey: "db-key",
|
||||
azureDalleApiKey: null,
|
||||
geminiApiKey: "db-gemini",
|
||||
smtpPassword: null,
|
||||
anonymizationSeed: "seed",
|
||||
}),
|
||||
update,
|
||||
},
|
||||
}));
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "anonymizationSeed"],
|
||||
});
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "singleton" },
|
||||
data: {
|
||||
azureOpenAiApiKey: null,
|
||||
geminiApiKey: null,
|
||||
anonymizationSeed: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("tests smtp and gemini connections with audited outcomes", async () => {
|
||||
testSmtpConnection.mockResolvedValue({ ok: true });
|
||||
generateGeminiImage.mockResolvedValue("data:image/png;base64,abc123");
|
||||
|
||||
const db = {
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
geminiApiKey: "gem-key",
|
||||
geminiModel: "gem-model",
|
||||
}),
|
||||
},
|
||||
};
|
||||
const ctx = createAdminContext(db);
|
||||
|
||||
expect(await testSettingsSmtpConnection(ctx)).toEqual({ ok: true });
|
||||
expect(await testSettingsGeminiConnection(ctx)).toEqual({
|
||||
ok: true,
|
||||
model: "gem-model",
|
||||
preview: "data:image/png;base64,abc123...",
|
||||
});
|
||||
expect(createAuditEntry).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reads the ai configured status from runtime-resolved settings", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
|
||||
const result = await getAiConfiguredStatus(createAdminContext({
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
azureOpenAiApiKey: null,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
expect(result).toEqual({ configured: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { isAiConfigured } from "../ai-client.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
import { testSmtpConnection } from "../lib/email.js";
|
||||
import {
|
||||
getRuntimeSecretStatuses,
|
||||
RUNTIME_SECRET_FIELDS,
|
||||
resolveSystemSettingsRuntime,
|
||||
} from "../lib/system-settings-runtime.js";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
import { DEFAULT_SUMMARY_PROMPT } from "./resource.js";
|
||||
import {
|
||||
buildSettingsUpdatePayload,
|
||||
buildSystemSettingsViewModel,
|
||||
sanitizeSettingsAuditSnapshot,
|
||||
type SettingsUpdateInput,
|
||||
testRuntimeAiConnection,
|
||||
} from "./settings-support.js";
|
||||
|
||||
type SettingsProcedureContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
|
||||
function withAuditUser(userId: string | undefined) {
|
||||
return userId ? { userId } : {};
|
||||
}
|
||||
|
||||
export async function getSystemSettingsView(
|
||||
ctx: SettingsProcedureContext,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSystemSettings(
|
||||
ctx: SettingsProcedureContext,
|
||||
input: SettingsUpdateInput,
|
||||
) {
|
||||
const { data, ignoredSecretFields } = buildSettingsUpdatePayload(input);
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
ignoredSecretFields,
|
||||
secretStorageMode: "environment-only" as const,
|
||||
};
|
||||
}
|
||||
|
||||
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<string, unknown>)
|
||||
: undefined;
|
||||
const sanitizedAfter = sanitizeSettingsAuditSnapshot(data);
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "SystemSettings",
|
||||
entityId: "singleton",
|
||||
entityName: "System Settings",
|
||||
action: before ? "UPDATE" : "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
...(sanitizedBefore !== undefined ? { before: sanitizedBefore } : {}),
|
||||
after: sanitizedAfter,
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
ignoredSecretFields,
|
||||
secretStorageMode: "environment-only" as const,
|
||||
};
|
||||
}
|
||||
|
||||
export async function clearStoredRuntimeSecrets(
|
||||
ctx: SettingsProcedureContext,
|
||||
) {
|
||||
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",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
after: { clearedFields },
|
||||
source: "ui",
|
||||
summary: `Cleared ${clearedFields.length} legacy runtime secret field(s) from database storage`,
|
||||
});
|
||||
|
||||
return { ok: true, clearedFields };
|
||||
}
|
||||
|
||||
export async function testSettingsAiConnection(
|
||||
ctx: SettingsProcedureContext,
|
||||
) {
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
}));
|
||||
return testRuntimeAiConnection(settings);
|
||||
}
|
||||
|
||||
export async function testSettingsSmtpConnection(
|
||||
ctx: SettingsProcedureContext,
|
||||
) {
|
||||
const result = await testSmtpConnection();
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "SystemSettings",
|
||||
entityId: "singleton",
|
||||
entityName: "SMTP Connection Test",
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
after: { testResult: result.ok ? "success" : "failed" },
|
||||
source: "ui",
|
||||
summary: result.ok ? "SMTP connection test succeeded" : "SMTP connection test failed",
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function testSettingsGeminiConnection(
|
||||
ctx: SettingsProcedureContext,
|
||||
) {
|
||||
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";
|
||||
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",
|
||||
...withAuditUser(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) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAiConfiguredStatus(
|
||||
ctx: SettingsProcedureContext,
|
||||
) {
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
aiProvider: true,
|
||||
azureOpenAiEndpoint: true,
|
||||
azureOpenAiDeployment: true,
|
||||
azureOpenAiApiKey: true,
|
||||
},
|
||||
}));
|
||||
return { configured: isAiConfigured(settings) };
|
||||
}
|
||||
@@ -1,187 +1,29 @@
|
||||
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";
|
||||
clearStoredRuntimeSecrets,
|
||||
getAiConfiguredStatus,
|
||||
getSystemSettingsView,
|
||||
testSettingsAiConnection,
|
||||
testSettingsGeminiConnection,
|
||||
testSettingsSmtpConnection,
|
||||
updateSystemSettings,
|
||||
} from "./settings-procedure-support.js";
|
||||
import { settingsUpdateInputSchema } 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,
|
||||
});
|
||||
}),
|
||||
getSystemSettings: adminProcedure.query(({ ctx }) => getSystemSettingsView(ctx)),
|
||||
|
||||
updateSystemSettings: adminProcedure
|
||||
.input(settingsUpdateInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { data, ignoredSecretFields } = buildSettingsUpdatePayload(input);
|
||||
.mutation(({ ctx, input }) => updateSystemSettings(ctx, input)),
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
ignoredSecretFields,
|
||||
secretStorageMode: "environment-only" as const,
|
||||
};
|
||||
}
|
||||
clearStoredRuntimeSecrets: adminProcedure.mutation(({ ctx }) => clearStoredRuntimeSecrets(ctx)),
|
||||
|
||||
// Fetch current settings for before-snapshot
|
||||
const before = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
|
||||
testAiConnection: adminProcedure.mutation(({ ctx }) => testSettingsAiConnection(ctx)),
|
||||
|
||||
await ctx.db.systemSettings.upsert({
|
||||
where: { id: "singleton" },
|
||||
create: { id: "singleton", ...data },
|
||||
update: data,
|
||||
});
|
||||
testSmtpConnection: adminProcedure.mutation(({ ctx }) => testSettingsSmtpConnection(ctx)),
|
||||
|
||||
const sanitizedBefore = before ? sanitizeSettingsAuditSnapshot(before as unknown as Record<string, unknown>) : undefined;
|
||||
const sanitizedAfter = sanitizeSettingsAuditSnapshot(data);
|
||||
testGeminiConnection: adminProcedure.mutation(({ ctx }) => testSettingsGeminiConnection(ctx)),
|
||||
|
||||
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) };
|
||||
}),
|
||||
getAiConfigured: adminProcedure.query(({ ctx }) => getAiConfiguredStatus(ctx)),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user