/** * Admin-only thumbnail proxy — serves small thumbnails for the admin panel * using raw Immich asset UUIDs (no token encoding needed since admin is authenticated). */ import { NextRequest, NextResponse } from 'next/server'; import { withAdmin } from '@/lib/admin/withAdmin'; import { getConfig } from '@/lib/config'; import { isUuid } from '@/lib/uuid'; export const GET = withAdmin( async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => { const config = getConfig(); // Credentials, not `needsSetup`: this route is how the operator picks the // albums that gallery.yaml is built from, so refusing to run until that file // exists is a deadlock (#507). if (config.needsCredentials) { return NextResponse.json({ error: 'Not configured' }, { status: 503 }); } const { id: assetId } = await params; if (!isUuid(assetId)) { return NextResponse.json({ error: 'Invalid asset ID' }, { status: 400 }); } try { const res = await fetch( // `edited=true`: the pickers show a photo as edited in Immich, like // the site does (#831). For a photo that is not edited Immich returns // the same bytes, so it needs no lookup first. `${config.immich.apiUrl}/assets/${assetId}/thumbnail?size=thumbnail&edited=true`, { headers: { 'x-api-key': config.immich.apiKey, Accept: 'application/octet-stream', }, }, ); if (!res.ok) { return NextResponse.json({ error: 'Asset not found' }, { status: 404 }); } const contentType = res.headers.get('content-type') || 'image/jpeg'; const body = res.body; return new NextResponse(body, { headers: { 'Content-Type': contentType, 'Cache-Control': 'private, max-age=3600', }, }); } catch (err) { console.error('[Admin Thumbnail] Failed to fetch:', err); return NextResponse.json({ error: 'Failed to fetch thumbnail' }, { status: 502 }); } }, );