Initial push
This commit is contained in:
133
app/api/players/[id]/route.ts
Normal file
133
app/api/players/[id]/route.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } 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 auth.api.getSession({ headers: 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 auth.api.getSession({ headers: 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 });
|
||||
}
|
||||
Reference in New Issue
Block a user