import "server-only"; import { prisma } from "@/lib/db"; // Fixed id of the one-and-only AiSettings row (see prisma/schema.prisma). const SETTINGS_ID = "singleton"; export interface AiSettingsView { apiUrl: string | null; model: string | null; hasApiKey: boolean; // Last 4 characters only (e.g. "••••••••f00d") -- enough to confirm // *which* key is configured without ever sending the real one back to // the browser. Null when no key is set. apiKeyMasked: string | null; } function maskKey(key: string): string { return `••••••••${key.slice(-4)}`; } /** * Builds an OpenAI-compatible endpoint URL from an admin-entered base URL * (e.g. "https://host/api/v1") plus a path (e.g. "models", * "chat/completions"). Shared by every call site that talks to the * configured provider, so the trailing-slash handling only lives once -- * `new URL(path, base)` silently drops the base's last path segment * (turning ".../v1" into ".../models" instead of ".../v1/models") unless * the base ends in "/". */ export function joinApiUrl(apiUrl: string, path: string): URL { const withTrailingSlash = apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`; return new URL(path, withTrailingSlash); } /** The client-safe view of the AI settings -- never includes the real key. */ export async function getAiSettingsView(): Promise { const settings = await prisma.aiSettings.findUnique({ where: { id: SETTINGS_ID } }); return { apiUrl: settings?.apiUrl ?? null, model: settings?.model ?? null, hasApiKey: !!settings?.apiKey, apiKeyMasked: settings?.apiKey ? maskKey(settings.apiKey) : null, }; } /** * The real, unmasked key -- for server-side use only (calling the * provider). Never pass this to a client component or return it from a * Server Action. */ export async function getAiCredentials(): Promise<{ apiUrl: string | null; apiKey: string | null; }> { const settings = await prisma.aiSettings.findUnique({ where: { id: SETTINGS_ID } }); return { apiUrl: settings?.apiUrl ?? null, apiKey: settings?.apiKey ?? null }; } /** * `apiKey` omitted (or empty) means "leave the currently saved key * alone" -- the form field is never prefilled with the real key, so * there's nothing to resubmit when the admin isn't changing it. */ export async function saveAiSettings(data: { apiUrl: string; apiKey?: string; model: string; }): Promise { await prisma.aiSettings.upsert({ where: { id: SETTINGS_ID }, create: { id: SETTINGS_ID, apiUrl: data.apiUrl, apiKey: data.apiKey || null, model: data.model, }, update: { apiUrl: data.apiUrl, model: data.model, ...(data.apiKey ? { apiKey: data.apiKey } : {}), }, }); }