mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-03 01:50:23 +08:00
feat: graphical model management in admin panel
Replace the provider credential fields and raw AI_MODELS_CONFIG JSON textarea with a Models section mirroring the in-app model settings UI: provider instance list with logos, credential fields per provider type, model add/remove with suggestions, per-model connectivity test, and a default-provider star. On save the server derives everything the runtime needs into settings.json: credential env vars (with _2 suffixes for multiple instances of one provider), AI_MODELS_CONFIG, and AI_PROVIDER/AI_MODEL for the default. Secrets round-trip as masked markers and are never sent back to the browser. The general settings registry now only covers non-provider settings (generation, access, features, observability, quota).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { timingSafeEqual } from "crypto"
|
||||
import { checkAdminAuth, maskSecret } from "@/lib/admin/auth"
|
||||
import {
|
||||
getEnvFallback,
|
||||
getValueSource,
|
||||
@@ -11,46 +11,10 @@ import {
|
||||
SETTINGS_REGISTRY,
|
||||
type SettingDef,
|
||||
} from "@/lib/admin/settings-registry"
|
||||
import { ServerModelsConfigSchema } from "@/lib/server-model-config"
|
||||
|
||||
// Zod schemas for json-type settings (kept here, server-side only — the
|
||||
// registry is imported by the client and must stay free of server deps)
|
||||
const JSON_VALIDATORS: Record<string, typeof ServerModelsConfigSchema> = {
|
||||
AI_MODELS_CONFIG: ServerModelsConfigSchema,
|
||||
}
|
||||
|
||||
export const runtime = "nodejs"
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
function checkAuth(req: Request): Response | null {
|
||||
const password = process.env.ADMIN_PASSWORD
|
||||
if (!password) {
|
||||
return Response.json(
|
||||
{
|
||||
error: "Admin panel is disabled. Set the ADMIN_PASSWORD environment variable to enable it.",
|
||||
},
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
const provided = req.headers.get("x-admin-password") || ""
|
||||
const a = Buffer.from(provided)
|
||||
const b = Buffer.from(password)
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
return Response.json(
|
||||
{ error: "Invalid admin password" },
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function maskSecret(value: string): { isSet: true; hint: string } {
|
||||
return {
|
||||
isSet: true,
|
||||
hint: value.length > 8 ? `…${value.slice(-4)}` : "••••",
|
||||
}
|
||||
}
|
||||
|
||||
function serializeSettings() {
|
||||
const fileValues = loadSettings()
|
||||
return SETTINGS_REGISTRY.map((def) => {
|
||||
@@ -65,7 +29,7 @@ function serializeSettings() {
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const authError = checkAuth(req)
|
||||
const authError = checkAdminAuth(req)
|
||||
if (authError) return authError
|
||||
|
||||
return Response.json({
|
||||
@@ -93,29 +57,13 @@ function validateValue(def: SettingDef, value: string): string | null {
|
||||
return def.options?.includes(value)
|
||||
? null
|
||||
: `Must be one of: ${def.options?.join(", ")}`
|
||||
case "json": {
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(value)
|
||||
} catch {
|
||||
return "Invalid JSON"
|
||||
}
|
||||
const schema = JSON_VALIDATORS[def.key]
|
||||
if (schema) {
|
||||
const result = schema.safeParse(parsed)
|
||||
if (!result.success) {
|
||||
return `Invalid value: ${result.error.issues[0]?.message ?? "schema mismatch"}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
const authError = checkAuth(req)
|
||||
const authError = checkAdminAuth(req)
|
||||
if (authError) return authError
|
||||
|
||||
if (!isSettingsWritable()) {
|
||||
|
||||
Reference in New Issue
Block a user