From a8c295285c520525ef28961142900d6ee35df2c4 Mon Sep 17 00:00:00 2001 From: floory <67979730+flooryyyy@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:58:55 +0100 Subject: [PATCH] feat(management): add AI provider admin CRUD --- .../(main)/management/AiProvidersSection.tsx | 288 ++++++++++++++++++ .../(main)/management/ManagementClient.tsx | 20 +- .../ai-providers/[id]/default/route.ts | 21 ++ .../api/management/ai-providers/[id]/route.ts | 44 +++ .../ai-providers/[id]/test/route.ts | 63 ++++ src/app/api/management/ai-providers/_lib.ts | 78 +++++ src/app/api/management/ai-providers/route.ts | 36 +++ src/app/components/Sidebar.tsx | 16 + src/app/db/migrations/34AddAiProvidersJson.ts | 14 + src/app/i18n/en.json | 7 +- src/app/i18n/fr.json | 7 +- src/app/i18n/it.json | 7 +- src/app/lib/types/db.ts | 1 + 13 files changed, 598 insertions(+), 4 deletions(-) create mode 100644 src/app/(main)/management/AiProvidersSection.tsx create mode 100644 src/app/api/management/ai-providers/[id]/default/route.ts create mode 100644 src/app/api/management/ai-providers/[id]/route.ts create mode 100644 src/app/api/management/ai-providers/[id]/test/route.ts create mode 100644 src/app/api/management/ai-providers/_lib.ts create mode 100644 src/app/api/management/ai-providers/route.ts create mode 100644 src/app/db/migrations/34AddAiProvidersJson.ts diff --git a/src/app/(main)/management/AiProvidersSection.tsx b/src/app/(main)/management/AiProvidersSection.tsx new file mode 100644 index 0000000..6362e2c --- /dev/null +++ b/src/app/(main)/management/AiProvidersSection.tsx @@ -0,0 +1,288 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { Bot, ChevronRight, Plus, TestTube2, Trash2 } from "lucide-react"; +import { Button } from "@components/ui/Button"; +import { Modal } from "@components/ui/Modal"; +import { toast } from "sonner"; +import type { Dict } from "@providers/I18nProvider"; + +export type AIProviderType = "openai" | "anthropic" | "google" | "custom"; + +export interface AIProvider { + id: string; + name: string; + type: AIProviderType; + apiKey: string; + baseUrl?: string; + model?: string; + enabled: boolean; + isDefault: boolean; +} + +const typeLabels: Record = { + openai: "OpenAI", + anthropic: "Anthropic", + google: "Google Gemini", + custom: "Custom", +}; + +export function AiProvidersSection({ dict }: { dict: Dict }) { + const [providers, setProviders] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [editing, setEditing] = useState(null); + const [testingId, setTestingId] = useState(null); + + const load = async () => { + setLoading(true); + try { + const res = await fetch("/api/management/ai-providers"); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to load providers"); + setProviders(Array.isArray(data.providers) ? data.providers : []); + } catch (error) { + console.error(error); + toast.error(dict.common.error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, []); + + const createProvider = async (provider: Omit) => { + setSaving(true); + try { + const res = await fetch("/api/management/ai-providers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(provider), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to create provider"); + await load(); + setEditing(null); + toast.success(dict.common.success); + } catch (error) { + console.error(error); + toast.error(dict.common.error); + } finally { + setSaving(false); + } + }; + + const updateProvider = async (provider: AIProvider) => { + setSaving(true); + try { + const res = await fetch(`/api/management/ai-providers/${provider.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(provider), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to update provider"); + await load(); + setEditing(null); + toast.success(dict.common.success); + } catch (error) { + console.error(error); + toast.error(dict.common.error); + } finally { + setSaving(false); + } + }; + + const deleteProvider = async (id: string) => { + setSaving(true); + try { + const res = await fetch(`/api/management/ai-providers/${id}`, { + method: "DELETE", + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to delete provider"); + await load(); + toast.success(dict.common.success); + } catch (error) { + console.error(error); + toast.error(dict.common.error); + } finally { + setSaving(false); + } + }; + + const setDefaultProvider = async (id: string) => { + setSaving(true); + try { + const res = await fetch(`/api/management/ai-providers/${id}/default`, { + method: "POST", + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Failed to set default provider"); + await load(); + toast.success(dict.common.success); + } catch (error) { + console.error(error); + toast.error(dict.common.error); + } finally { + setSaving(false); + } + }; + + const toggleEnabled = async (provider: AIProvider) => { + await updateProvider({ ...provider, enabled: !provider.enabled }); + }; + + const testConnection = async (providerId: string) => { + setTestingId(providerId); + try { + const res = await fetch(`/api/management/ai-providers/${providerId}/test`, { + method: "POST", + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Connection failed"); + toast.success(data.message || "Connection successful"); + } catch (error) { + console.error(error); + toast.error(error instanceof Error ? error.message : dict.common.error); + } finally { + setTestingId(null); + } + }; + + if (loading) { + return

{dict.common.loading}

; + } + + return ( +
+
+
+
+

{dict.management.aiProviders || "AI Providers"}

+

+ {dict.management.aiProvidersDesc || "Configure model providers for workspace AI features."} +

+
+ +
+ +
+ {providers.map((p) => ( +
+
+
+ +
+ +
+

{p.name}

+
+ + {p.enabled ? dict.common.enabled : dict.common.disabled} · {typeLabels[p.type]} +
+ {p.isDefault &&

{dict.management.defaultProvider || "Default provider"}

} +
+ +
+ + + +
+
+
+ ))} +
+
+ + {editing && ( + setEditing(null)} + onSave={(next) => (editing.id ? updateProvider(next as AIProvider) : createProvider(next as Omit))} + dict={dict} + /> + )} +
+ ); +} + +function AiProviderModal({ + provider, + saving, + onClose, + onSave, + dict, +}: { + provider: AIProvider; + saving: boolean; + onClose: () => void; + onSave: (provider: Partial) => void; + dict: Dict; +}) { + const [formData, setFormData] = useState(provider); + return ( + +
{ + e.preventDefault(); + onSave(formData); + }} + > +
+ + setFormData({ ...formData, name: e.target.value })} required /> +
+
+ + +
+
+ + setFormData({ ...formData, apiKey: e.target.value })} required type="password" /> +
+
+ + setFormData({ ...formData, baseUrl: e.target.value })} placeholder="Optional for standard endpoints" /> +
+
+ + setFormData({ ...formData, model: e.target.value })} /> +
+
+ +
+
+
+ ); +} diff --git a/src/app/(main)/management/ManagementClient.tsx b/src/app/(main)/management/ManagementClient.tsx index ac00ae7..1221897 100644 --- a/src/app/(main)/management/ManagementClient.tsx +++ b/src/app/(main)/management/ManagementClient.tsx @@ -20,6 +20,7 @@ import { Modal } from "@components/ui/Modal"; import { AuditTable, type AuditLog } from "@components/audit/AuditTable"; import { AuditExportModal } from "@components/audit/AuditExportModal"; import { VercelIcon } from "@components/icons/VercelIcon"; +import { AiProvidersSection } from "./AiProvidersSection"; type ProviderKey = | "google" @@ -97,7 +98,12 @@ export function ManagementClient() { useEffect(() => { const handleHashChange = () => { const hash = window.location.hash.replace("#", ""); - if (hash === "audit" || hash === "authentication" || hash === "sso") { + if ( + hash === "audit" || + hash === "authentication" || + hash === "sso" || + hash === "ai-providers" + ) { setActiveTab(hash); } else { setActiveTab("authentication"); @@ -117,6 +123,7 @@ export function ManagementClient() { const titles: Record = { authentication: dict.pages.management, sso: dict.pages.ssoProviders, + "ai-providers": dict.management.aiProviders || "AI Providers", audit: dict.pages.securityAudit, }; @@ -323,6 +330,15 @@ export function ManagementClient() { {dict.management.ssoProviders} +