Initial push

This commit is contained in:
2026-03-08 15:49:34 +01:00
parent 8da12bb7d1
commit 47127f276d
101 changed files with 13844 additions and 8 deletions

View File

@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { mcProcessManager } from "@/lib/minecraft/process";
import { db } from "@/lib/db";
import { auditLogs } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { z } from "zod";
import { nanoid } from "nanoid";
const ActionSchema = z.object({
action: z.enum(["start", "stop", "restart"]),
force: z.boolean().optional().default(false),
});
export async function POST(req: NextRequest) {
// Auth
const session = await auth.api.getSession({ headers: req.headers });
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Role check — only admin+
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Rate limiting
const ip = getClientIp(req);
const { allowed } = checkRateLimit(ip, 20); // stricter limit for control actions
if (!allowed) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
// Validate body
let body: z.infer<typeof ActionSchema>;
try {
body = ActionSchema.parse(await req.json());
} catch {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
}
const { action, force } = body;
try {
switch (action) {
case "start":
await mcProcessManager.start();
break;
case "stop":
await mcProcessManager.stop(force);
break;
case "restart":
await mcProcessManager.restart(force);
break;
}
// Audit log
await db.insert(auditLogs).values({
id: nanoid(),
userId: session.user.id,
action: `server.${action}${force ? ".force" : ""}`,
target: "server",
targetId: null,
details: JSON.stringify({ action, force }),
ipAddress: ip,
createdAt: Date.now(),
});
return NextResponse.json({ success: true, action });
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { serverSettings } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { z } from "zod";
const UpdateSettingsSchema = z.object({
minecraftPath: z.string().optional(),
serverJar: z.string().optional(),
serverVersion: z.string().optional(),
serverType: z.enum(["vanilla", "paper", "spigot", "bukkit", "fabric", "forge", "bedrock"]).optional(),
maxRam: z.number().min(512).max(32768).optional(),
minRam: z.number().min(256).max(32768).optional(),
rconEnabled: z.boolean().optional(),
rconPort: z.number().min(1).max(65535).optional(),
rconPassword: z.string().min(8).optional(),
javaArgs: z.string().max(1000).optional(),
autoStart: z.boolean().optional(),
restartOnCrash: z.boolean().optional(),
backupEnabled: z.boolean().optional(),
backupSchedule: z.string().optional(),
bluemapEnabled: z.boolean().optional(),
bluemapUrl: z.string().url().optional().or(z.literal("")),
});
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const settings = await db.select().from(serverSettings).get();
// Never return RCON password
if (settings) {
const { rconPassword: _, ...safe } = settings;
return NextResponse.json({ settings: safe });
}
return NextResponse.json({ settings: null });
}
export async function PATCH(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (session.user.role !== "superadmin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const ip = getClientIp(req);
const { allowed } = checkRateLimit(ip);
if (!allowed) return NextResponse.json({ error: "Too many requests" }, { status: 429 });
let body: z.infer<typeof UpdateSettingsSchema>;
try {
body = UpdateSettingsSchema.parse(await req.json());
} catch {
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}
const existing = await db.select().from(serverSettings).get();
if (existing) {
await db.update(serverSettings).set({ ...body, updatedAt: Date.now() });
} else {
await db.insert(serverSettings).values({ id: 1, ...body, updatedAt: Date.now() });
}
return NextResponse.json({ success: true });
}

View File

@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { mcProcessManager } from "@/lib/minecraft/process";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const ip = getClientIp(req);
const { allowed } = checkRateLimit(ip);
if (!allowed) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
const status = mcProcessManager.getStatus();
return NextResponse.json(status);
}

View File

@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { fetchVanillaVersions, fetchPaperVersions, fetchFabricVersions, type VersionInfo } from "@/lib/minecraft/versions";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);
const { allowed } = checkRateLimit(ip, 20);
if (!allowed) return NextResponse.json({ error: "Too many requests" }, { status: 429 });
const type = req.nextUrl.searchParams.get("type") ?? "vanilla";
try {
let versionInfos: VersionInfo[];
switch (type) {
case "paper":
versionInfos = await fetchPaperVersions();
break;
case "fabric":
versionInfos = await fetchFabricVersions();
break;
case "vanilla":
default:
versionInfos = await fetchVanillaVersions();
}
const versions = versionInfos.map((v) => v.id);
return NextResponse.json({ versions, type });
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to fetch versions";
return NextResponse.json({ error: message }, { status: 503 });
}
}