// /api/thumbnails/[...path]/route.ts // This endpoint serves thumbnails from the thumbnails directory import { NextResponse } from 'next/server'; import * as fs from 'fs'; import * as path from 'path'; import { getViewer } from '@/lib/video-access'; // Path to thumbnails directory (should match UPLOADS_DIR/thumbnails) const THUMBNAILS_DIR = path.join(process.env.UPLOADS_DIR || '/uploads', 'thumbnails'); export async function GET( request: Request, context: { params: Promise<{ path?: string[] }> } ) { try { const params = await context.params; const pathSegments = params?.path || []; if (pathSegments.length === 0) { return NextResponse.json( { error: 'Invalid thumbnail request' }, { status: 400 } ); } // Thumbnails show course content, so they need a signed-in user. if (!(await getViewer())) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // Construct the file path and validate it const requestedPath = path.join(THUMBNAILS_DIR, ...pathSegments); // Security: prevent directory traversal attacks if (!requestedPath.startsWith(THUMBNAILS_DIR + path.sep)) { return NextResponse.json( { error: 'Invalid request' }, { status: 403 } ); } // Check if file exists if (!fs.existsSync(requestedPath)) { console.log(`[Thumbnails] File not found: ${requestedPath}`); return NextResponse.json( { error: 'Not found' }, { status: 404 } ); } // Read the file const fileContent = await fs.promises.readFile(requestedPath); // Determine content type based on file extension let contentType = 'application/octet-stream'; if (requestedPath.endsWith('.jpg') || requestedPath.endsWith('.jpeg')) { contentType = 'image/jpeg'; } else if (requestedPath.endsWith('.png')) { contentType = 'image/png'; } else if (requestedPath.endsWith('.webp')) { contentType = 'image/webp'; } else if (requestedPath.endsWith('.gif')) { contentType = 'image/gif'; } // Return the file with appropriate headers return new NextResponse(fileContent, { status: 200, headers: { 'Content-Type': contentType, 'Cache-Control': 'private, max-age=31536000, immutable', // Cache for 1 year (thumbnails don't change) }, }); } catch (error) { console.error('[Thumbnails] Error serving thumbnail file:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } } // HEAD request support for thumbnail validation export async function HEAD( request: Request, context: { params: Promise<{ path?: string[] }> } ) { try { const params = await context.params; const pathSegments = params?.path || []; if (pathSegments.length === 0) { return new NextResponse(null, { status: 400 }); } if (!(await getViewer())) return new NextResponse(null, { status: 401 }); const requestedPath = path.join(THUMBNAILS_DIR, ...pathSegments); // Security: prevent directory traversal attacks if (!requestedPath.startsWith(THUMBNAILS_DIR + path.sep)) { return new NextResponse(null, { status: 403 }); } // Check if file exists if (!fs.existsSync(requestedPath)) { return new NextResponse(null, { status: 404 }); } // Return headers only return new NextResponse(null, { status: 200, headers: { 'Content-Type': 'image/jpeg', // Default for HEAD requests 'Cache-Control': 'private, max-age=31536000, immutable', }, }); } catch (error) { console.error('[Thumbnails] Error in HEAD request:', error); return new NextResponse(null, { status: 500 }); } }