Penalty Übersicht für Nutzer und Penalty-Log

This commit is contained in:
PxlLoewe
2025-06-21 22:05:16 -07:00
parent 4732ecb770
commit 93962a9ce4
15 changed files with 402 additions and 25 deletions

View File

@@ -13,7 +13,6 @@ export const Penalty = async () => {
type: "TIME_BAN",
},
});
console.log("Open Penaltys:", openPenaltys);
if (!openPenaltys[0]) {
return null;
}

View File

@@ -0,0 +1,30 @@
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { prisma } from "@repo/db";
import { Error } from "_components/Error";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const penalty = await prisma.penalty.findUnique({
where: {
id: Number(id),
},
include: {
User: true,
CreatedUser: true,
},
});
if (!penalty) return <Error statusCode={404} title="User not found" />;
return (
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2">
<ExclamationTriangleIcon className="w-5 h-5" />
Strafe #{penalty.id}
</p>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,15 @@
import { Error } from "_components/Error";
import { getServerSession } from "api/auth/[...nextauth]/auth";
export default async function ReportLayout({ children }: { children: React.ReactNode }) {
const session = await getServerSession();
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
const user = session.user;
if (!user?.permissions.includes("ADMIN_EVENT"))
return <Error title="Keine Berechtigung" statusCode={403} />;
return <>{children}</>;
}

View File

@@ -0,0 +1,88 @@
"use client";
import { Eye, LockKeyhole, RedoDot, Timer } from "lucide-react";
import Link from "next/link";
import { PaginatedTable } from "_components/PaginatedTable";
import { Penalty, PenaltyType, Report, User } from "@repo/db";
import { ColumnDef } from "@tanstack/react-table";
import { formatDistance } from "date-fns";
import { de } from "date-fns/locale";
export default function ReportPage() {
return (
<PaginatedTable
prismaModel="penalty"
include={{
CreatedUser: true,
Report: true,
}}
columns={
[
{
accessorKey: "type",
header: "Typ",
cell: ({ row }) => {
switch (row.getValue("type") as PenaltyType) {
case "KICK":
return (
<div className="text-warning flex gap-3">
<RedoDot />
Kick
</div>
);
case "TIME_BAN": {
const length = formatDistance(
new Date(row.original.timestamp),
new Date(row.original.until || Date.now()),
{ locale: de },
);
return (
<div className="text-warning flex gap-3">
<Timer />
Zeit Sperre ({length})
</div>
);
}
case "BAN":
return (
<div className="text-error flex gap-3">
<LockKeyhole /> Bann
</div>
);
}
},
},
{
accessorKey: "CreatedUser",
header: "Bestraft durch",
cell: ({ row }) => {
const user = row.getValue("CreatedUser") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => {
const report = row.original.Report;
if (!report[0]) return null;
return (
<Link href={`/admin/report/${report[0].id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" />
Report Anzeigen
</button>
</Link>
);
},
},
] as ColumnDef<Penalty & { Report: Report[] }>[]
}
/>
);
}

View File

@@ -1,7 +1,11 @@
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { prisma } from "@repo/db";
import { Error } from "_components/Error";
import { ReportAdmin, ReportSenderInfo } from "(app)/admin/report/_components/form";
import {
ReportAdmin,
ReportPenalties,
ReportSenderInfo,
} from "(app)/admin/report/_components/form";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
@@ -33,6 +37,9 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ReportAdmin report={report} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6">
<ReportPenalties report={report} />
</div>
</div>
);
}

View File

@@ -1,11 +1,15 @@
"use client";
import { editReport } from "(app)/admin/report/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import { Report as IReport, User } from "@repo/db";
import { Report as IReport, Penalty, PenaltyType, Report, User } from "@repo/db";
import { ReportSchema, Report as IReportZod } from "@repo/db/zod";
import { ColumnDef } from "@tanstack/react-table";
import { PaginatedTable } from "_components/PaginatedTable";
import { Button } from "_components/ui/Button";
import { Switch } from "_components/ui/Switch";
import { Trash } from "lucide-react";
import { formatDistance } from "date-fns";
import { de } from "date-fns/locale";
import { Eye, LockKeyhole, RedoDot, Shield, Timer, Trash } from "lucide-react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -24,10 +28,12 @@ export const ReportSenderInfo = ({
const { Reported, Sender } = report;
return (
<div className="card-body">
<Link href={`/admin/user/${Reported?.id}`} className="card-title link link-hover">
<h2 className="card-title">
<Link href={`/admin/user/${Reported?.id}`} className=" link link-hover">
{Reported?.firstname} {Reported?.lastname} ({Reported?.publicId}) als{" "}
<span className="text-primary">{report.reportedUserRole}</span>
</Link>
<span className="text-primary">{report.reportedUserRole}</span>
</h2>
<div className="textarea w-full text-left">{report.text}</div>
<Link
href={`/admin/user/${Reported?.id}`}
@@ -85,7 +91,7 @@ export const ReportAdmin = ({
{report.Reviewer &&
`Kommentar von ${Reviewer?.firstname} ${Reviewer?.lastname} (${Reviewer?.publicId})`}
</p>
<Switch form={form} name="reviewed" label="Report als geklärt markieren" />
<Switch form={form} name="reviewed" label="Report als erledigt markieren" />
<div className="card-actions flex justify-between">
<Button
role="submit"
@@ -126,3 +132,109 @@ export const ReportAdmin = ({
</form>
);
};
export const ReportPenalties = ({
report,
}: {
report: IReport & {
Reported?: User;
Sender?: User;
Reviewer?: User | null;
};
}) => {
if (!report.penaltyId)
return (
<div className="card-body">
<h2 className="card-title">
<Shield className="w-5 h-5" /> Strafen zu diesem Report
</h2>
<p className="text-sm text-gray-600">Es wurden keine Strafen zu diesem Report erfasst.</p>
</div>
);
return (
<div className="card-body">
<h2 className="card-title">
<Shield className="w-5 h-5" /> Strafen zu diesem Report
</h2>
<PaginatedTable
prismaModel="penalty"
include={{
CreatedUser: true,
Report: true,
}}
filter={{
id: report.penaltyId,
}}
columns={
[
{
accessorKey: "type",
header: "Typ",
cell: ({ row }) => {
switch (row.getValue("type") as PenaltyType) {
case "KICK":
return (
<div className="text-warning flex gap-3">
<RedoDot />
Kick
</div>
);
case "TIME_BAN": {
const length = formatDistance(
new Date(row.original.timestamp),
new Date(row.original.until || Date.now()),
{ locale: de },
);
return (
<div className="text-warning flex gap-3">
<Timer />
Zeit Sperre ({length})
</div>
);
}
case "BAN":
return (
<div className="text-error flex gap-3">
<LockKeyhole /> Bann
</div>
);
}
},
},
{
accessorKey: "CreatedUser",
header: "Bestraft durch",
cell: ({ row }) => {
const user = row.getValue("CreatedUser") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => {
const report = row.original.Report;
if (!report[0]) return null;
return (
<Link href={`/admin/report/${report[0].id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" />
Report Anzeigen
</button>
</Link>
);
},
},
] as ColumnDef<Penalty & { Report: Report[] }>[]
}
/>
</div>
);
};

View File

@@ -5,6 +5,8 @@ import {
ConnectedAircraft,
ConnectedDispatcher,
DiscordAccount,
Penalty,
PenaltyType,
PERMISSION,
Report,
Station,
@@ -31,12 +33,23 @@ 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 { de } from "date-fns/locale";
import { formatDistance } from "date-fns";
interface ProfileFormProps {
user: User;
@@ -300,11 +313,99 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
);
};
export const UserPenalties = ({ user }: { user: User }) => {
return (
<div className="card-body">
<h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Nutzer Strafen
</h2>
<PaginatedTable
prismaModel="penalty"
include={{
CreatedUser: true,
Report: true,
}}
filter={{
userId: user.id,
}}
columns={
[
{
accessorKey: "type",
header: "Typ",
cell: ({ row }) => {
switch (row.getValue("type") as PenaltyType) {
case "KICK":
return (
<div className="text-warning flex gap-3">
<RedoDot />
Kick
</div>
);
case "TIME_BAN": {
const length = formatDistance(
new Date(row.original.timestamp),
new Date(row.original.until || Date.now()),
{ locale: de },
);
return (
<div className="text-warning flex gap-3">
<Timer />
Zeit Sperre ({length})
</div>
);
}
case "BAN":
return (
<div className="text-error flex gap-3">
<LockKeyhole /> Bann
</div>
);
}
},
},
{
accessorKey: "CreatedUser",
header: "Bestraft durch",
cell: ({ row }) => {
const user = row.getValue("CreatedUser") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) => new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => {
const report = row.original.Report;
if (!report[0]) return null;
return (
<Link href={`/admin/report/${report[0].id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" />
Report Anzeigen
</button>
</Link>
);
},
},
] as ColumnDef<Penalty & { Report: Report[] }>[]
}
/>
</div>
);
};
export const UserReports = ({ user }: { user: User }) => {
return (
<div className="card-body">
<h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> User Reports
<ExclamationTriangleIcon className="w-5 h-5" /> Nutzer Reports
</h2>
<PaginatedTable
prismaModel="report"
@@ -433,7 +534,7 @@ export const AdminForm = ({
role="submit"
className="btn-sm btn-wide btn-outline btn-error"
>
<HobbyKnifeIcon /> User Sperren
<HobbyKnifeIcon /> HUB zugang sperren
</Button>
)}
{user.isBanned && (
@@ -451,7 +552,7 @@ export const AdminForm = ({
role="submit"
className="btn-sm btn-wide btn-outline btn-warning"
>
<HobbyKnifeIcon /> User Entperren
<HobbyKnifeIcon /> HUB zugang entsperren
</Button>
)}
{discordAccount && (
@@ -509,8 +610,6 @@ export const AdminForm = ({
<h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Reports
</h2>
{/* TODO: Report summary Here */}
<div className="stats flex">
<div className="stat">
<div className="stat-figure text-primary">

View File

@@ -1,6 +1,12 @@
import { PersonIcon } from "@radix-ui/react-icons";
import { prisma } from "@repo/db";
import { AdminForm, ConnectionHistory, ProfileForm, UserReports } from "./_components/forms";
import {
AdminForm,
ConnectionHistory,
ProfileForm,
UserPenalties,
UserReports,
} from "./_components/forms";
import { Error } from "../../../../_components/Error";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
@@ -115,6 +121,9 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6">
<UserReports user={user} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6">
<UserPenalties user={user} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6">
<ConnectionHistory user={user} />
</div>

View File

@@ -12,6 +12,12 @@ export const editUser = async (id: string, data: Prisma.UserUpdateInput) => {
});
};
export const addPenalty = async (data: Prisma.PenaltyCreateInput) => {
return await prisma.penalty.create({
data,
});
};
export const resetPassword = async (id: string) => {
const array = new Uint8Array(8);
crypto.getRandomValues(array);

View File

@@ -80,6 +80,11 @@ export const VerticalNav = async () => {
<Link href="/admin/report">Reports</Link>
</li>
)}
{session.user.permissions.includes("ADMIN_USER") && (
<li>
<Link href="/admin/penalty">Audit-Log</Link>
</li>
)}
</ul>
</details>
</li>

View File

@@ -19,7 +19,7 @@
"node": ">=18",
"pnpm": ">=10"
},
"packageManager": "pnpm@10.11.1",
"packageManager": "pnpm@10.12.1",
"workspaces": [
"apps/*",
"packages/*"

View File

@@ -7,8 +7,7 @@ model Penalty {
reason String
until DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
timestamp DateTime @default(now())
// relations:
User User @relation(fields: [userId], references: [id])

14
pnpm-lock.yaml generated
View File

@@ -92,7 +92,7 @@ importers:
version: 0.5.7(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.13.3(@types/dom-mediacapture-record@1.0.22))
'@next-auth/prisma-adapter':
specifier: ^1.0.7
version: 1.0.7(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
version: 1.0.7(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
'@radix-ui/react-icons':
specifier: ^1.3.2
version: 1.3.2(react@19.1.0)
@@ -167,7 +167,7 @@ importers:
version: 15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
version: 4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
npm:
specifier: ^11.4.1
version: 11.4.1
@@ -325,7 +325,7 @@ importers:
version: 5.0.1(react-hook-form@7.57.0(react@19.1.0))
'@next-auth/prisma-adapter':
specifier: ^1.0.7
version: 1.0.7(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
version: 1.0.7(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
'@radix-ui/react-icons':
specifier: ^1.3.2
version: 1.3.2(react@19.1.0)
@@ -403,7 +403,7 @@ importers:
version: 15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
version: 4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next-remove-imports:
specifier: ^1.0.12
version: 1.0.12(webpack@5.99.9)
@@ -6596,10 +6596,10 @@ snapshots:
'@tybys/wasm-util': 0.9.0
optional: true
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))':
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))':
dependencies:
'@prisma/client': 6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3)
next-auth: 4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next-auth: 4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@next/env@15.3.3': {}
@@ -10945,7 +10945,7 @@ snapshots:
neo-async@2.6.2: {}
next-auth@4.24.11(next@15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
next-auth@4.24.11(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/runtime': 7.27.4
'@panva/hkdf': 1.2.1