Berechtigungen werden nun beim Verbinden überprüft, Bannen wird nun im Piloten-fenster gelogt

This commit is contained in:
PxlLoewe
2025-06-23 23:30:12 -07:00
parent dabcad2525
commit 2e5340d8be
14 changed files with 224 additions and 39 deletions

View File

@@ -1,7 +1,10 @@
"use server";
import { Prisma, prisma } from "@repo/db";
export const addPenalty = async (data: Prisma.PenaltyCreateInput) => {
export const addPenalty = async (
data: Prisma.Without<Prisma.PenaltyCreateInput, Prisma.PenaltyUncheckedCreateInput> &
Prisma.PenaltyUncheckedCreateInput,
) => {
return await prisma.penalty.create({
data,
});

View File

@@ -7,6 +7,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { formatDistance } from "date-fns";
import { de } from "date-fns/locale";
import { cn } from "../../../../helper/cn";
import { HobbyKnifeIcon } from "@radix-ui/react-icons";
export const penaltyColumns: ColumnDef<Penalty & { Report: Report }>[] = [
{
@@ -39,10 +40,17 @@ export const penaltyColumns: ColumnDef<Penalty & { Report: Report }>[] = [
</div>
);
}
case "PERMISSIONS_REVOCED":
return (
<div className={cn("text-error flex gap-3", row.original.suspended && "text-gray-400")}>
<LockKeyhole /> Rechte entzogen {row.original.suspended && "(ausgesetzt)"}
</div>
);
case "BAN":
return (
<div className={cn("text-error flex gap-3", row.original.suspended && "text-gray-400")}>
<LockKeyhole /> Bann {row.original.suspended && "(ausgesetzt)"}
<HobbyKnifeIcon /> Bann {row.original.suspended && "(ausgesetzt)"}
</div>
);
}

View File

@@ -0,0 +1,105 @@
import { ReactNode, useState } from "react";
import { cn } from "../../../../../../helper/cn";
export const PenaltyDropdown = ({
onClick,
btnClassName,
showDatePicker,
btnTip,
Icon,
}: {
onClick: (data: { reason: string; until: Date | null }) => void;
showDatePicker?: boolean;
btnClassName?: string;
btnTip?: string;
Icon: ReactNode;
}) => {
const [reason, setReason] = useState("");
const [until, setUntil] = useState<string>("default");
return (
<details className="dropdown dropdown-left dropdown-center">
<summary className={cn("btn btn-xs btn-square btn-soft", btnClassName)}>{Icon}</summary>
<div className="dropdown-content flex gap-3 items-center bg-base-100 rounded-box z-1 p-2 mr-3 shadow-sm">
<input
value={reason}
onChange={(e) => setReason(e.target.value)}
type="text"
className="input min-w-[250px]"
placeholder="Begründung"
/>
{showDatePicker && (
<select
className="select min-w-[150px] select-bordered"
value={until}
onChange={(e) => setUntil(e.target.value)}
>
<option value="default" disabled>
Unbegrenzt
</option>
<option value="1h">1 Stunde</option>
<option value="6h">6 Stunden</option>
<option value="12h">12 Stunden</option>
<option value="24h">24 Stunden</option>
<option value="72h">72 Stunden</option>
<option value="1w">1 Woche</option>
<option value="2w">2 Wochen</option>
<option value="1m">1 Monat</option>
<option value="3m">3 Monate</option>
<option value="6m">6 Monate</option>
<option value="1y">1 Jahr</option>
</select>
)}
<button
className={cn("btn btn-square btn-soft tooltip tooltip-bottom", btnClassName)}
data-tip={btnTip}
onClick={() => {
let untilDate: Date | null = null;
if (until !== "default") {
const now = new Date();
switch (until) {
case "1h":
untilDate = new Date(now.getTime() + 1 * 60 * 60 * 1000);
break;
case "6h":
untilDate = new Date(now.getTime() + 6 * 60 * 60 * 1000);
break;
case "12h":
untilDate = new Date(now.getTime() + 12 * 60 * 60 * 1000);
break;
case "24h":
untilDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
break;
case "72h":
untilDate = new Date(now.getTime() + 72 * 60 * 60 * 1000);
break;
case "1w":
untilDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
break;
case "2w":
untilDate = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
break;
case "1m":
untilDate = new Date(now.setMonth(now.getMonth() + 1));
break;
case "3m":
untilDate = new Date(now.setMonth(now.getMonth() + 3));
break;
case "6m":
untilDate = new Date(now.setMonth(now.getMonth() + 6));
break;
case "1y":
untilDate = new Date(now.setFullYear(now.getFullYear() + 1));
break;
default:
untilDate = null;
}
}
onClick({ reason, until: untilDate });
}}
>
{Icon}
</button>
</div>
</details>
);
};

View File

@@ -33,13 +33,24 @@ import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod";
import { useRouter } from "next/navigation";
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
import { cn } from "../../../../../../helper/cn";
import { ChartBarBigIcon, Check, Eye, PlaneIcon, Timer, X } from "lucide-react";
import {
ChartBarBigIcon,
Check,
Eye,
LockKeyhole,
PlaneIcon,
RedoDot,
Timer,
X,
} from "lucide-react";
import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table";
import { Error } from "_components/Error";
import { useSession } from "next-auth/react";
import { setStandardName } from "../../../../../../helper/discord";
import { penaltyColumns } from "(app)/admin/penalty/page";
import { PenaltyDropdown } from "(app)/admin/user/[id]/_components/AddPenaltyDropdown";
import { addPenalty } from "(app)/admin/penalty/actions";
interface ProfileFormProps {
user: User;
@@ -304,12 +315,60 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
};
export const UserPenalties = ({ user }: { user: User }) => {
const createdUser = useSession().data?.user;
const penaltyTable = useRef<PaginatedTableRef>(null);
return (
<div className="card-body">
<h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Nutzer Strafen
<h2 className="card-title flex justify-between">
<span className="flex items-center gap-2">
<ExclamationTriangleIcon className="w-5 h-5" /> Audit-log
</span>
<div className="flex gap-2">
<PenaltyDropdown
Icon={<RedoDot size={15} />}
onClick={async ({ reason, until }) => {
if (!reason) return toast.error("Bitte gib einen Grund für die Strafe an.");
if (!until) return toast.error("Bitte gib eine Dauer für die Strafe ein.");
if (!createdUser)
return toast.error("Du musst eingeloggt sein, um eine Strafe zu erstellen.");
await addPenalty({
reason,
until,
type: "TIME_BAN",
userId: user.id,
createdUserId: createdUser.id,
});
penaltyTable.current?.refresh();
toast.success("Time-Ban wurde hinzugefügt!");
}}
btnClassName="btn btn-outline btn-warning tooltip-warning"
btnTip="Timeban hinzufügen"
showDatePicker={true}
/>
<PenaltyDropdown
Icon={<LockKeyhole size={15} />}
onClick={async ({ reason }) => {
if (!reason) return toast.error("Bitte gib einen Grund für die Strafe an.");
if (!createdUser)
return toast.error("Du musst eingeloggt sein, um eine Strafe zu erstellen.");
await addPenalty({
reason,
type: "BAN",
userId: user.id,
createdUserId: createdUser.id,
});
await editUser(user.id, { isBanned: true, permissions: [] });
penaltyTable.current?.refresh();
toast.success("Ban wurde hinzugefügt!");
}}
btnClassName="btn btn-outline btn-error tooltip-error"
btnTip="Rechte-entzug hinzufügen"
/>
</div>
</h2>
<PaginatedTable
initialOrderBy={[{ id: "timestamp", desc: true }]}
ref={penaltyTable}
prismaModel="penalty"
include={{
CreatedUser: true,
@@ -443,24 +502,6 @@ export const AdminForm = ({
>
<LockOpen1Icon /> Passwort zurücksetzen
</Button>
{!user.isBanned && (
<Button
onClick={async () => {
await editUser(user.id, { isBanned: true });
toast.success("Nutzer wurde gesperrt!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
router.refresh();
}}
role="submit"
className="btn-sm flex-1 min-w-[250px] btn-outline btn-error"
>
<HobbyKnifeIcon /> HUB zugang sperren
</Button>
)}
{user.isBanned && (
<Button
onClick={async () => {