72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
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) });
|
|
}
|