fix: OpenRouter API key save fails with network error
All checks were successful
Deploy to VPS / deploy (push) Successful in 40s

The 0004_add_openrouter_provider.sql migration existed but was never
registered in _journal.json, so the 'openrouter' value was missing from
the api_key_provider PostgreSQL enum. Inserting an OpenRouter key threw
a DB error that was unhandled, causing Next.js to return an HTML 500;
the frontend's res.json() then threw, showing "Netzwerkfehler".

Fixes:
- Add 0004_add_openrouter_provider to _journal.json (idx 7) so the
  migration runs on next deploy and registers 'openrouter' in the enum
- Fix null-label duplicate check: use isNull() instead of passing
  undefined to and(), which incorrectly matched all provider keys
- Wrap DB insert in try/catch to return a proper JSON error instead of
  crashing with an unhandled exception

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO
2026-04-15 08:41:32 +00:00
parent 3366f84137
commit b8f4427f90
2 changed files with 34 additions and 21 deletions

View File

@@ -50,6 +50,13 @@
"when": 1776451200000,
"tag": "0006_seed_system_skills_fix",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1776537600000,
"tag": "0004_add_openrouter_provider",
"breakpoints": true
}
]
}

View File

@@ -3,7 +3,7 @@
import { db } from '@/lib/db';
import { tenantApiKeys } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
import { eq, and, isNull } from 'drizzle-orm';
import { requirePermission } from '@/lib/auth/rbac';
import { encrypt, keyHint } from '@/lib/crypto';
import { logAuditEvent } from '@/lib/auth/audit';
@@ -53,7 +53,7 @@ export async function POST(request: Request) {
return Response.json({ error: 'API-Schlüssel ist erforderlich (mindestens 8 Zeichen).' }, { status: 400 });
}
// Check for duplicate provider+label
// Check for duplicate provider+label (null labels must use IS NULL, not equality)
const existing = await db
.select({ id: tenantApiKeys.id })
.from(tenantApiKeys)
@@ -61,7 +61,7 @@ export async function POST(request: Request) {
and(
eq(tenantApiKeys.tenantId, ctx.tenantId),
eq(tenantApiKeys.provider, provider as AIProvider),
label ? eq(tenantApiKeys.label, label) : undefined,
label ? eq(tenantApiKeys.label, label) : isNull(tenantApiKeys.label),
),
)
.limit(1);
@@ -84,24 +84,30 @@ export async function POST(request: Request) {
}
const hint = keyHint(apiKey);
const [created] = await db
.insert(tenantApiKeys)
.values({
tenantId: ctx.tenantId,
provider: provider as AIProvider,
encryptedKey,
keyHint: hint,
label: label || null,
createdByUserId: ctx.userId,
})
.returning({
id: tenantApiKeys.id,
provider: tenantApiKeys.provider,
keyHint: tenantApiKeys.keyHint,
label: tenantApiKeys.label,
isActive: tenantApiKeys.isActive,
createdAt: tenantApiKeys.createdAt,
});
let created: { id: string; provider: string; keyHint: string; label: string | null; isActive: boolean; createdAt: Date };
try {
[created] = await db
.insert(tenantApiKeys)
.values({
tenantId: ctx.tenantId,
provider: provider as AIProvider,
encryptedKey,
keyHint: hint,
label: label || null,
createdByUserId: ctx.userId,
})
.returning({
id: tenantApiKeys.id,
provider: tenantApiKeys.provider,
keyHint: tenantApiKeys.keyHint,
label: tenantApiKeys.label,
isActive: tenantApiKeys.isActive,
createdAt: tenantApiKeys.createdAt,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return Response.json({ error: `Datenbankfehler beim Speichern: ${msg}` }, { status: 500 });
}
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
await logAuditEvent(ctx, 'create', 'tenant_api_key', created.id, { provider, label: label || null }, ip);