diff --git a/apps/dispatch-server/routes/aircraft.ts b/apps/dispatch-server/routes/aircraft.ts index 725d66d6..6b801e8d 100644 --- a/apps/dispatch-server/routes/aircraft.ts +++ b/apps/dispatch-server/routes/aircraft.ts @@ -136,7 +136,7 @@ router.delete("/:id", async (req, res) => { const aircraft = await prisma.connectedAircraft.update({ where: { id: Number(id) }, data: { logoutTime: new Date() }, - include: bann ? { User: true } : undefined, + include: { User: true }, }); if (!aircraft) { @@ -149,7 +149,7 @@ router.delete("/:id", async (req, res) => { io.to(`user:${aircraft.userId}`).emit("notification", { type: "admin-message", message: `Du wurdest von ${getPublicUser(req.user).publicId} ${until ? `bis zum ${new Date(until).toLocaleString()} ` : ""} ${ - status === "ban" ? "gebannt" : "gekickt" + status === "ban" ? "gekickt, deine Rechte wurden entzogen!" : "gekickt" }`, status, data: { admin: getPublicUser(req.user), reason }, @@ -162,7 +162,7 @@ router.delete("/:id", async (req, res) => { where: { id: aircraft.userId }, data: { permissions: { - set: req.user.permissions.filter((p) => p !== "PILOT"), + set: aircraft.User.permissions.filter((p) => p !== "PILOT"), }, }, }); @@ -170,7 +170,7 @@ router.delete("/:id", async (req, res) => { await prisma.penalty.create({ data: { userId: aircraft.userId, - type: bann ? (until ? "TIME_BAN" : "BAN") : "KICK", + type: bann ? (until ? "TIME_BAN" : "PERMISSIONS_REVOCED") : "KICK", until: until ? new Date(until) : new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 50), reason: reason, createdUserId: req.user.id, diff --git a/apps/dispatch-server/routes/dispatcher.ts b/apps/dispatch-server/routes/dispatcher.ts index a49942a3..31dd6460 100644 --- a/apps/dispatch-server/routes/dispatcher.ts +++ b/apps/dispatch-server/routes/dispatcher.ts @@ -29,8 +29,6 @@ router.patch("/:id", async (req, res) => { res.json(newDispatcher); }); -import { Request, Response } from "express"; - router.delete("/:id", async (req, res) => { const { id } = req.params; const bann = req.body?.bann as boolean; @@ -53,7 +51,7 @@ router.delete("/:id", async (req, res) => { const dispatcher = await prisma.connectedDispatcher.update({ where: { id: Number(id) }, data: { logoutTime: new Date() }, - include: bann ? { user: true } : undefined, + include: { user: true }, }); if (!dispatcher) { @@ -66,7 +64,7 @@ router.delete("/:id", async (req, res) => { io.to(`user:${dispatcher.userId}`).emit("notification", { type: "admin-message", message: `Du wurdest von ${getPublicUser(req.user).publicId} ${until ? `bis zum ${new Date(until).toLocaleString()} ` : ""} ${ - status === "ban" ? "gebannt" : "gekickt" + status === "ban" ? "gekickt, deine Rechte wurden entzogen" : "gekickt" }`, status, data: { admin: getPublicUser(req.user), reason }, @@ -79,7 +77,7 @@ router.delete("/:id", async (req, res) => { where: { id: dispatcher.userId }, data: { permissions: { - set: req.user.permissions.filter((p) => p !== "DISPO"), + set: dispatcher.user.permissions.filter((p) => p !== "DISPO"), }, }, }); @@ -87,7 +85,7 @@ router.delete("/:id", async (req, res) => { await prisma.penalty.create({ data: { userId: dispatcher.userId, - type: bann ? (until ? "TIME_BAN" : "BAN") : "KICK", + type: bann ? (until ? "TIME_BAN" : "PERMISSIONS_REVOCED") : "KICK", until: until ? new Date(until) : new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 50), reason: reason, createdUserId: req.user.id, diff --git a/apps/dispatch-server/socket-events/connect-dispatch.ts b/apps/dispatch-server/socket-events/connect-dispatch.ts index c6494c22..7dfb6a5c 100644 --- a/apps/dispatch-server/socket-events/connect-dispatch.ts +++ b/apps/dispatch-server/socket-events/connect-dispatch.ts @@ -10,7 +10,14 @@ export const handleConnectDispatch = const user: User = socket.data.user; // User ID aus dem JWT-Token if (!user) return Error("User not found"); - console.log("Disponent connected:", user.publicId); + + if (!user.permissions.includes("DISPO")) { + socket.emit("connect-message", { + message: "Fehlende Berechtigung", + }); + socket.disconnect(); + return; + } if (!user.permissions?.includes("DISPO")) { socket.emit("error", "You do not have permission to connect to the dispatch server."); diff --git a/apps/dispatch-server/socket-events/connect-pilot.ts b/apps/dispatch-server/socket-events/connect-pilot.ts index 285fc28b..6e969142 100644 --- a/apps/dispatch-server/socket-events/connect-pilot.ts +++ b/apps/dispatch-server/socket-events/connect-pilot.ts @@ -17,6 +17,14 @@ export const handleConnectPilot = }, }); + if (!user.permissions.includes("PILOT")) { + socket.emit("connect-message", { + message: "Fehlende Berechtigung", + }); + socket.disconnect(); + return; + } + if (!user) return Error("User not found"); const existingConnection = await prisma.connectedAircraft.findFirst({ diff --git a/apps/dispatch/app/_components/navbar/AdminPanel.tsx b/apps/dispatch/app/_components/navbar/AdminPanel.tsx index acf566b7..e8f888d9 100644 --- a/apps/dispatch/app/_components/navbar/AdminPanel.tsx +++ b/apps/dispatch/app/_components/navbar/AdminPanel.tsx @@ -13,7 +13,6 @@ import { Plane, RedoDot, Shield, - ShieldAlert, Speaker, User, UserCheck, @@ -266,7 +265,7 @@ export default function AdminPanel() { } /> } @@ -319,7 +318,7 @@ export default function AdminPanel() { } /> } diff --git a/apps/dispatch/app/_store/dispatch/connectionStore.ts b/apps/dispatch/app/_store/dispatch/connectionStore.ts index 356b3060..cfe40b6c 100644 --- a/apps/dispatch/app/_store/dispatch/connectionStore.ts +++ b/apps/dispatch/app/_store/dispatch/connectionStore.ts @@ -56,8 +56,14 @@ dispatchSocket.on("connect_error", (err) => { }); }); +dispatchSocket.on("connect-message", (data) => { + useDispatchConnectionStore.setState({ + message: data.message, + }); +}); + dispatchSocket.on("disconnect", () => { - useDispatchConnectionStore.setState({ status: "disconnected", message: "" }); + useDispatchConnectionStore.setState({ status: "disconnected" }); useAudioStore.getState().disconnect(); }); diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index 3d40e2c5..4067624f 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -81,8 +81,14 @@ pilotSocket.on("connect_error", (err) => { }); }); +pilotSocket.on("connect-message", (data) => { + usePilotConnectionStore.setState({ + message: data.message, + }); +}); + pilotSocket.on("disconnect", () => { - usePilotConnectionStore.setState({ status: "disconnected", message: "" }); + usePilotConnectionStore.setState({ status: "disconnected" }); useAudioStore.getState().disconnect(); }); diff --git a/apps/dispatch/app/dispatch/layout.tsx b/apps/dispatch/app/dispatch/layout.tsx index 30f3cb62..4d5f8b46 100644 --- a/apps/dispatch/app/dispatch/layout.tsx +++ b/apps/dispatch/app/dispatch/layout.tsx @@ -22,6 +22,8 @@ export default async function RootLayout({ until: { gte: new Date(), }, + suspended: false, + type: { in: ["TIME_BAN", "BAN"] }, }, }); diff --git a/apps/dispatch/app/pilot/layout.tsx b/apps/dispatch/app/pilot/layout.tsx index c28893eb..5e60497e 100644 --- a/apps/dispatch/app/pilot/layout.tsx +++ b/apps/dispatch/app/pilot/layout.tsx @@ -22,6 +22,7 @@ export default async function RootLayout({ until: { gte: new Date(), }, + suspended: false, type: { in: ["TIME_BAN", "BAN"] }, }, }); diff --git a/apps/hub/app/(app)/admin/penalty/actions.ts b/apps/hub/app/(app)/admin/penalty/actions.ts index 98c85d82..0df62807 100644 --- a/apps/hub/app/(app)/admin/penalty/actions.ts +++ b/apps/hub/app/(app)/admin/penalty/actions.ts @@ -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.PenaltyUncheckedCreateInput, +) => { return await prisma.penalty.create({ data, }); diff --git a/apps/hub/app/(app)/admin/penalty/page.tsx b/apps/hub/app/(app)/admin/penalty/page.tsx index c67b5edb..6ec17182 100644 --- a/apps/hub/app/(app)/admin/penalty/page.tsx +++ b/apps/hub/app/(app)/admin/penalty/page.tsx @@ -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[] = [ { @@ -39,10 +40,17 @@ export const penaltyColumns: ColumnDef[] = [ ); } + + case "PERMISSIONS_REVOCED": + return ( +
+ Rechte entzogen {row.original.suspended && "(ausgesetzt)"} +
+ ); case "BAN": return (
- Bann {row.original.suspended && "(ausgesetzt)"} + Bann {row.original.suspended && "(ausgesetzt)"}
); } diff --git a/apps/hub/app/(app)/admin/user/[id]/_components/AddPenaltyDropdown.tsx b/apps/hub/app/(app)/admin/user/[id]/_components/AddPenaltyDropdown.tsx new file mode 100644 index 00000000..6e185081 --- /dev/null +++ b/apps/hub/app/(app)/admin/user/[id]/_components/AddPenaltyDropdown.tsx @@ -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("default"); + return ( +
+ {Icon} +
+ setReason(e.target.value)} + type="text" + className="input min-w-[250px]" + placeholder="Begründung" + /> + {showDatePicker && ( + + )} + +
+
+ ); +}; 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 73541aab..0b8eb526 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx @@ -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(null); return (
-

- Nutzer Strafen +

+ + Audit-log + +
+ } + 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} + /> + } + 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" + /> +

Passwort zurücksetzen - {!user.isBanned && ( - - )} {user.isBanned && (