diff --git a/src/actions/commandocentrum.ts b/src/actions/commandocentrum.ts index 76a25023..6fb86bbf 100644 --- a/src/actions/commandocentrum.ts +++ b/src/actions/commandocentrum.ts @@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache"; import { requireStaff } from "@/lib/admin/guard"; import { rcon } from "@/lib/services/rcon"; +import { CurrencyType } from "@/lib/services/currency"; const PATH = "/admin/commandocentrum"; @@ -51,3 +52,158 @@ export async function hotelAlert(formData: FormData): Promise { } revalidatePath(PATH); } + +/** Disconnect/kick a user from the hotel (rcon: disconnect). */ +export async function disconnectUser(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const username = String(formData.get("username") ?? "").trim(); + if (!userId || !username) return; + try { + await rcon.disconnectUser(userId, username); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Send an alert to a specific user (rcon: alertuser). */ +export async function alertUser(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const message = String(formData.get("message") ?? "").trim().slice(0, 512); + if (!userId || !message) return; + try { + await rcon.alertUser(userId, message); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Forward a user to a specific room (rcon: forwarduser). */ +export async function forwardUser(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const roomId = Number(formData.get("roomId")); + if (!userId || !roomId) return; + try { + await rcon.forwardUser(userId, roomId); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Give credits to a user (rcon: givecredits). */ +export async function giveCredits(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const credits = Number(formData.get("credits")); + if (!userId || !credits || credits <= 0) return; + try { + await rcon.giveCredits(userId, credits); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Give duckets to a user (rcon: givepoints type=duckets). */ +export async function giveDuckets(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const amount = Number(formData.get("amount")); + if (!userId || !amount || amount <= 0) return; + try { + await rcon.giveDuckets(userId, amount); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Give diamonds to a user (rcon: givepoints type=diamonds). */ +export async function giveDiamonds(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const amount = Number(formData.get("amount")); + if (!userId || !amount || amount <= 0) return; + try { + await rcon.giveDiamonds(userId, amount); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Give a badge to a user (rcon: givebadge). */ +export async function giveBadge(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const badge = String(formData.get("badge") ?? "").trim(); + if (!userId || !badge) return; + try { + await rcon.giveBadge(userId, badge); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Set a user's motto (rcon: setmotto). */ +export async function setMotto(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const motto = String(formData.get("motto") ?? "").trim().slice(0, 127); + if (!userId || !motto) return; + try { + await rcon.setMotto(userId, motto); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Set a user's rank (rcon: setrank). */ +export async function setRank(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const rank = Number(formData.get("rank")); + if (!userId || rank < 0 || rank > 10) return; + try { + await rcon.setRank(userId, rank); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Execute a command as a user (rcon: executecommand). */ +export async function executeCommand(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const command = String(formData.get("command") ?? "").trim(); + if (!userId || !command) return; + try { + await rcon.executeCommand(userId, command); + } catch { + // best-effort + } + revalidatePath(PATH); +} + +/** Send a gift to a user (rcon: sendgift). */ +export async function sendGift(formData: FormData): Promise { + await requireStaff(); + const userId = Number(formData.get("userId")); + const itemId = Number(formData.get("itemId")); + const message = String(formData.get("message") ?? "Here is a gift.").trim().slice(0, 255); + if (!userId || !itemId) return; + try { + await rcon.sendGift(userId, itemId, message); + } catch { + // best-effort + } + revalidatePath(PATH); +} diff --git a/src/app/admin/commandocentrum/page.tsx b/src/app/admin/commandocentrum/page.tsx index 80acaa1f..22c1c87f 100644 --- a/src/app/admin/commandocentrum/page.tsx +++ b/src/app/admin/commandocentrum/page.tsx @@ -4,6 +4,17 @@ import { updateCatalog, updateNavigator, updateWordFilter, + disconnectUser, + alertUser, + forwardUser, + giveCredits, + giveDuckets, + giveDiamonds, + giveBadge, + setMotto, + setRank, + executeCommand, + sendGift, } from "@/actions/commandocentrum"; import { DiagnosticRow, InfoItem, StatusCard } from "@/components/admin/dashboard"; import { prisma } from "@/lib/prisma"; @@ -21,8 +32,6 @@ function fromDate(d: Date | null | undefined): string { return d ? d.toISOString().slice(0, 19).replace("T", " ") : "—"; } -// stacktrace is a MySQL BLOB (Prisma `Bytes`), so it arrives as a Buffer/ -// Uint8Array. Decode to UTF-8 and trim to a single readable preview line. function decodeStacktrace(raw: unknown): string { if (raw == null) return ""; try { @@ -45,8 +54,6 @@ function uptime(seconds: number): string { } export default async function CommandoCentrum() { - // Gather everything in parallel; any failure degrades to a safe default so the - // panel still renders (and the DB-down state is itself a diagnostic). const [onlineCount, totalUsers, errors, dbOk, emulatorOnline, maintenance, activity] = await Promise.all([ prisma.user.count({ where: { online: "1" } }).catch(() => 0), @@ -63,7 +70,6 @@ export default async function CommandoCentrum() { .catch(() => [] as Awaited>), ]); - // Resolve staff usernames for the activity feed. const staffIds = Array.from(new Set(activity.map((a) => Number(a.userId)))).filter((n) => n > 0); let nameById = new Map(); if (staffIds.length) { @@ -224,6 +230,111 @@ export default async function CommandoCentrum() { + {/* ── User RCON actions ──────────────────────────────────── */} +
+

User actions (RCON)

+

+ Enter a User ID to perform actions on a specific user. Username required for disconnect. +

+ +
+
+

Disconnect / Kick

+

Force disconnect a user.

+ + + +
+ +
+

Alert user

+

Send a popup alert to a user.

+ + + +
+ +
+

Forward to room

+

Teleport user to a room ID.

+ + + +
+
+ +
+
+

Give credits

+

Add credits to a user.

+ + + +
+ +
+

Give duckets

+

Add duckets to a user.

+ + + +
+ +
+

Give diamonds

+

Add diamonds to a user.

+ + + +
+
+ +
+
+

Give badge

+

Give a badge code (e.g. HOF1).

+ + + +
+ +
+

Set motto

+

Change user's motto (max 127 chars).

+ + + +
+ +
+

Set rank

+

Change user rank (0–10).

+ + + +
+
+ +
+
+

Execute command

+

Run a chat command as the user.

+ + + +
+ +
+

Send gift

+

Send a furniture item as gift.

+ + + + +
+
+
+ {/* ── Staff activity ─────────────────────────────────────── */}

Recent staff activity

@@ -309,4 +420,4 @@ export default async function CommandoCentrum() {
); -} +} \ No newline at end of file diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index a8dbf0b4..dffb5fb3 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -9,7 +9,7 @@ const PER_PAGE = 25; export default async function AdminUsers({ searchParams, }: { - searchParams: Promise<{ q?: string; page?: string }>; + searchParams: Promise<{ q?: string; page?: string; form?: string }>; }) { const sp = await searchParams; const q = (sp.q ?? "").trim(); @@ -79,6 +79,7 @@ export default async function AdminUsers({ Rank Credits Status + Actions @@ -95,6 +96,29 @@ export default async function AdminUsers({ {u.online === "1" ? "online" : "offline"} + +
+ Edit + + + +
+ ))}