import { type NextRequest, NextResponse } from "next/server"; import { HABBO_IMAGER_URL, resolveImagerBase } from "@/lib/imager"; import { siteSettings } from "@/lib/services/site-settings"; /** * GET /api/imaging/avatar?figure=…&size=l&direction=2&… * * Redirects to Habbo's public imager. Server-side proxying fails in production * (Habbo blocks datacenter IPs), so the browser loads the image from Habbo. */ const FIGURE_RE = /^[a-z]{2}-\d+/i; const FIGURE_MAX_LEN = 512; const FIGURE_MAX_PARTS = 24; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const figure = ( searchParams.get("figure") ?? searchParams.get("look") ?? "" ).trim(); if (!figure) { return NextResponse.json( { error: "Missing figure parameter" }, { status: 400 }, ); } if (figure.length > FIGURE_MAX_LEN || !FIGURE_RE.test(figure)) { return NextResponse.json( { error: "Invalid figure format" }, { status: 400 }, ); } if (figure.split(".").length > FIGURE_MAX_PARTS) { return NextResponse.json( { error: "Figure has too many parts" }, { status: 400 }, ); } const sizeParam = searchParams.get("size"); const size: "s" | "m" | "l" = sizeParam === "s" || sizeParam === "l" ? sizeParam : "m"; const direction = clampInt(searchParams.get("direction"), 0, 7, 2); const headDirection = clampInt( searchParams.get("head_direction") ?? searchParams.get("headDirection"), 0, 7, 3, ); const headOnly = searchParams.get("headonly") === "1" || searchParams.get("headOnly") === "1"; const gesture = searchParams.get("gesture") ?? undefined; const action = searchParams.get("action") ?? undefined; const params = new URLSearchParams({ figure, direction: String(direction), head_direction: String(headDirection), size, }); if (headOnly) params.set("headonly", "1"); if (gesture) params.set("gesture", gesture); if (action) params.set("action", action); let base = HABBO_IMAGER_URL; try { const configured = await siteSettings.get( "habbo_imaging_url", HABBO_IMAGER_URL, ); base = resolveImagerBase(configured); } catch { base = HABBO_IMAGER_URL; } const target = `${base}?${params.toString()}`; return NextResponse.redirect(target, { status: 302, headers: { "Cache-Control": "public, max-age=3600", "Access-Control-Allow-Origin": "*", }, }); } export async function OPTIONS() { return new NextResponse(null, { status: 204, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, If-None-Match", }, }); } function clampInt( raw: string | null, min: number, max: number, fallback: number, ): number { if (raw == null) return fallback; const n = Number.parseInt(raw, 10); if (Number.isNaN(n) || n < min || n > max) return fallback; return n; }