import { NextRequest, NextResponse } from 'next/server' import { readFile, stat } from 'fs/promises' import path from 'path' import { auth } from '@/auth' import { canAccessUploadedNoteImage } from '@/lib/upload-access' const UPLOAD_DIR = path.join(process.cwd(), 'data', 'uploads') const MIME_MAP: Record = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', } export async function GET( _req: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { const session = await auth() const { path: segments } = await params // Only serve from uploads/notes/ (flat or userId/file) if (!segments.length || segments[0] !== 'notes') { return new NextResponse('Not found', { status: 404 }) } // Reject path traversal if (segments.some((s) => s === '..' || s.includes('\0'))) { return new NextResponse('Forbidden', { status: 403 }) } const filename = segments[segments.length - 1] // Never serve meta sidecars as images if (filename.endsWith('.meta.json')) { return new NextResponse('Not found', { status: 404 }) } const allowed = await canAccessUploadedNoteImage(segments, session?.user?.id) if (!allowed) { return new NextResponse(session?.user?.id ? 'Forbidden' : 'Unauthorized', { status: session?.user?.id ? 403 : 401, headers: { // Do not let browsers / CDNs cache a denial (would stick a broken image) 'Cache-Control': 'no-store', }, }) } const ext = path.extname(filename).toLowerCase() const contentType = MIME_MAP[ext] if (!contentType) { return new NextResponse('Unsupported file type', { status: 400 }) } const safePath = path.join(UPLOAD_DIR, ...segments) if (!safePath.startsWith(UPLOAD_DIR + path.sep) && safePath !== UPLOAD_DIR) { return new NextResponse('Forbidden', { status: 403 }) } try { const fileStats = await stat(safePath) if (!fileStats.isFile()) { return new NextResponse('Not found', { status: 404 }) } const buffer = await readFile(safePath) return new NextResponse(buffer, { headers: { 'Content-Type': contentType, // private: only for authenticated session fetches; still long cache after auth ok 'Cache-Control': 'private, max-age=31536000, immutable', 'Content-Length': String(buffer.length), }, }) } catch { return new NextResponse('Not found', { status: 404 }) } }