Files
hoerdle/app/api/audio/[filename]/route.ts

56 lines
1.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { readFile, stat } from 'fs/promises';
import path from 'path';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
try {
const { filename } = await params;
// Security: Prevent path traversal attacks
// Only allow alphanumeric, hyphens, underscores, and dots
const safeFilenamePattern = /^[a-zA-Z0-9_\-\.]+\.mp3$/;
if (!safeFilenamePattern.test(filename)) {
return new NextResponse('Invalid filename', { status: 400 });
}
// Additional check: ensure no path separators
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
return new NextResponse('Invalid filename', { status: 400 });
}
const filePath = path.join(process.cwd(), 'public/uploads', filename);
// Security: Verify the resolved path is still within uploads directory
const uploadsDir = path.join(process.cwd(), 'public/uploads');
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(uploadsDir)) {
return new NextResponse('Forbidden', { status: 403 });
}
// Check if file exists
try {
await stat(filePath);
} catch {
return new NextResponse('File not found', { status: 404 });
}
// Read file
const fileBuffer = await readFile(filePath);
// Return with proper headers
return new NextResponse(fileBuffer, {
headers: {
'Content-Type': 'audio/mpeg',
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=3600, must-revalidate',
},
});
} catch (error) {
console.error('Error serving audio file:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}