diff --git a/apps/hub/app/(app)/_components/Penalty.tsx b/apps/hub/app/(app)/_components/Penalty.tsx index 6a3f07ab..5c0a4ded 100644 --- a/apps/hub/app/(app)/_components/Penalty.tsx +++ b/apps/hub/app/(app)/_components/Penalty.tsx @@ -13,7 +13,6 @@ export const Penalty = async () => { type: "TIME_BAN", }, }); - console.log("Open Penaltys:", openPenaltys); if (!openPenaltys[0]) { return null; } diff --git a/apps/hub/app/(app)/admin/penalty/[id]/page.tsx b/apps/hub/app/(app)/admin/penalty/[id]/page.tsx new file mode 100644 index 00000000..5335ff2c --- /dev/null +++ b/apps/hub/app/(app)/admin/penalty/[id]/page.tsx @@ -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 ; + + return ( +
+
+

+ + Strafe #{penalty.id} +

+
+
+ ); +} diff --git a/apps/hub/app/(app)/admin/penalty/_components/form.tsx b/apps/hub/app/(app)/admin/penalty/_components/form.tsx new file mode 100644 index 00000000..e69de29b diff --git a/apps/hub/app/(app)/admin/penalty/actions.ts b/apps/hub/app/(app)/admin/penalty/actions.ts new file mode 100644 index 00000000..df7f9e98 --- /dev/null +++ b/apps/hub/app/(app)/admin/penalty/actions.ts @@ -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, + }); +}; diff --git a/apps/hub/app/(app)/admin/penalty/layout.tsx b/apps/hub/app/(app)/admin/penalty/layout.tsx new file mode 100644 index 00000000..e13fe5ac --- /dev/null +++ b/apps/hub/app/(app)/admin/penalty/layout.tsx @@ -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 ; + + const user = session.user; + + if (!user?.permissions.includes("ADMIN_EVENT")) + return ; + + return <>{children}; +} diff --git a/apps/hub/app/(app)/admin/penalty/page.tsx b/apps/hub/app/(app)/admin/penalty/page.tsx new file mode 100644 index 00000000..162acc91 --- /dev/null +++ b/apps/hub/app/(app)/admin/penalty/page.tsx @@ -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 ( + { + switch (row.getValue("type") as PenaltyType) { + case "KICK": + return ( +
+ + Kick +
+ ); + case "TIME_BAN": { + const length = formatDistance( + new Date(row.original.timestamp), + new Date(row.original.until || Date.now()), + { locale: de }, + ); + return ( +
+ + Zeit Sperre ({length}) +
+ ); + } + case "BAN": + return ( +
+ Bann +
+ ); + } + }, + }, + { + 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 ( + + + + ); + }, + }, + ] as ColumnDef[] + } + /> + ); +} diff --git a/apps/hub/app/(app)/admin/report/[id]/page.tsx b/apps/hub/app/(app)/admin/report/[id]/page.tsx index 178bd992..546aad4b 100644 --- a/apps/hub/app/(app)/admin/report/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/report/[id]/page.tsx @@ -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 }>
+
+ +
); } diff --git a/apps/hub/app/(app)/admin/report/_components/form.tsx b/apps/hub/app/(app)/admin/report/_components/form.tsx index 8e5b81f5..04806719 100644 --- a/apps/hub/app/(app)/admin/report/_components/form.tsx +++ b/apps/hub/app/(app)/admin/report/_components/form.tsx @@ -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 (
- - {Reported?.firstname} {Reported?.lastname} ({Reported?.publicId}) als{" "} +

+ + {Reported?.firstname} {Reported?.lastname} ({Reported?.publicId}) als{" "} + {report.reportedUserRole} - +

{report.text}
- +
+ + ); + }, + }, + ] as ColumnDef[] + } + /> +
+ ); +}; diff --git a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx index a9e79a19..7918dba2 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx @@ -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 ( +
+

+ Nutzer Strafen +

+ { + switch (row.getValue("type") as PenaltyType) { + case "KICK": + return ( +
+ + Kick +
+ ); + case "TIME_BAN": { + const length = formatDistance( + new Date(row.original.timestamp), + new Date(row.original.until || Date.now()), + { locale: de }, + ); + return ( +
+ + Zeit Sperre ({length}) +
+ ); + } + case "BAN": + return ( +
+ Bann +
+ ); + } + }, + }, + { + 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 ( + + + + ); + }, + }, + ] as ColumnDef[] + } + /> +
+ ); +}; + export const UserReports = ({ user }: { user: User }) => { return (

- User Reports + Nutzer Reports

- User Sperren + HUB zugang sperren )} {user.isBanned && ( @@ -451,7 +552,7 @@ export const AdminForm = ({ role="submit" className="btn-sm btn-wide btn-outline btn-warning" > - User Entperren + HUB zugang entsperren )} {discordAccount && ( @@ -509,8 +610,6 @@ export const AdminForm = ({

Reports

- - {/* TODO: Report summary Here */}
diff --git a/apps/hub/app/(app)/admin/user/[id]/page.tsx b/apps/hub/app/(app)/admin/user/[id]/page.tsx index 88007af3..5d2a4aea 100644 --- a/apps/hub/app/(app)/admin/user/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/page.tsx @@ -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 }>
+
+ +
diff --git a/apps/hub/app/(app)/admin/user/action.ts b/apps/hub/app/(app)/admin/user/action.ts index 9f2b1323..0a7590e9 100644 --- a/apps/hub/app/(app)/admin/user/action.ts +++ b/apps/hub/app/(app)/admin/user/action.ts @@ -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); diff --git a/apps/hub/app/_components/Nav.tsx b/apps/hub/app/_components/Nav.tsx index d9a3c1cf..0a004445 100644 --- a/apps/hub/app/_components/Nav.tsx +++ b/apps/hub/app/_components/Nav.tsx @@ -80,6 +80,11 @@ export const VerticalNav = async () => { Reports )} + {session.user.permissions.includes("ADMIN_USER") && ( +
  • + Audit-Log +
  • + )} diff --git a/package.json b/package.json index 95163b5d..110a8e5f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "node": ">=18", "pnpm": ">=10" }, - "packageManager": "pnpm@10.11.1", + "packageManager": "pnpm@10.12.1", "workspaces": [ "apps/*", "packages/*" diff --git a/packages/database/prisma/schema/penalty.prisma b/packages/database/prisma/schema/penalty.prisma index 78af9442..6643e310 100644 --- a/packages/database/prisma/schema/penalty.prisma +++ b/packages/database/prisma/schema/penalty.prisma @@ -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]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b3ff9f4..16ed81e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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