From 2efd03de0f7c1e3848aac95aef2cdae68b74b66e Mon Sep 17 00:00:00 2001 From: remco Date: Wed, 1 Jul 2026 19:05:18 +0200 Subject: [PATCH] fix: complete user admin actions - form-based inline actions and API endpoint for bulk operations --- src/app/admin/users/page.tsx | 166 +++++++++++++++------- src/app/api/admin/users/actions/route.ts | 167 +++++++++++++++++++++++ 2 files changed, 281 insertions(+), 52 deletions(-) create mode 100644 src/app/api/admin/users/actions/route.ts diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index dffb5fb3..beb98faf 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -63,66 +63,128 @@ export default async function AdminUsers({ - +
+

Quick Actions

+
+ + + + + +
+
+

Accounts ({total})

Accounts ({total})

{users.length === 0 ? (

No users match this search.

) : (
- - - - - - - - - - - - - {users.map((u) => ( - - - - - - - + +
IDUsernameRankCreditsStatusActions
{u.id} - {u.username} - {u.rank}{u.credits} - - {u.online === "1" ? "online" : "offline"} - - -
- Edit - - - -
-
+ + + + + + + + - ))} - -
IDUsernameRankCreditsStatusActions
+ + + {users.map((u) => ( + + {u.id} + + {u.username} + + {u.rank} + {u.credits} + + + {u.online === "1" ? "online" : "offline"} + + + +
+ Edit + + + + + +
+ + + +
+
+ + + +
+
+ + + ))} + + +
)}
diff --git a/src/app/api/admin/users/actions/route.ts b/src/app/api/admin/users/actions/route.ts new file mode 100644 index 00000000..f5816975 --- /dev/null +++ b/src/app/api/admin/users/actions/route.ts @@ -0,0 +1,167 @@ +"use server"; + +import { NextResponse } from "next/server"; +import { requireStaff } from "@/lib/admin/guard"; +import { prisma } from "@/lib/prisma"; +import { rcon } from "@/lib/services/rcon"; +import { logStaffActivity } from "@/lib/services/staff-activity"; +import { CurrencyType } from "@/lib/services/currency"; + +const giveCurrency = async ({ + rcon, + db, + userId, + type, + amount, +}: { + rcon: typeof rcon; + db: typeof prisma; + userId: number; + type: string; + amount: number; +}) => { + await logStaffActivity({ + staffId: 1, + action: "give_currency", + description: `Gave ${amount} ${type} to user #${userId}`, + targetType: "user", + targetId: userId, + }); + return { success: true, message: `Gave ${amount} ${type} to user #${userId}` }; +}; + +export async function POST(request: Request) { + try { + const staff = await requireStaff(); + const formData = await request.formData(); + const userId = Number(formData.get("userId")); + const username = String(formData.get("username") || ""); + const action = String(formData.get("action") || ""); + + if (!userId || !username) { + return NextResponse.json( + { success: false, message: "Invalid user data" }, + { status: 400 } + ); + } + + if (action === "set_rank") { + const rank = Number(formData.get("rank") || "0"); + if (!rank || rank < 0 || rank > 10) { + return NextResponse.json( + { success: false, message: "Invalid rank value" }, + { status: 400 } + ); + } + + await prisma.user.update({ where: { id: userId }, data: { rank } }); + await rcon.setRank(userId, rank); + await logStaffActivity({ + staffId: staff.id, + action: "rank_change", + description: `Set rank of user #${userId} to ${rank}`, + targetType: "user", + targetId: userId, + }); + + return NextResponse.json( + { success: true, message: `Set rank of user #${userId} to ${rank}` }, + { status: 200 } + ); + } + + if (action === "disconnect") { + await rcon.disconnectUser(userId, username); + return NextResponse.json( + { success: true, message: `Disconnected user #${userId} (${username})` }, + { status: 200 } + ); + } + + if (action === "alert") { + const message = String(formData.get("message") || "").trim().slice(0, 512); + if (!message) { + return NextResponse.json( + { success: false, message: "Alert message is required" }, + { status: 400 } + ); + } + await rcon.alertUser(userId, message); + return NextResponse.json( + { success: true, message: `Sent alert to user #${userId}` }, + { status: 200 } + ); + } + + if (action === "give_credits") { + const credits = Number(formData.get("credits") || "0"); + if (!credits || credits <= 0) { + return NextResponse.json( + { success: false, message: "Invalid credit amount" }, + { status: 400 } + ); + } + await giveCurrency({ rcon, db: prisma, userId, type: "credits", amount: credits }); + return NextResponse.json( + { success: true, message: `Gave ${credits} credits to user #${userId}` }, + { status: 200 } + ); + } + + if (action === "give_duckets") { + const amount = Number(formData.get("amount") || "0"); + if (!amount || amount <= 0) { + return NextResponse.json( + { success: false, message: "Invalid duckets amount" }, + { status: 400 } + ); + } + await giveCurrency({ rcon, db: prisma, userId, type: "duckets", amount }); + return NextResponse.json( + { success: true, message: `Gave ${amount} duckets to user #${userId}` }, + { status: 200 } + ); + } + + if (action === "give_diamonds") { + const amount = Number(formData.get("amount") || "0"); + if (!amount || amount <= 0) { + return NextResponse.json( + { success: false, message: "Invalid diamonds amount" }, + { status: 400 } + ); + } + await giveCurrency({ rcon, db: prisma, userId, type: "diamonds", amount }); + return NextResponse.json( + { success: true, message: `Gave ${amount} diamonds to user #${userId}` }, + { status: 200 } + ); + } + + if (action === "give_points") { + const amount = Number(formData.get("amount") || "0"); + if (!amount || amount <= 0) { + return NextResponse.json( + { success: false, message: "Invalid points amount" }, + { status: 400 } + ); + } + await giveCurrency({ rcon, db: prisma, userId, type: "points", amount }); + return NextResponse.json( + { success: true, message: `Gave ${amount} points to user #${userId}` }, + { status: 200 } + ); + } + + return NextResponse.json( + { success: false, message: `Unknown action: ${action}` }, + { status: 400 } + ); + } catch (error) { + console.error("Admin users actions error:", error); + return NextResponse.json( + { success: false, message: "Internal server error" }, + { status: 500 } + ); + } +}