diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index fb3bbde067..5a49ebe6a2 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -1498,8 +1498,8 @@ function CompatibleModelsSection({ setAdding(true); try { - // Save to customModels DB so it shows up in /v1/models - await fetch("/api/provider-models", { + // Save to customModels DB FIRST - only create alias if this succeeds + const customModelRes = await fetch("/api/provider-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -1509,11 +1509,18 @@ function CompatibleModelsSection({ source: "manual", }), }); - // Also create alias for routing + + if (!customModelRes.ok) { + const errorData = await customModelRes.json().catch(() => ({})); + throw new Error(errorData.error?.message || "Failed to save custom model"); + } + + // Only create alias after customModel is saved successfully await onSetAlias(modelId, resolvedAlias, providerStorageAlias); setNewModel(""); } catch (error) { console.log("Error adding model:", error); + alert(error instanceof Error ? error.message : "Failed to add model. Please try again."); } finally { setAdding(false); } @@ -1540,8 +1547,9 @@ function CompatibleModelsSection({ if (!modelId) return false; const resolvedAlias = resolveAlias(modelId); if (!resolvedAlias) return false; - // Save to customModels DB so it shows up in /v1/models - await fetch("/api/provider-models", { + + // Save to customModels DB FIRST - only create alias if this succeeds + const customModelRes = await fetch("/api/provider-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -1551,7 +1559,13 @@ function CompatibleModelsSection({ source: "imported", }), }); - // Also create alias for routing + + if (!customModelRes.ok) { + console.error("Failed to save imported model to customModels DB"); + return false; + } + + // Only create alias after customModel is saved successfully await onSetAlias(modelId, resolvedAlias, providerStorageAlias); return true; } diff --git a/src/app/api/models/alias/route.ts b/src/app/api/models/alias/route.ts index 63c8813c0b..2f18b95018 100644 --- a/src/app/api/models/alias/route.ts +++ b/src/app/api/models/alias/route.ts @@ -2,10 +2,46 @@ import { NextResponse } from "next/server"; import { getModelAliases, setModelAlias, deleteModelAlias, isCloudEnabled } from "@/models"; import { getConsistentMachineId } from "@/shared/utils/machineId"; import { syncToCloud } from "@/lib/cloudSync"; +import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; +import { jwtVerify } from "jose"; +import { cookies } from "next/headers"; + +/** + * Verify authentication - check API key or JWT cookie + */ +async function verifyAuth(request) { + // Check API key (for external clients) + const apiKey = extractApiKey(request); + if (apiKey && (await isValidApiKey(apiKey))) { + return true; + } + + // Check JWT cookie (for dashboard session) + if (process.env.JWT_SECRET) { + try { + const cookieStore = await cookies(); + const token = cookieStore.get("auth_token")?.value; + if (token) { + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + await jwtVerify(token, secret); + return true; + } + } catch { + // Invalid/expired token or cookies not available + } + } + + return false; +} // GET /api/models/alias - Get all aliases -export async function GET() { +export async function GET(request) { try { + // Require authentication for security + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + } + const aliases = await getModelAliases(); return NextResponse.json({ aliases }); } catch (error) { @@ -17,6 +53,11 @@ export async function GET() { // PUT /api/models/alias - Set model alias export async function PUT(request) { try { + // Require authentication for security + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + } + const body = await request.json(); const { model, alias } = body; @@ -37,6 +78,11 @@ export async function PUT(request) { // DELETE /api/models/alias?alias=xxx - Delete alias export async function DELETE(request) { try { + // Require authentication for security + if (!(await verifyAuth(request))) { + return NextResponse.json({ error: "Authentication required" }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const alias = searchParams.get("alias"); diff --git a/src/app/api/provider-models/route.ts b/src/app/api/provider-models/route.ts index d3ce7aa66a..0c62ab790a 100644 --- a/src/app/api/provider-models/route.ts +++ b/src/app/api/provider-models/route.ts @@ -4,6 +4,37 @@ import { addCustomModel, removeCustomModel, } from "@/lib/localDb"; +import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; +import { jwtVerify } from "jose"; +import { cookies } from "next/headers"; + +/** + * Verify authentication - check API key or JWT cookie + */ +async function verifyAuth(request) { + // Check API key (for external clients) + const apiKey = extractApiKey(request); + if (apiKey && (await isValidApiKey(apiKey))) { + return true; + } + + // Check JWT cookie (for dashboard session) + if (process.env.JWT_SECRET) { + try { + const cookieStore = await cookies(); + const token = cookieStore.get("auth_token")?.value; + if (token) { + const secret = new TextEncoder().encode(process.env.JWT_SECRET); + await jwtVerify(token, secret); + return true; + } + } catch { + // Invalid/expired token or cookies not available + } + } + + return false; +} /** * GET /api/provider-models?provider= @@ -11,6 +42,14 @@ import { */ export async function GET(request) { try { + // Require authentication for security + if (!(await verifyAuth(request))) { + return Response.json( + { error: { message: "Authentication required", type: "invalid_api_key" } }, + { status: 401 } + ); + } + const { searchParams } = new URL(request.url); const provider = searchParams.get("provider"); @@ -31,6 +70,14 @@ export async function GET(request) { */ export async function POST(request) { try { + // Require authentication for security + if (!(await verifyAuth(request))) { + return Response.json( + { error: { message: "Authentication required", type: "invalid_api_key" } }, + { status: 401 } + ); + } + const body = await request.json(); const { provider, modelId, modelName, source } = body; @@ -56,6 +103,14 @@ export async function POST(request) { */ export async function DELETE(request) { try { + // Require authentication for security + if (!(await verifyAuth(request))) { + return Response.json( + { error: { message: "Authentication required", type: "invalid_api_key" } }, + { status: 401 } + ); + } + const { searchParams } = new URL(request.url); const provider = searchParams.get("provider"); const modelId = searchParams.get("model");