f30e7158-97a8-4eaa-b0e8-a28e8a54a3fe
This commit is contained in:
1 parent
9feb124da6
commit
e16d7a748a
24 files changed
+641
-12
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Typed setting schema — validation per key (PRD §5.5: "Typed, database-backed")
|
||||
const SETTING_SCHEMA = z.record(z.string(), z.string())
|
||||
|
||||
// GET /api/aos/settings — all settings as a typed object
|
||||
export async function GET() {
|
||||
const rows = await db.setting.findMany()
|
||||
const settings: Record<string, string> = {}
|
||||
for (const r of rows) settings[r.key] = r.value
|
||||
return NextResponse.json({ settings })
|
||||
}
|
||||
|
||||
// PATCH /api/aos/settings — update multiple settings at once.
|
||||
// Each key is validated against an allow-list to prevent arbitrary writes.
|
||||
const ALLOWED_KEYS = new Set([
|
||||
'company.name', 'company.legalForm', 'company.vatId', 'company.registerCourt', 'company.registerNumber',
|
||||
'company.city', 'company.zip', 'company.street', 'company.bank', 'company.bic',
|
||||
'invoice.numberPrefix', 'invoice.numberYear', 'invoice.numberNext',
|
||||
'tax.standardBp', 'tax.reducedBp', 'tax.reverseChargeDefault',
|
||||
'leadFinder.radiusKm', 'leadFinder.costCapEur', 'leadFinder.autoApproveThreshold',
|
||||
'erechnung.format', 'erechnung.leitwegId',
|
||||
'compliance.retentionInvoicesYears', 'compliance.retentionBusinessYears',
|
||||
'compliance.encryptionKeyEscrow',
|
||||
'locale.default', 'locale.available',
|
||||
'mail.smtpHost', 'mail.smtpPort', 'mail.imapHost', 'mail.imapPort',
|
||||
])
|
||||
|
||||
// Numeric keys that must parse as numbers
|
||||
const NUMERIC_KEYS = new Set([
|
||||
'invoice.numberNext', 'tax.standardBp', 'tax.reducedBp',
|
||||
'leadFinder.radiusKm', 'leadFinder.costCapEur', 'leadFinder.autoApproveThreshold',
|
||||
'mail.smtpPort', 'mail.imapPort',
|
||||
'compliance.retentionInvoicesYears', 'compliance.retentionBusinessYears',
|
||||
])
|
||||
|
||||
export async function PATCH(req: Request) {
|
||||
const body = (await req.json()) as Record<string, string>
|
||||
const parsed = SETTING_SCHEMA.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: 'invalid payload', details: parsed.error.flatten() }, { status: 400 })
|
||||
}
|
||||
|
||||
const updates = parsed.data
|
||||
const errors: string[] = []
|
||||
const before: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (!ALLOWED_KEYS.has(key)) {
|
||||
errors.push(`unknown key: ${key}`)
|
||||
continue
|
||||
}
|
||||
if (NUMERIC_KEYS.has(key)) {
|
||||
if (isNaN(Number(value))) {
|
||||
errors.push(`${key} must be numeric, got "${value}"`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
const existing = await db.setting.findUnique({ where: { key } })
|
||||
if (existing) before[key] = existing.value
|
||||
await db.setting.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
})
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return NextResponse.json({ error: 'validation_failed', details: errors }, { status: 422 })
|
||||
}
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'lena@nordlicht.digital',
|
||||
action: 'settings.update',
|
||||
entity: 'Setting',
|
||||
entityId: 'bulk',
|
||||
before: JSON.stringify(before),
|
||||
after: JSON.stringify(updates),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ ok: true, updated: Object.keys(updates).length })
|
||||
}
|
||||
Reference in new issue
Block a user