fix: complete user admin actions - form-based inline actions and API endpoint for bulk operations
This commit is contained in:
1 parent
c3de7cb576
commit
2efd03de0f
2 files changed
+281
-52
No files matched your search
+114
-52
@@ -63,66 +63,128 @@ export default async function AdminUsers({
|
|||||||
<button type="submit" className="btn btn-primary">
|
<button type="submit" className="btn btn-primary">
|
||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<section className="admin-section">
|
||||||
|
<h2>Quick Actions</h2>
|
||||||
|
<form
|
||||||
|
action="/api/admin/users/actions"
|
||||||
|
method="post"
|
||||||
|
className="card"
|
||||||
|
style={{ display: "flex", gap: "1rem", alignItems: "center", flexWrap: "wrap", marginBottom: "1.5rem" }}
|
||||||
|
>
|
||||||
|
<input type="hidden" name="action" value="alert" />
|
||||||
|
<input
|
||||||
|
name="userId"
|
||||||
|
type="number"
|
||||||
|
placeholder="User ID"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
style={{ width: 150 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="message"
|
||||||
|
type="text"
|
||||||
|
placeholder="Alert message (max 512 chars)"
|
||||||
|
maxLength={512}
|
||||||
|
style={{ flex: 1, minWidth: 300 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Send Alert
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="admin-section">
|
<section className="admin-section">
|
||||||
|
<h2>Accounts ({total})</h2>
|
||||||
<h2>Accounts ({total})</h2>
|
<h2>Accounts ({total})</h2>
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<p className="muted">No users match this search.</p>
|
<p className="muted">No users match this search.</p>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ overflowX: "auto" }}>
|
<div style={{ overflowX: "auto" }}>
|
||||||
<table>
|
<form action="/api/admin/users/actions" method="post">
|
||||||
<thead>
|
<table>
|
||||||
<tr>
|
<thead>
|
||||||
<th>ID</th>
|
<tr>
|
||||||
<th>Username</th>
|
<th>ID</th>
|
||||||
<th>Rank</th>
|
<th>Username</th>
|
||||||
<th>Credits</th>
|
<th>Rank</th>
|
||||||
<th>Status</th>
|
<th>Credits</th>
|
||||||
<th>Actions</th>
|
<th>Status</th>
|
||||||
</tr>
|
<th style={{ width: "200px" }}>Actions</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{users.map((u) => (
|
|
||||||
<tr key={u.id}>
|
|
||||||
<td className="muted">{u.id}</td>
|
|
||||||
<td>
|
|
||||||
<Link href={`/admin/users/${u.id}`}>{u.username}</Link>
|
|
||||||
</td>
|
|
||||||
<td>{u.rank}</td>
|
|
||||||
<td>{u.credits}</td>
|
|
||||||
<td>
|
|
||||||
<span className={`admin-badge ${u.online === "1" ? "ok" : ""}`}>
|
|
||||||
{u.online === "1" ? "online" : "offline"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div style={{ display: "flex", gap: "0.25rem", flexWrap: "wrap" }}>
|
|
||||||
<Link href={`/admin/users/${u.id}/edit`} className="btn btn-outline" style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}>Edit</Link>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline"
|
|
||||||
onClick={() => alert(`Set rank for ${u.username}`)}
|
|
||||||
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
|
|
||||||
>Set Rank</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-danger"
|
|
||||||
onClick={() => alert(`Disconnect ${u.username}`)}
|
|
||||||
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
|
|
||||||
>Disconnect</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => alert(`Alert ${u.username}`)}
|
|
||||||
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
|
|
||||||
>Alert</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{users.map((u) => (
|
||||||
|
<tr key={u.id}>
|
||||||
|
<td className="muted">{u.id}</td>
|
||||||
|
<td>
|
||||||
|
<Link href={`/admin/users/${u.id}`}>{u.username}</Link>
|
||||||
|
</td>
|
||||||
|
<td>{u.rank}</td>
|
||||||
|
<td>{u.credits}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`admin-badge ${u.online === "1" ? "ok" : ""}`}>
|
||||||
|
{u.online === "1" ? "online" : "offline"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style={{ display: "flex", gap: "0.25rem", flexWrap: "wrap", justifyContent: "flex-start" }}>
|
||||||
|
<Link href={`/admin/users/${u.id}/edit`} className="btn btn-outline" style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}>Edit</Link>
|
||||||
|
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
|
||||||
|
<input type="hidden" name="userId" value={u.id} />
|
||||||
|
<input type="hidden" name="username" value={u.username} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
value="set_rank"
|
||||||
|
className="btn btn-outline"
|
||||||
|
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!confirm(`Set rank for ${u.username}?`)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
>Set Rank</button>
|
||||||
|
</form>
|
||||||
|
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
|
||||||
|
<input type="hidden" name="userId" value={u.id} />
|
||||||
|
<input type="hidden" name="username" value={u.username} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
value="disconnect"
|
||||||
|
className="btn btn-danger"
|
||||||
|
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!confirm(`Disconnect ${u.username}?`)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
>Disconnect</button>
|
||||||
|
</form>
|
||||||
|
<form style={{ display: "inline-block" }} action="/api/admin/users/actions" method="post">
|
||||||
|
<input type="hidden" name="userId" value={u.id} />
|
||||||
|
<input type="hidden" name="username" value={u.username} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
value="alert"
|
||||||
|
className="btn btn-primary"
|
||||||
|
style={{ fontSize: "0.75rem", padding: "0.25rem 0.5rem" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!confirm(`Send alert to ${u.username}?`)) e.preventDefault();
|
||||||
|
}}
|
||||||
|
>Alert</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user