import { NextRequest, NextResponse } from "next/server"; import { getBoothConfig, getBoothStyles } from "@/lib/config"; import { checkRateLimit, incrementRateLimit } from "@/lib/rate-limit"; import { orchestrateGeneration } from "@/lib/ai/orchestrator"; import { isContentSafetyError } from "@/lib/ai/safety"; import { logGenerationEvent } from "@/lib/storage"; import { validateAndSanitizeImage, getClientIp } from "@/lib/security"; export async function POST(req: NextRequest) { const requestStartTime = Date.now(); let clientId = "127.0.0.1"; let selectedStyle: any = null; let requestedStyleId = ""; try { const body = await req.json().catch(() => null); if (!body || typeof body !== "object") { return NextResponse.json( { error: "Invalid JSON request body" }, { status: 400 } ); } const { imageBase64, styleId, clientId: clientProvidedId, mimeType } = body; requestedStyleId = typeof styleId === "string" ? styleId.slice(0, 64) : ""; const config = getBoothConfig(); // 1. Image binary validation & magic bytes verification (rejects executables, scripts, etc.) const maxUploadBytes = (config.camera.maxUploadSizeMb || 10) * 1024 * 1024; const validation = validateAndSanitizeImage(imageBase64, maxUploadBytes); if (!validation.valid) { console.warn(`[Security Alert] Rejected image upload: ${validation.error}`); return NextResponse.json( { error: validation.error || "Invalid or disallowed image file" }, { status: 400 } ); } const styles = getBoothStyles(); // Find requested style selectedStyle = styles.find((s) => s.id === styleId) || styles[0]; if (!selectedStyle) { return NextResponse.json( { error: "No styles available in configuration" }, { status: 400 } ); } // Determine client identifier for rate-limiting (bind strictly to verified IP) const ip = getClientIp(req); // Sanitize optional device token if provided, but always prefix with verified IP so clients cannot evade IP quotas const safeClientTag = typeof clientProvidedId === "string" ? clientProvidedId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 32) : ""; clientId = safeClientTag ? `${ip}#${safeClientTag}` : ip; // Check device rate limit const limitCheck = checkRateLimit( clientId, config.limits.maxGenerationsPerDevice, config.limits.cooldownMinutes ); if (!limitCheck.allowed) { // Record rate limit refusal await logGenerationEvent({ styleId: selectedStyle.id, styleName: selectedStyle.name, provider: "unknown", model: "rate_limiter", durationMs: Date.now() - requestStartTime, status: "refused", errorType: "rate_limit", errorMessage: "Device reached maximum generation quota within cooldown window.", refusalReason: `Rate limit hit. Wait ${limitCheck.resetMinutes} min.`, clientId, }); return NextResponse.json( { error: "Rate limit reached for this device.", remaining: 0, resetMinutes: limitCheck.resetMinutes, }, { status: 429 } ); } // Execute generation pipeline with sanitized image and verified MIME type const result = await orchestrateGeneration({ config, style: selectedStyle, imageBase64: validation.sanitizedBase64!, mimeType: validation.mimeType, }); // Save successful generation & image file to persistent storage const record = await logGenerationEvent({ styleId: selectedStyle.id, styleName: selectedStyle.name, provider: result.providerUsed, model: result.modelUsed, durationMs: result.durationMs, status: "success", rawImageUrl: result.imageUrl, clientId, }); // Increment usage for this device incrementRateLimit(clientId); const updatedLimit = checkRateLimit( clientId, config.limits.maxGenerationsPerDevice, config.limits.cooldownMinutes ); const servedImageUrl = record.imageUrl || result.imageUrl; return NextResponse.json({ success: true, imageUrl: servedImageUrl, providerUsed: result.providerUsed, modelUsed: result.modelUsed, durationMs: result.durationMs, fallbackTriggered: result.fallbackTriggered, remainingQuota: updatedLimit.remaining, warnings: result.warnings, }); } catch (error: any) { console.error("[API Generate] Exception:", error); const isSafety = isContentSafetyError(error); // Record error or content safety refusal in admin logs try { await logGenerationEvent({ styleId: selectedStyle?.id || requestedStyleId || "unknown", styleName: selectedStyle?.name || "Unknown Disguise", provider: error.provider || "unknown", model: error.model || "ai-pipeline", durationMs: Date.now() - requestStartTime, status: isSafety ? "refused" : "error", errorType: isSafety ? "safety_refusal" : "server_error", errorMessage: error.message || "An unexpected error occurred during generation.", refusalReason: isSafety ? (error.message || "Content safety filter blocked output") : undefined, clientId, }); } catch (logErr) { console.error("[API Generate] Failed to log error event:", logErr); } return NextResponse.json( { success: false, isSafetyRejection: isSafety, error: isSafety ? "This photo was rejected by the content safety filter. Please snap or select a different photo." : (error.message || "An unexpected error occurred during generation."), }, { status: isSafety ? 422 : 500 } ); } }