From 3c620b9b670b4b6b80fee8b35d02393edd8205f0 Mon Sep 17 00:00:00 2001
From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com>
Date: Wed, 4 Jun 2025 17:27:58 -0700
Subject: [PATCH] Completed Admin Users form
---
.../modules/socketJWTmiddleware.ts | 3 +-
apps/dispatch-server/package.json | 1 +
apps/dispatch-server/routes/aircraft.ts | 61 ++-
apps/dispatch-server/routes/dispatcher.ts | 62 ++-
.../app/_components/QueryProvider.tsx | 15 +-
.../_components/customToasts/AdminMessage.tsx | 34 ++
.../customToasts/HPGnotification.tsx | 4 +-
.../{ => customToasts}/StationStatusToast.tsx | 0
.../app/_components/navbar/AdminPanel.tsx | 437 ++++++++++--------
.../app/_components/navbar/Settings.tsx | 6 +-
.../app/_helpers/LivekitRoomManager.ts | 9 +
apps/dispatch/app/_querys/aircrafts.ts | 11 +
apps/dispatch/app/_querys/connected-user.ts | 11 +
apps/dispatch/app/_querys/livekit.ts | 28 ++
apps/dispatch/app/_querys/user.ts | 2 +-
.../app/api/livekit-participant/route.ts | 85 ++++
apps/dispatch/app/api/livekit-token/route.ts | 5 +-
.../dispatch/_components/navbar/Navbar.tsx | 18 +-
package.json | 1 +
packages/database/prisma/json/SocketEvents.ts | 16 +-
packages/database/prisma/schema/user.prisma | 1 +
pnpm-lock.yaml | 17 +-
22 files changed, 592 insertions(+), 235 deletions(-)
create mode 100644 apps/dispatch/app/_components/customToasts/AdminMessage.tsx
rename apps/dispatch/app/_components/{ => customToasts}/StationStatusToast.tsx (100%)
create mode 100644 apps/dispatch/app/_helpers/LivekitRoomManager.ts
create mode 100644 apps/dispatch/app/_querys/livekit.ts
create mode 100644 apps/dispatch/app/api/livekit-participant/route.ts
diff --git a/apps/dispatch-server/modules/socketJWTmiddleware.ts b/apps/dispatch-server/modules/socketJWTmiddleware.ts
index 6c556a58..252f55fe 100644
--- a/apps/dispatch-server/modules/socketJWTmiddleware.ts
+++ b/apps/dispatch-server/modules/socketJWTmiddleware.ts
@@ -1,6 +1,7 @@
import { ExtendedError, Server, Socket } from "socket.io";
import { prisma } from "@repo/db";
-if (!process.env.DISPATCH_APP_TOKEN) throw new Error("DISPATCH_APP_TOKEN is not defined");
+import jwt from "jsonwebtoken";
+/* if (!process.env.DISPATCH_APP_TOKEN) throw new Error("DISPATCH_APP_TOKEN is not defined"); */
export const jwtMiddleware = async (socket: Socket, next: (err?: ExtendedError) => void) => {
try {
diff --git a/apps/dispatch-server/package.json b/apps/dispatch-server/package.json
index c659b110..35e57464 100644
--- a/apps/dispatch-server/package.json
+++ b/apps/dispatch-server/package.json
@@ -24,6 +24,7 @@
"@react-email/components": "^0.0.41",
"@redis/json": "^5.1.1",
"@socket.io/redis-adapter": "^8.3.0",
+ "@types/jsonwebtoken": "^9.0.9",
"axios": "^1.9.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
diff --git a/apps/dispatch-server/routes/aircraft.ts b/apps/dispatch-server/routes/aircraft.ts
index 576d3e5f..7c1522b2 100644
--- a/apps/dispatch-server/routes/aircraft.ts
+++ b/apps/dispatch-server/routes/aircraft.ts
@@ -1,4 +1,11 @@
-import { ConnectedAircraft, getPublicUser, MissionLog, Prisma, prisma } from "@repo/db";
+import {
+ AdminMessage,
+ ConnectedAircraft,
+ getPublicUser,
+ MissionLog,
+ Prisma,
+ prisma,
+} from "@repo/db";
import { Router } from "express";
import { io } from "../index";
@@ -95,6 +102,7 @@ router.patch("/:id", async (req, res) => {
// When change is only the estimated logout time, we don't need to emit an event
if (Object.keys(aircraftUpdate).length === 1 && aircraftUpdate.esimatedLogoutTime) return;
io.to("dispatchers").emit("update-connectedAircraft", updatedConnectedAircraft);
+
io.to(`user:${updatedConnectedAircraft.userId}`).emit(
"aircraft-update",
updatedConnectedAircraft,
@@ -105,18 +113,61 @@ router.patch("/:id", async (req, res) => {
}
});
-// Delete a connectedAircraft by ID
+// Kick a connectedAircraft by ID
router.delete("/:id", async (req, res) => {
const { id } = req.params;
+ const bann = req.body?.bann as boolean;
+
+ const requiredPermission = bann ? "ADMIN_USER" : "ADMIN_KICK";
+
+ if (!req.user) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ if (!req.user.permissions.includes(requiredPermission)) {
+ res.status(403).json({ error: "Forbidden" });
+ return;
+ }
+
try {
- await prisma.connectedAircraft.delete({
+ const aircraft = await prisma.connectedAircraft.update({
where: { id: Number(id) },
+ data: { logoutTime: new Date() },
+ include: bann ? { User: true } : undefined,
});
- io.to("dispatchers").emit("delete-connectedAircraft", id);
+
+ if (!aircraft) {
+ res.status(404).json({ error: "ConnectedAircraft not found" });
+ return;
+ }
+
+ const status = bann ? "ban" : "kick";
+
+ io.to(`user:${aircraft.userId}`).emit("notification", {
+ type: "admin-message",
+ message: "Verbindung durch einen Administrator getrennt",
+ status,
+ data: { admin: getPublicUser(req.user) },
+ } as AdminMessage);
+
+ io.in(`user:${aircraft.userId}`).disconnectSockets(true);
+
+ if (bann) {
+ await prisma.user.update({
+ where: { id: aircraft.userId },
+ data: {
+ permissions: {
+ set: req.user.permissions.filter((p) => p !== "PILOT"),
+ },
+ },
+ });
+ }
+
res.status(204).send();
} catch (error) {
console.error(error);
- res.status(500).json({ error: "Failed to delete connectedAircraft" });
+ res.status(500).json({ error: "Failed to disconnect pilot" });
}
});
diff --git a/apps/dispatch-server/routes/dispatcher.ts b/apps/dispatch-server/routes/dispatcher.ts
index a3c79915..61f85e29 100644
--- a/apps/dispatch-server/routes/dispatcher.ts
+++ b/apps/dispatch-server/routes/dispatcher.ts
@@ -1,5 +1,6 @@
-import { Prisma, prisma } from "@repo/db";
+import { AdminMessage, getPublicUser, Prisma, prisma } from "@repo/db";
import { Router } from "express";
+import { io } from "index";
import { pubClient } from "modules/redis";
const router: Router = Router();
@@ -28,4 +29,63 @@ 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;
+
+ const requiredPermission = bann ? "ADMIN_USER" : "ADMIN_KICK";
+
+ if (!req.user) {
+ res.status(401).json({ error: "Unauthorized" });
+ return;
+ }
+
+ if (!req.user.permissions.includes(requiredPermission)) {
+ res.status(403).json({ error: "Forbidden" });
+ return;
+ }
+
+ try {
+ const dispatcher = await prisma.connectedDispatcher.update({
+ where: { id: Number(id) },
+ data: { logoutTime: new Date() },
+ include: bann ? { user: true } : undefined,
+ });
+
+ if (!dispatcher) {
+ res.status(404).json({ error: "ConnectedDispatcher not found" });
+ return;
+ }
+
+ const status = bann ? "ban" : "kick";
+
+ io.to(`user:${dispatcher.userId}`).emit("notification", {
+ type: "admin-message",
+ message: "Verbindung durch einen Administrator getrennt",
+ status,
+ data: { admin: getPublicUser(req.user) },
+ } as AdminMessage);
+
+ io.in(`user:${dispatcher.userId}`).disconnectSockets(true);
+
+ if (bann) {
+ await prisma.user.update({
+ where: { id: dispatcher.userId },
+ data: {
+ permissions: {
+ set: req.user.permissions.filter((p) => p !== "DISPO"),
+ },
+ },
+ });
+ }
+
+ res.status(204).send();
+ } catch (error) {
+ console.error(error);
+ res.status(500).json({ error: "Failed to disconnect dispatcher" });
+ }
+});
+
export default router;
diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx
index d0ec5293..39eacfcb 100644
--- a/apps/dispatch/app/_components/QueryProvider.tsx
+++ b/apps/dispatch/app/_components/QueryProvider.tsx
@@ -8,6 +8,8 @@ import { dispatchSocket } from "dispatch/socket";
import { Mission, NotificationPayload } from "@repo/db";
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
import { useMapStore } from "_store/mapStore";
+import { AdminMessageToast } from "_components/customToasts/AdminMessage";
+import { pilotSocket } from "pilot/socket";
export function QueryProvider({ children }: { children: ReactNode }) {
const mapStore = useMapStore((s) => s);
@@ -39,6 +41,9 @@ export function QueryProvider({ children }: { children: ReactNode }) {
queryClient.invalidateQueries({
queryKey: ["aircrafts"],
});
+ queryClient.invalidateQueries({
+ queryKey: ["dispatchers"],
+ });
};
const invalidateConenctedAircrafts = () => {
@@ -58,13 +63,18 @@ export function QueryProvider({ children }: { children: ReactNode }) {
toast.custom(
(t) =>
{event.message}
+