import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/lib/auth"; import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit"; import { sanitizeFilePath } from "@/lib/security/sanitize"; import { db } from "@/lib/db"; import { auditLogs } from "@/lib/db/schema"; import { nanoid } from "nanoid"; import * as fs from "node:fs"; import * as path from "node:path"; const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB const BLOCKED_EXTENSIONS = new Set([".exe", ".bat", ".cmd", ".sh", ".ps1"]); export async function POST(req: NextRequest) { const session = await auth.api.getSession({ headers: req.headers }); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!["superadmin", "admin"].includes(session.user.role ?? "")) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const ip = getClientIp(req); const { allowed } = checkRateLimit(ip, 20); if (!allowed) return NextResponse.json({ error: "Too many requests" }, { status: 429 }); const mcBase = path.resolve(process.env.MC_SERVER_PATH ?? "/opt/minecraft/server"); const targetDir = req.nextUrl.searchParams.get("path") ?? "/"; let resolvedDir: string; try { resolvedDir = sanitizeFilePath(targetDir, mcBase); } catch { return NextResponse.json({ error: "Invalid path" }, { status: 400 }); } const formData = await req.formData(); const file = formData.get("file") as File | null; if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 }); // Check file size if (file.size > MAX_FILE_SIZE) { return NextResponse.json({ error: "File too large (max 500 MB)" }, { status: 413 }); } // Check extension const ext = path.extname(file.name).toLowerCase(); if (BLOCKED_EXTENSIONS.has(ext)) { return NextResponse.json({ error: "File type not allowed" }, { status: 400 }); } // Sanitize filename: allow alphanumeric, dots, dashes, underscores, spaces const safeName = file.name.replace(/[^a-zA-Z0-9._\- ]/g, "_"); const destPath = path.join(resolvedDir, safeName); // Ensure destination is still within base if (!destPath.startsWith(mcBase)) { return NextResponse.json({ error: "Invalid destination" }, { status: 400 }); } const buffer = Buffer.from(await file.arrayBuffer()); fs.mkdirSync(resolvedDir, { recursive: true }); fs.writeFileSync(destPath, buffer); await db.insert(auditLogs).values({ id: nanoid(), userId: session.user.id, action: "file.upload", target: "file", targetId: path.relative(mcBase, destPath), details: JSON.stringify({ size: file.size, name: safeName }), ipAddress: ip, createdAt: Date.now(), }); return NextResponse.json({ success: true, path: path.relative(mcBase, destPath) }); }