import { NextResponse } from 'next/server'; import { z } from 'zod'; import { requireSuperAdmin, withAuth, withPermission } from '@/lib/api/helpers'; import { parseBody } from '@/lib/api/route-helpers'; import { errorResponse, ValidationError } from '@/lib/errors'; import { getPublicOcrConfig, saveOcrConfig, OCR_MODELS } from '@/lib/services/ocr-config.service'; const saveSchema = z.object({ /** When 'global', requires super_admin and stores at port_id=null. */ scope: z.enum(['port', 'global']), provider: z.enum(['openai', 'claude']), model: z.string().min(1), apiKey: z.string().optional(), clearApiKey: z.boolean().optional(), useGlobal: z.boolean().optional(), aiEnabled: z.boolean().optional(), manualEntry: z.boolean().optional(), }); // Only role tiers that hold `admin.manage_settings` (director / super_admin) // may read or write the OCR config: the apiKey is stored encrypted but is // passed straight into the receipt-scan handler, so a swapped key would // exfiltrate every subsequent receipt image to whatever endpoint that key // authenticates with. export const GET = withAuth( withPermission('admin', 'manage_settings', async (req, ctx) => { try { const url = new URL(req.url); const scope = url.searchParams.get('scope') ?? 'port'; if (scope === 'global') { requireSuperAdmin(ctx, 'admin.ocr-settings.read.global'); } const config = await getPublicOcrConfig(scope === 'global' ? null : ctx.portId); return NextResponse.json({ data: config, models: OCR_MODELS }); } catch (error) { return errorResponse(error); } }), ); export const PUT = withAuth( withPermission('admin', 'manage_settings', async (req, ctx) => { try { const body = await parseBody(req, saveSchema); if (body.scope === 'global') { requireSuperAdmin(ctx, 'admin.ocr-settings.write.global'); } const validModels = OCR_MODELS[body.provider]; if (!validModels.includes(body.model)) { throw new ValidationError(`Invalid model for provider ${body.provider}`); } await saveOcrConfig( body.scope === 'global' ? null : ctx.portId, { provider: body.provider, model: body.model, apiKey: body.apiKey, clearApiKey: body.clearApiKey, useGlobal: body.useGlobal, aiEnabled: body.aiEnabled, manualEntry: body.manualEntry, }, ctx.userId, ); return new NextResponse(null, { status: 204 }); } catch (error) { return errorResponse(error); } }), );