Added time Ban and penalty
This commit is contained in:
@@ -117,6 +117,8 @@ router.patch("/:id", async (req, res) => {
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const bann = req.body?.bann as boolean;
|
||||
const reason = req.body?.reason as string;
|
||||
const until = req.body?.until as Date | null;
|
||||
|
||||
const requiredPermission = bann ? "ADMIN_USER" : "ADMIN_KICK";
|
||||
|
||||
@@ -146,14 +148,16 @@ router.delete("/:id", async (req, res) => {
|
||||
|
||||
io.to(`user:${aircraft.userId}`).emit("notification", {
|
||||
type: "admin-message",
|
||||
message: "Verbindung durch einen Administrator getrennt",
|
||||
message: `Du wurdest von ${getPublicUser(req.user).publicId} ${until ? `bis zum ${new Date(until).toLocaleString()} ` : ""} ${
|
||||
status === "ban" ? "gebannt" : "gekickt"
|
||||
}`,
|
||||
status,
|
||||
data: { admin: getPublicUser(req.user) },
|
||||
data: { admin: getPublicUser(req.user), reason },
|
||||
} as AdminMessage);
|
||||
|
||||
io.in(`user:${aircraft.userId}`).disconnectSockets(true);
|
||||
|
||||
if (bann) {
|
||||
if (bann && !until) {
|
||||
await prisma.user.update({
|
||||
where: { id: aircraft.userId },
|
||||
data: {
|
||||
@@ -163,6 +167,15 @@ router.delete("/:id", async (req, res) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.penalty.create({
|
||||
data: {
|
||||
userId: aircraft.userId,
|
||||
type: bann ? (until ? "TIME_BAN" : "BAN") : "KICK",
|
||||
until: until ? new Date(until) : null,
|
||||
reason: reason,
|
||||
createdUserId: req.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
|
||||
@@ -34,6 +34,8 @@ import { Request, Response } from "express";
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const bann = req.body?.bann as boolean;
|
||||
const reason = req.body?.reason as string;
|
||||
const until = req.body?.until as Date | null;
|
||||
|
||||
const requiredPermission = bann ? "ADMIN_USER" : "ADMIN_KICK";
|
||||
|
||||
@@ -63,14 +65,16 @@ router.delete("/:id", async (req, res) => {
|
||||
|
||||
io.to(`user:${dispatcher.userId}`).emit("notification", {
|
||||
type: "admin-message",
|
||||
message: "Verbindung durch einen Administrator getrennt",
|
||||
message: `Du wurdest von ${getPublicUser(req.user).publicId} ${until ? `bis zum ${new Date(until).toLocaleString()} ` : ""} ${
|
||||
status === "ban" ? "gebannt" : "gekickt"
|
||||
}`,
|
||||
status,
|
||||
data: { admin: getPublicUser(req.user) },
|
||||
data: { admin: getPublicUser(req.user), reason },
|
||||
} as AdminMessage);
|
||||
|
||||
io.in(`user:${dispatcher.userId}`).disconnectSockets(true);
|
||||
|
||||
if (bann) {
|
||||
if (bann && !until) {
|
||||
await prisma.user.update({
|
||||
where: { id: dispatcher.userId },
|
||||
data: {
|
||||
@@ -80,6 +84,15 @@ router.delete("/:id", async (req, res) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.penalty.create({
|
||||
data: {
|
||||
userId: dispatcher.userId,
|
||||
type: bann ? (until ? "TIME_BAN" : "BAN") : "KICK",
|
||||
until: until ? new Date(until) : null,
|
||||
reason: reason,
|
||||
createdUserId: req.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const Error = ({ statusCode, title }: { statusCode: number; title: string }) => {
|
||||
export const Error = ({
|
||||
statusCode,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
statusCode: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center h-full">
|
||||
<div className="rounded-2xl bg-base-300 p-8 text-center max-w-md w-full">
|
||||
<h1 className="text-6xl font-bold text-red-500">{statusCode}</h1>
|
||||
<p className="text-xl font-semibold mt-4">Oh nein! Ein Fehler ist aufgetreten.</p>
|
||||
<p className="text-gray-600 mt-2">{title || "Ein unerwarteter Fehler ist aufgetreten."}</p>
|
||||
<p className="text-xl font-semibold mt-4">
|
||||
{title ? title : "Oh nein! Ein Fehler ist aufgetreten."}
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{description || "Ein unerwarteter Fehler ist aufgetreten."}
|
||||
</p>
|
||||
<button onClick={() => window.location.reload()} className="btn btn-dash my-2">
|
||||
Refresh Page
|
||||
</button>
|
||||
|
||||
@@ -19,10 +19,9 @@ export const AdminMessageToast = ({ event, t }: { event: AdminMessage; t: Toast
|
||||
event.status == "kick" && "text-yellow-500 ",
|
||||
)}
|
||||
>
|
||||
Du wurdes durch den Admin {event.data?.admin.publicId}{" "}
|
||||
{event.status == "ban" ? "gebannt" : "gekickt"}!
|
||||
{event.message}
|
||||
</h1>
|
||||
<p>{event.message}</p>
|
||||
<p>{event.data?.reason}</p>
|
||||
</div>
|
||||
<div className="ml-11">
|
||||
<button className="btn" onClick={handleClick}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { PublicUser } from "@repo/db";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { cn } from "_helpers/cn";
|
||||
import { getConnectedAircraftsAPI, kickAircraftAPI } from "_querys/aircrafts";
|
||||
import { getConnectedDispatcherAPI, kickDispatcherAPI } from "_querys/dispatcher";
|
||||
import { getLivekitRooms, kickLivekitParticipant } from "_querys/livekit";
|
||||
@@ -18,9 +19,115 @@ import {
|
||||
UserCheck,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { ReactNode, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
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 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 tooltip-warning",
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default function AdminPanel() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: pilots } = useQuery({
|
||||
@@ -45,9 +152,6 @@ export default function AdminPanel() {
|
||||
queryClient.invalidateQueries({ queryKey: ["livekit-rooms"] });
|
||||
},
|
||||
});
|
||||
const editUSerMutation = useMutation({
|
||||
mutationFn: editUserAPI,
|
||||
});
|
||||
const kickPilotMutation = useMutation({
|
||||
mutationFn: kickAircraftAPI,
|
||||
onSuccess: () => {
|
||||
@@ -153,22 +257,23 @@ export default function AdminPanel() {
|
||||
)}
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
onClick={() => kickPilotMutation.mutate({ id: p.id })}
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
onClick={() => {
|
||||
kickPilotMutation.mutate({ id: p.id, bann: true });
|
||||
}}
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<PenaltyDropdown
|
||||
btnClassName="btn-warning"
|
||||
btnTip="Kick"
|
||||
Icon={<RedoDot size={15} />}
|
||||
onClick={({ reason }) =>
|
||||
kickPilotMutation.mutate({ id: p.id, reason })
|
||||
}
|
||||
/>
|
||||
<PenaltyDropdown
|
||||
btnClassName="btn-error"
|
||||
btnTip="Ban"
|
||||
showDatePicker
|
||||
Icon={<LockKeyhole size={15} />}
|
||||
onClick={({ reason, until }) =>
|
||||
kickPilotMutation.mutate({ id: p.id, reason, bann: true, until })
|
||||
}
|
||||
/>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
|
||||
target="_blank"
|
||||
@@ -205,22 +310,23 @@ export default function AdminPanel() {
|
||||
)}
|
||||
</td>
|
||||
<td className="flex gap-2">
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||
data-tip="Kick"
|
||||
onClick={() => kickDispatchMutation.mutate({ id: d.id })}
|
||||
>
|
||||
<RedoDot size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||
data-tip="Ban"
|
||||
onClick={() => {
|
||||
kickDispatchMutation.mutate({ id: d.id, bann: true });
|
||||
}}
|
||||
>
|
||||
<LockKeyhole size={15} />
|
||||
</button>
|
||||
<PenaltyDropdown
|
||||
btnClassName="btn-warning"
|
||||
btnTip="Kick"
|
||||
Icon={<RedoDot size={15} />}
|
||||
onClick={({ reason }) =>
|
||||
kickDispatchMutation.mutate({ id: d.id, reason })
|
||||
}
|
||||
/>
|
||||
<PenaltyDropdown
|
||||
btnClassName="btn-error"
|
||||
btnTip="Ban"
|
||||
showDatePicker
|
||||
Icon={<LockKeyhole size={15} />}
|
||||
onClick={({ reason, until }) =>
|
||||
kickDispatchMutation.mutate({ id: d.id, reason, bann: true, until })
|
||||
}
|
||||
/>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${d.userId}`}
|
||||
target="_blank"
|
||||
|
||||
@@ -29,9 +29,19 @@ export const getConnectedAircraftPositionLogAPI = async ({ id }: { id: number })
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const kickAircraftAPI = async ({ id, bann }: { id: number; bann?: boolean }) => {
|
||||
export const kickAircraftAPI = async ({
|
||||
id,
|
||||
bann,
|
||||
reason,
|
||||
until = null,
|
||||
}: {
|
||||
id: number;
|
||||
bann?: boolean;
|
||||
reason: string;
|
||||
until?: Date | null;
|
||||
}) => {
|
||||
const res = await serverApi.delete(`/aircrafts/${id}`, {
|
||||
data: { bann },
|
||||
data: { bann, reason, until },
|
||||
});
|
||||
console.log(res.status);
|
||||
if (res.status != 204) {
|
||||
|
||||
@@ -25,9 +25,19 @@ export const getConnectedDispatcherAPI = async (filter?: Prisma.ConnectedDispatc
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const kickDispatcherAPI = async ({ id, bann }: { id: number; bann?: boolean }) => {
|
||||
export const kickDispatcherAPI = async ({
|
||||
id,
|
||||
bann,
|
||||
reason,
|
||||
until = null,
|
||||
}: {
|
||||
id: number;
|
||||
bann?: boolean;
|
||||
reason: string;
|
||||
until?: Date | null;
|
||||
}) => {
|
||||
const res = await serverApi.delete(`/dispatcher/${id}`, {
|
||||
data: { bann },
|
||||
data: { bann, reason, until },
|
||||
});
|
||||
console.log(res.status);
|
||||
if (res.status != 204) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import Navbar from "./_components/navbar/Navbar";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
||||
import { Error } from "_components/Error";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "VAR: Disponent",
|
||||
@@ -15,6 +16,15 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession();
|
||||
const openPenaltys = await prisma.penalty.findMany({
|
||||
where: {
|
||||
userId: session?.user.id,
|
||||
until: {
|
||||
gte: new Date(),
|
||||
},
|
||||
type: "TIME_BAN",
|
||||
},
|
||||
});
|
||||
|
||||
if (!session || !session.user) {
|
||||
redirect("/login");
|
||||
@@ -25,6 +35,17 @@ export default async function RootLayout({
|
||||
|
||||
if (!session.user.permissions.includes("DISPO"))
|
||||
return <Error title="Zugriff verweigert" statusCode={403} />;
|
||||
|
||||
if (openPenaltys[0]) {
|
||||
return (
|
||||
<Error
|
||||
title="Du hast eine aktive Strafe"
|
||||
statusCode={403}
|
||||
description={`Du bist bis zum ${new Date(openPenaltys[0].until!).toLocaleString()} gesperrt.`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
@@ -3,6 +3,7 @@ import Navbar from "./_components/navbar/Navbar";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
||||
import { Error } from "_components/Error";
|
||||
import { prisma } from "@repo/db";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "VAR: Pilot",
|
||||
@@ -15,6 +16,15 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession();
|
||||
const openPenaltys = await prisma.penalty.findMany({
|
||||
where: {
|
||||
userId: session?.user.id,
|
||||
until: {
|
||||
gte: new Date(),
|
||||
},
|
||||
type: "TIME_BAN",
|
||||
},
|
||||
});
|
||||
|
||||
if (!session || !session.user.firstname) {
|
||||
redirect("/login");
|
||||
@@ -25,6 +35,17 @@ export default async function RootLayout({
|
||||
|
||||
if (!session.user.permissions.includes("PILOT"))
|
||||
return <Error title="Zugriff verweigert" statusCode={403} />;
|
||||
|
||||
if (openPenaltys[0]) {
|
||||
return (
|
||||
<Error
|
||||
title="Du hast eine aktive Strafe"
|
||||
statusCode={403}
|
||||
description={`Du bist bis zum ${new Date(openPenaltys[0].until!).toLocaleString()} gesperrt.`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
36
apps/hub/app/(app)/_components/Penalty.tsx
Normal file
36
apps/hub/app/(app)/_components/Penalty.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { prisma } from "@repo/db";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
export const Penalty = async () => {
|
||||
const session = await getServerSession();
|
||||
const openPenaltys = await prisma.penalty.findMany({
|
||||
where: {
|
||||
userId: session?.user.id,
|
||||
until: {
|
||||
gte: new Date(),
|
||||
},
|
||||
type: "TIME_BAN",
|
||||
},
|
||||
});
|
||||
console.log("Open Penaltys:", openPenaltys);
|
||||
if (!openPenaltys[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-3xl text-center text-error">
|
||||
<TriangleAlert />
|
||||
Aktive Strafe
|
||||
</h2>
|
||||
<p>Du hast eine aktive Strafe, die dich daran hindert, an Flügen teilzunehmen.</p>
|
||||
<p>Strafe: {openPenaltys[0].reason}</p>
|
||||
{openPenaltys[0].until && (
|
||||
<p>Bis: {new Date(openPenaltys[0].until).toLocaleDateString()}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import Events from "./_components/FeaturedEvents";
|
||||
import { Stats } from "./_components/Stats";
|
||||
import { Badges } from "./_components/Badges";
|
||||
import { RecentFlights } from "(app)/_components/RecentFlights";
|
||||
import { Penalty } from "(app)/_components/Penalty";
|
||||
|
||||
export default async function Home({
|
||||
searchParams,
|
||||
@@ -12,6 +13,7 @@ export default async function Home({
|
||||
const view = stats || "pilot";
|
||||
return (
|
||||
<div>
|
||||
<Penalty />
|
||||
<Stats stats={view} />
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface AdminMessage {
|
||||
message: string;
|
||||
data?: {
|
||||
admin: PublicUser;
|
||||
reason: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
23
packages/database/prisma/schema/penalty.prisma
Normal file
23
packages/database/prisma/schema/penalty.prisma
Normal file
@@ -0,0 +1,23 @@
|
||||
model Penalty {
|
||||
id Int @id @default(autoincrement())
|
||||
userId String
|
||||
createdUserId String
|
||||
|
||||
type PenaltyType
|
||||
reason String
|
||||
until DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// relations:
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
CreatedUser User @relation("CreatedPenalties", fields: [createdUserId], references: [id])
|
||||
Report Report[]
|
||||
}
|
||||
|
||||
enum PenaltyType {
|
||||
KICK
|
||||
TIME_BAN
|
||||
BAN
|
||||
}
|
||||
@@ -8,8 +8,10 @@ model Report {
|
||||
reviewerComment String?
|
||||
reviewed Boolean @default(false)
|
||||
reviewerUserId String?
|
||||
penaltyId Int?
|
||||
|
||||
// relations:
|
||||
Penalty Penalty? @relation(fields: [penaltyId], references: [id])
|
||||
Sender User @relation("SentReports", fields: [senderUserId], references: [id])
|
||||
Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id])
|
||||
Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
|
||||
|
||||
@@ -66,6 +66,8 @@ model User {
|
||||
ConnectedDispatcher ConnectedDispatcher[]
|
||||
ConnectedAircraft ConnectedAircraft[]
|
||||
PositionLog PositionLog[]
|
||||
Penalty Penalty[]
|
||||
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user