134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth, getAuthSession } from "@/lib/auth";
|
|
import { db } from "@/lib/db";
|
|
import { mcPlayers, playerBans, playerChatHistory, playerSpawnPoints, auditLogs } from "@/lib/db/schema";
|
|
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
|
|
import { eq, desc } from "drizzle-orm";
|
|
import { rconClient } from "@/lib/minecraft/rcon";
|
|
import { sanitizeRconCommand } from "@/lib/security/sanitize";
|
|
import { z } from "zod";
|
|
import { nanoid } from "nanoid";
|
|
|
|
export async function GET(
|
|
req: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
const session = await getAuthSession(req.headers);
|
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
|
|
const { id } = await params;
|
|
const player = await db.select().from(mcPlayers).where(eq(mcPlayers.id, id)).get();
|
|
if (!player) return NextResponse.json({ error: "Player not found" }, { status: 404 });
|
|
|
|
const [bans, chatHistory, spawnPoints] = await Promise.all([
|
|
db.select().from(playerBans).where(eq(playerBans.playerId, id)).orderBy(desc(playerBans.bannedAt)),
|
|
db.select().from(playerChatHistory).where(eq(playerChatHistory.playerId, id)).orderBy(desc(playerChatHistory.timestamp)).limit(200),
|
|
db.select().from(playerSpawnPoints).where(eq(playerSpawnPoints.playerId, id)),
|
|
]);
|
|
|
|
return NextResponse.json({ player, bans, chatHistory, spawnPoints });
|
|
}
|
|
|
|
const BanSchema = z.object({
|
|
reason: z.string().min(1).max(500),
|
|
expiresAt: z.number().optional(),
|
|
});
|
|
|
|
export async function POST(
|
|
req: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
const session = await getAuthSession(req.headers);
|
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
if (!["superadmin", "admin", "moderator"].includes(session.user.role ?? "")) {
|
|
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 });
|
|
|
|
const { id } = await params;
|
|
const { searchParams } = new URL(req.url);
|
|
const action = searchParams.get("action");
|
|
|
|
const player = await db.select().from(mcPlayers).where(eq(mcPlayers.id, id)).get();
|
|
if (!player) return NextResponse.json({ error: "Player not found" }, { status: 404 });
|
|
|
|
if (action === "ban") {
|
|
let body: z.infer<typeof BanSchema>;
|
|
try {
|
|
body = BanSchema.parse(await req.json());
|
|
} catch {
|
|
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
|
|
}
|
|
|
|
// Insert ban record
|
|
await db.insert(playerBans).values({
|
|
id: nanoid(),
|
|
playerId: id,
|
|
reason: body.reason,
|
|
bannedBy: session.user.id,
|
|
bannedAt: Date.now(),
|
|
expiresAt: body.expiresAt ?? null,
|
|
isActive: true,
|
|
});
|
|
|
|
await db.update(mcPlayers).set({ isBanned: true }).where(eq(mcPlayers.id, id));
|
|
|
|
// Execute ban via RCON
|
|
try {
|
|
const cmd = sanitizeRconCommand(`ban ${player.username} ${body.reason}`);
|
|
await rconClient.sendCommand(cmd);
|
|
} catch { /* RCON might be unavailable */ }
|
|
|
|
// Audit
|
|
await db.insert(auditLogs).values({
|
|
id: nanoid(),
|
|
userId: session.user.id,
|
|
action: "player.ban",
|
|
target: "player",
|
|
targetId: id,
|
|
details: JSON.stringify({ reason: body.reason }),
|
|
ipAddress: ip,
|
|
createdAt: Date.now(),
|
|
});
|
|
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
if (action === "unban") {
|
|
await db.update(playerBans).set({ isActive: false, unbannedBy: session.user.id, unbannedAt: Date.now() }).where(eq(playerBans.playerId, id));
|
|
await db.update(mcPlayers).set({ isBanned: false }).where(eq(mcPlayers.id, id));
|
|
|
|
try {
|
|
const cmd = sanitizeRconCommand(`pardon ${player.username}`);
|
|
await rconClient.sendCommand(cmd);
|
|
} catch { /* RCON might be unavailable */ }
|
|
|
|
await db.insert(auditLogs).values({
|
|
id: nanoid(), userId: session.user.id, action: "player.unban",
|
|
target: "player", targetId: id, details: null, ipAddress: ip, createdAt: Date.now(),
|
|
});
|
|
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
if (action === "kick") {
|
|
const { reason } = await req.json();
|
|
try {
|
|
const cmd = sanitizeRconCommand(`kick ${player.username} ${reason ?? "Kicked by admin"}`);
|
|
await rconClient.sendCommand(cmd);
|
|
} catch (err) {
|
|
return NextResponse.json({ error: "RCON unavailable" }, { status: 503 });
|
|
}
|
|
await db.insert(auditLogs).values({
|
|
id: nanoid(), userId: session.user.id, action: "player.kick",
|
|
target: "player", targetId: id, details: JSON.stringify({ reason }), ipAddress: ip, createdAt: Date.now(),
|
|
});
|
|
return NextResponse.json({ success: true });
|
|
}
|
|
|
|
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
|
}
|