Completed Admin Users form
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { ExtendedError, Server, Socket } from "socket.io";
|
import { ExtendedError, Server, Socket } from "socket.io";
|
||||||
import { prisma } from "@repo/db";
|
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) => {
|
export const jwtMiddleware = async (socket: Socket, next: (err?: ExtendedError) => void) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@react-email/components": "^0.0.41",
|
"@react-email/components": "^0.0.41",
|
||||||
"@redis/json": "^5.1.1",
|
"@redis/json": "^5.1.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -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 { Router } from "express";
|
||||||
import { io } from "../index";
|
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
|
// 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;
|
if (Object.keys(aircraftUpdate).length === 1 && aircraftUpdate.esimatedLogoutTime) return;
|
||||||
io.to("dispatchers").emit("update-connectedAircraft", updatedConnectedAircraft);
|
io.to("dispatchers").emit("update-connectedAircraft", updatedConnectedAircraft);
|
||||||
|
|
||||||
io.to(`user:${updatedConnectedAircraft.userId}`).emit(
|
io.to(`user:${updatedConnectedAircraft.userId}`).emit(
|
||||||
"aircraft-update",
|
"aircraft-update",
|
||||||
updatedConnectedAircraft,
|
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) => {
|
router.delete("/:id", async (req, res) => {
|
||||||
const { id } = req.params;
|
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 {
|
try {
|
||||||
await prisma.connectedAircraft.delete({
|
const aircraft = await prisma.connectedAircraft.update({
|
||||||
where: { id: Number(id) },
|
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();
|
res.status(204).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.status(500).json({ error: "Failed to delete connectedAircraft" });
|
res.status(500).json({ error: "Failed to disconnect pilot" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Prisma, prisma } from "@repo/db";
|
import { AdminMessage, getPublicUser, Prisma, prisma } from "@repo/db";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
import { io } from "index";
|
||||||
import { pubClient } from "modules/redis";
|
import { pubClient } from "modules/redis";
|
||||||
|
|
||||||
const router: Router = Router();
|
const router: Router = Router();
|
||||||
@@ -28,4 +29,63 @@ router.patch("/:id", async (req, res) => {
|
|||||||
res.json(newDispatcher);
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { dispatchSocket } from "dispatch/socket";
|
|||||||
import { Mission, NotificationPayload } from "@repo/db";
|
import { Mission, NotificationPayload } from "@repo/db";
|
||||||
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
import { HPGnotificationToast } from "_components/customToasts/HPGnotification";
|
||||||
import { useMapStore } from "_store/mapStore";
|
import { useMapStore } from "_store/mapStore";
|
||||||
|
import { AdminMessageToast } from "_components/customToasts/AdminMessage";
|
||||||
|
import { pilotSocket } from "pilot/socket";
|
||||||
|
|
||||||
export function QueryProvider({ children }: { children: ReactNode }) {
|
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||||
const mapStore = useMapStore((s) => s);
|
const mapStore = useMapStore((s) => s);
|
||||||
@@ -39,6 +41,9 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["aircrafts"],
|
queryKey: ["aircrafts"],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["dispatchers"],
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidateConenctedAircrafts = () => {
|
const invalidateConenctedAircrafts = () => {
|
||||||
@@ -58,13 +63,18 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
toast.custom(
|
toast.custom(
|
||||||
(t) => <HPGnotificationToast event={notification} mapStore={mapStore} t={t} />,
|
(t) => <HPGnotificationToast event={notification} mapStore={mapStore} t={t} />,
|
||||||
{
|
{
|
||||||
duration: 9999,
|
duration: 99999,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "admin-message":
|
||||||
|
toast.custom((t) => <AdminMessageToast event={notification} t={t} />, {
|
||||||
|
duration: 999999,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
toast(notification.message);
|
toast("unbekanntes Notification-Event");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -76,6 +86,7 @@ export function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
dispatchSocket.on("pilots-update", invalidateConnectedUsers);
|
||||||
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
dispatchSocket.on("update-connectedAircraft", invalidateConenctedAircrafts);
|
||||||
dispatchSocket.on("notification", handleNotification);
|
dispatchSocket.on("notification", handleNotification);
|
||||||
|
pilotSocket.on("notification", handleNotification);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dispatchSocket.off("update-mission", invalidateMission);
|
dispatchSocket.off("update-mission", invalidateMission);
|
||||||
|
|||||||
34
apps/dispatch/app/_components/customToasts/AdminMessage.tsx
Normal file
34
apps/dispatch/app/_components/customToasts/AdminMessage.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { AdminMessage } from "@repo/db";
|
||||||
|
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||||
|
import { cn } from "_helpers/cn";
|
||||||
|
import { TriangleAlert } from "lucide-react";
|
||||||
|
import toast, { Toast } from "react-hot-toast";
|
||||||
|
|
||||||
|
export const AdminMessageToast = ({ event, t }: { event: AdminMessage; t: Toast }) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
toast.dismiss(t.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNotification icon={<TriangleAlert />} className="flex flex-row">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"font-bold",
|
||||||
|
event.status == "ban" && "text-red-500 ",
|
||||||
|
event.status == "kick" && "text-yellow-500 ",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Du wurdes durch den Admin {event.data?.admin.publicId}{" "}
|
||||||
|
{event.status == "ban" ? "gebannt" : "gekickt"}!
|
||||||
|
</h1>
|
||||||
|
<p>{event.message}</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-11">
|
||||||
|
<button className="btn" onClick={handleClick}>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</BaseNotification>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NotificationPayload } from "@repo/db";
|
import { NotificationPayload, ValidationFailed, ValidationSuccess } from "@repo/db";
|
||||||
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
import { BaseNotification } from "_components/customToasts/BaseNotification";
|
||||||
import { MapStore, useMapStore } from "_store/mapStore";
|
import { MapStore, useMapStore } from "_store/mapStore";
|
||||||
import { Check, Cross } from "lucide-react";
|
import { Check, Cross } from "lucide-react";
|
||||||
@@ -9,7 +9,7 @@ export const HPGnotificationToast = ({
|
|||||||
t,
|
t,
|
||||||
mapStore,
|
mapStore,
|
||||||
}: {
|
}: {
|
||||||
event: NotificationPayload;
|
event: ValidationFailed | ValidationSuccess;
|
||||||
t: Toast;
|
t: Toast;
|
||||||
mapStore: MapStore;
|
mapStore: MapStore;
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1,21 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
import { PublicUser } from "@repo/db";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { getConnectedAircraftsAPI, kickAircraftAPI } from "_querys/aircrafts";
|
||||||
|
import { getConnectedDispatcherAPI, kickDispatcherAPI } from "_querys/connected-user";
|
||||||
|
import { getLivekitRooms, kickLivekitParticipant } from "_querys/livekit";
|
||||||
|
import { editUserAPI } from "_querys/user";
|
||||||
|
import { ParticipantInfo } from "livekit-server-sdk";
|
||||||
import {
|
import {
|
||||||
ArrowLeftRight,
|
|
||||||
Eye,
|
Eye,
|
||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
Plane,
|
Plane,
|
||||||
RedoDot,
|
RedoDot,
|
||||||
Shield,
|
Shield,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
|
Speaker,
|
||||||
User,
|
User,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export default function AdminPanel() {
|
export default function AdminPanel() {
|
||||||
const path = usePathname();
|
const queryClient = useQueryClient();
|
||||||
|
const { data: pilots } = useQuery({
|
||||||
|
queryKey: ["pilots"],
|
||||||
|
queryFn: () => getConnectedAircraftsAPI(),
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
const { data: dispatcher } = useQuery({
|
||||||
|
queryKey: ["dispatcher"],
|
||||||
|
|
||||||
|
queryFn: () => getConnectedDispatcherAPI(),
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
const { data: livekitRooms } = useQuery({
|
||||||
|
queryKey: ["connected-audio-users"],
|
||||||
|
queryFn: () => getLivekitRooms(),
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
const kickLivekitParticipantMutation = useMutation({
|
||||||
|
mutationFn: kickLivekitParticipant,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["connected-audio-users"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const editUSerMutation = useMutation({
|
||||||
|
mutationFn: editUserAPI,
|
||||||
|
});
|
||||||
|
const kickPilotMutation = useMutation({
|
||||||
|
mutationFn: kickAircraftAPI,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Pilot wurde erfolgreich gekickt");
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["connected-audio-users"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const kickDispatchMutation = useMutation({
|
||||||
|
mutationFn: kickDispatcherAPI,
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Disponent wurde erfolgreich gekickt");
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["dispatcher"],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["connected-audio-users"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const participants: { participant: ParticipantInfo; room: string }[] = [];
|
||||||
|
|
||||||
|
if (livekitRooms) {
|
||||||
|
livekitRooms?.forEach((room) => {
|
||||||
|
room.participants.forEach((participant) => {
|
||||||
|
participants.push({
|
||||||
|
participant,
|
||||||
|
room: room.room.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const livekitUserNotConnected = participants.filter((p) => {
|
||||||
|
const pilot = pilots?.find(
|
||||||
|
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
|
||||||
|
);
|
||||||
|
const fDispatcher = dispatcher?.find(
|
||||||
|
(d) => (d.publicUser as unknown as PublicUser).publicId === p.participant.identity,
|
||||||
|
);
|
||||||
|
return !pilot && !fDispatcher;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Livekit Rooms", livekitRooms);
|
||||||
|
|
||||||
const modalRef = useRef<HTMLDialogElement>(null);
|
const modalRef = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +110,7 @@ export default function AdminPanel() {
|
|||||||
>
|
>
|
||||||
<Shield size={18} /> Admin Panel
|
<Shield size={18} /> Admin Panel
|
||||||
</button>
|
</button>
|
||||||
<dialog ref={modalRef} className="modal">
|
<dialog ref={modalRef} className="modal min-w-[500px]">
|
||||||
<div className="modal-box w-11/12 max-w-7xl">
|
<div className="modal-box w-11/12 max-w-7xl">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||||
@@ -50,203 +131,171 @@ export default function AdminPanel() {
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Station</th>
|
<th>Station</th>
|
||||||
<th>Voice</th>
|
<th>Voice</th>
|
||||||
<th>Dispatch</th>
|
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
{pilots?.map((p) => {
|
||||||
<td>VAR0124</td>
|
const publicUser = p.publicUser as unknown as PublicUser;
|
||||||
<td>Max Mustermann</td>
|
const livekitParticipant = participants.find(
|
||||||
<td>Christoph 31</td>
|
(p) => p.participant.identity === publicUser.publicId,
|
||||||
<td className="text-error">
|
);
|
||||||
<span>Nicht verbunden</span>
|
return (
|
||||||
</td>
|
<tr key={p.id}>
|
||||||
<td className="text-success">
|
<td className="flex items-center gap-2">
|
||||||
<span>Verbunden</span>
|
<Plane /> {publicUser.publicId}
|
||||||
</td>
|
</td>
|
||||||
<td className="flex gap-2">
|
<td>{publicUser.fullName}</td>
|
||||||
<button
|
<td>{p.Station.bosCallsign}</td>
|
||||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
<td>
|
||||||
data-tip="Kick"
|
{!livekitParticipant ? (
|
||||||
>
|
<span className="text-error">Nicht verbunden</span>
|
||||||
<RedoDot size={15} />
|
) : (
|
||||||
</button>
|
<span className="text-success">{livekitParticipant.room}</span>
|
||||||
<button
|
)}
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
</td>
|
||||||
data-tip="Ban"
|
<td className="flex gap-2">
|
||||||
>
|
<button
|
||||||
<LockKeyhole size={15} />
|
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||||
</button>
|
data-tip="Kick"
|
||||||
<button
|
onClick={() => kickPilotMutation.mutate({ id: p.id })}
|
||||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
>
|
||||||
data-tip="Profil"
|
<RedoDot size={15} />
|
||||||
>
|
</button>
|
||||||
<User size={15} />
|
<button
|
||||||
</button>
|
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||||
</td>
|
data-tip="Ban"
|
||||||
</tr>
|
onClick={() => {
|
||||||
<tr>
|
kickPilotMutation.mutate({ id: p.id, bann: true });
|
||||||
<td>VAR0124</td>
|
}}
|
||||||
<td>Max Mustermann</td>
|
>
|
||||||
<td>Christoph 31</td>
|
<LockKeyhole size={15} />
|
||||||
<td className="text-error">
|
</button>
|
||||||
<span>Nicht verbunden</span>
|
<a
|
||||||
</td>
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.userId}`}
|
||||||
<td className="text-success">
|
target="_blank"
|
||||||
<span>Verbunden</span>
|
rel="noopener noreferrer"
|
||||||
</td>
|
>
|
||||||
<td className="flex gap-2">
|
<button
|
||||||
<button
|
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
data-tip="Profil"
|
||||||
data-tip="Kick"
|
>
|
||||||
>
|
<User size={15} />
|
||||||
<RedoDot size={15} />
|
</button>
|
||||||
</button>
|
</a>
|
||||||
<button
|
</td>
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
</tr>
|
||||||
data-tip="Ban"
|
);
|
||||||
>
|
})}
|
||||||
<LockKeyhole size={15} />
|
{dispatcher?.map((d) => {
|
||||||
</button>
|
const publicUser = d.publicUser as unknown as PublicUser;
|
||||||
<button
|
const livekitParticipant = participants.find(
|
||||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
(p) => p.participant.identity === publicUser.publicId,
|
||||||
data-tip="Profil"
|
);
|
||||||
>
|
return (
|
||||||
<User size={15} />
|
<tr key={d.id}>
|
||||||
</button>
|
<td className="flex items-center gap-2">
|
||||||
</td>
|
<Workflow /> {publicUser.publicId}
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
<td>{publicUser.fullName}</td>
|
||||||
<td>VAR0124</td>
|
<td>{d.zone}</td>
|
||||||
<td>Max Mustermann</td>
|
<td>
|
||||||
<td>Christoph 31</td>
|
{!livekitParticipant ? (
|
||||||
<td className="text-error">
|
<span className="text-error">Nicht verbunden</span>
|
||||||
<span>Nicht verbunden</span>
|
) : (
|
||||||
</td>
|
<span className="text-success">{livekitParticipant.room}</span>
|
||||||
<td className="text-success">
|
)}
|
||||||
<span>Verbunden</span>
|
</td>
|
||||||
</td>
|
<td className="flex gap-2">
|
||||||
<td className="flex gap-2">
|
<button
|
||||||
<button
|
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
data-tip="Kick"
|
||||||
data-tip="Kick"
|
onClick={() => kickDispatchMutation.mutate({ id: d.id })}
|
||||||
>
|
>
|
||||||
<RedoDot size={15} />
|
<RedoDot size={15} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||||
data-tip="Ban"
|
data-tip="Ban"
|
||||||
>
|
onClick={() => {
|
||||||
<LockKeyhole size={15} />
|
kickDispatchMutation.mutate({ id: d.id, bann: true });
|
||||||
</button>
|
}}
|
||||||
<button
|
>
|
||||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
<LockKeyhole size={15} />
|
||||||
data-tip="Profil"
|
</button>
|
||||||
>
|
<a
|
||||||
<User size={15} />
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${d.userId}`}
|
||||||
</button>
|
target="_blank"
|
||||||
</td>
|
rel="noopener noreferrer"
|
||||||
</tr>
|
>
|
||||||
<tr>
|
<button
|
||||||
<td>VAR0124</td>
|
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||||
<td>Max Mustermann</td>
|
data-tip="Profil"
|
||||||
<td>Christoph 31</td>
|
>
|
||||||
<td className="text-error">
|
<User size={15} />
|
||||||
<span>Nicht verbunden</span>
|
</button>
|
||||||
</td>
|
</a>
|
||||||
<td className="text-success">
|
</td>
|
||||||
<span>Verbunden</span>
|
</tr>
|
||||||
</td>
|
);
|
||||||
<td className="flex gap-2">
|
})}
|
||||||
<button
|
{livekitUserNotConnected.map((p) => {
|
||||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
const publicUser = JSON.parse(
|
||||||
data-tip="Kick"
|
p.participant.attributes.publicUser || "{}",
|
||||||
>
|
) as PublicUser;
|
||||||
<RedoDot size={15} />
|
return (
|
||||||
</button>
|
<tr key={p.participant.identity}>
|
||||||
<button
|
<td className="flex items-center gap-2">
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
<Speaker /> {p.participant.identity}
|
||||||
data-tip="Ban"
|
</td>
|
||||||
>
|
<td>{publicUser?.fullName}</td>
|
||||||
<LockKeyhole size={15} />
|
<td>
|
||||||
</button>
|
<span className="text-error">Nicht verbunden</span>
|
||||||
<button
|
</td>
|
||||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
<td>
|
||||||
data-tip="Profil"
|
<span className="text-success">{p.room}</span>
|
||||||
>
|
</td>
|
||||||
<User size={15} />
|
<td className="flex gap-2">
|
||||||
</button>
|
<button
|
||||||
</td>
|
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
||||||
</tr>
|
data-tip="Kick"
|
||||||
<tr>
|
onClick={() =>
|
||||||
<td>VAR0124</td>
|
kickLivekitParticipantMutation.mutate({
|
||||||
<td>Max Mustermann</td>
|
roomName: p.room,
|
||||||
<td>Christoph 31</td>
|
identity: p.participant.identity,
|
||||||
<td className="text-error">
|
})
|
||||||
<span>Nicht verbunden</span>
|
}
|
||||||
</td>
|
>
|
||||||
<td className="text-success">
|
<RedoDot size={15} />
|
||||||
<span>Verbunden</span>
|
</button>
|
||||||
</td>
|
<button
|
||||||
<td className="flex gap-2">
|
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
||||||
<button
|
data-tip="Ban"
|
||||||
className="btn btn-xs btn-square btn-warning btn-soft tooltip tooltip-bottom tooltip-warning"
|
>
|
||||||
data-tip="Kick"
|
<LockKeyhole size={15} />
|
||||||
>
|
</button>
|
||||||
<RedoDot size={15} />
|
<a
|
||||||
</button>
|
href={`${process.env.NEXT_PUBLIC_HUB_URL}/admin/user/${p.participant.attributes.userId}`}
|
||||||
<button
|
target="_blank"
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
rel="noopener noreferrer"
|
||||||
data-tip="Ban"
|
>
|
||||||
>
|
<button
|
||||||
<LockKeyhole size={15} />
|
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
||||||
</button>
|
data-tip="Profil"
|
||||||
<button
|
>
|
||||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
<User size={15} />
|
||||||
data-tip="Profil"
|
</button>
|
||||||
>
|
</a>
|
||||||
<User size={15} />
|
</td>
|
||||||
</button>
|
</tr>
|
||||||
</td>
|
);
|
||||||
</tr>
|
})}
|
||||||
<tr>
|
|
||||||
<td>VAR0124</td>
|
|
||||||
<td>Max Mustermann</td>
|
|
||||||
<td>Christoph 31</td>
|
|
||||||
<td className="text-error">
|
|
||||||
<span>Nicht verbunden</span>
|
|
||||||
</td>
|
|
||||||
<td className="text-success">
|
|
||||||
<span>Verbunden</span>
|
|
||||||
</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"
|
|
||||||
>
|
|
||||||
<RedoDot size={15} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-square btn-error btn-soft tooltip tooltip-bottom tooltip-error"
|
|
||||||
data-tip="Ban"
|
|
||||||
>
|
|
||||||
<LockKeyhole size={15} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-square btn-info btn-soft tooltip tooltip-bottom tooltip-info"
|
|
||||||
data-tip="Profil"
|
|
||||||
>
|
|
||||||
<User size={15} />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-300 shadow-md w-full mt-4 max-h-48 overflow-y-auto">
|
{/* <div className="card bg-base-300 shadow-md w-full mt-4 max-h-48 overflow-y-auto">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="card-title flex items-center gap-2">
|
<div className="card-title flex items-center gap-2">
|
||||||
<ShieldAlert size={20} /> Allgemeine Befehle
|
<ShieldAlert size={20} /> Allgemeine Befehle
|
||||||
@@ -266,7 +315,7 @@ export default function AdminPanel() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" className="modal-backdrop">
|
<form method="dialog" className="modal-backdrop">
|
||||||
<button>close</button>
|
<button>close</button>
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export const SettingsBtn = () => {
|
|||||||
const testSoundRef = useRef<HTMLAudioElement | null>(null);
|
const testSoundRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
const editUserMutation = useMutation({
|
const editUserMutation = useMutation({
|
||||||
mutationFn: ({ user }: { user: Prisma.UserUpdateInput }) =>
|
mutationFn: editUserAPI,
|
||||||
editUserAPI(session.data!.user.id, user),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -201,7 +200,8 @@ export const SettingsBtn = () => {
|
|||||||
onSubmit={() => false}
|
onSubmit={() => false}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
testSoundRef.current?.pause();
|
testSoundRef.current?.pause();
|
||||||
const res = await editUserMutation.mutateAsync({
|
await editUserMutation.mutateAsync({
|
||||||
|
id: session.data!.user.id,
|
||||||
user: {
|
user: {
|
||||||
settingsMicDevice: selectedDevice,
|
settingsMicDevice: selectedDevice,
|
||||||
settingsMicVolume: micVol,
|
settingsMicVolume: micVol,
|
||||||
|
|||||||
9
apps/dispatch/app/_helpers/LivekitRoomManager.ts
Normal file
9
apps/dispatch/app/_helpers/LivekitRoomManager.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { RoomServiceClient } from "livekit-server-sdk";
|
||||||
|
|
||||||
|
if (!process.env.NEXT_PUBLIC_LIVEKIT_URL) throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not defined");
|
||||||
|
|
||||||
|
export const RoomManager = new RoomServiceClient(
|
||||||
|
process.env.NEXT_PUBLIC_LIVEKIT_URL!,
|
||||||
|
process.env.LIVEKIT_API_KEY,
|
||||||
|
process.env.LIVEKIT_API_SECRET,
|
||||||
|
);
|
||||||
@@ -27,3 +27,14 @@ export const getConnectedAircraftPositionLogAPI = async ({ id }: { id: number })
|
|||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const kickAircraftAPI = async ({ id, bann }: { id: number; bann?: boolean }) => {
|
||||||
|
const res = await serverApi.delete(`/aircrafts/${id}`, {
|
||||||
|
data: { bann },
|
||||||
|
});
|
||||||
|
console.log(res.status);
|
||||||
|
if (res.status != 204) {
|
||||||
|
throw new Error("Failed to kick aircraft");
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -35,3 +35,14 @@ export const getConnectedDispatcherAPI = async (filter?: Prisma.ConnectedDispatc
|
|||||||
}
|
}
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const kickDispatcherAPI = async ({ id, bann }: { id: number; bann?: boolean }) => {
|
||||||
|
const res = await serverApi.delete(`/dispatcher/${id}`, {
|
||||||
|
data: { bann },
|
||||||
|
});
|
||||||
|
console.log(res.status);
|
||||||
|
if (res.status != 204) {
|
||||||
|
throw new Error("Failed to kick aircraft");
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|||||||
28
apps/dispatch/app/_querys/livekit.ts
Normal file
28
apps/dispatch/app/_querys/livekit.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { Room } from "livekit-client";
|
||||||
|
import { ParticipantInfo } from "livekit-server-sdk";
|
||||||
|
|
||||||
|
export const getLivekitRooms = async () => {
|
||||||
|
const res = await axios.get<
|
||||||
|
{
|
||||||
|
room: Room;
|
||||||
|
participants: ParticipantInfo[];
|
||||||
|
}[]
|
||||||
|
>("/api/livekit-participant");
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error("Failed to fetch keywords");
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const kickLivekitParticipant = async (body: { identity: string; roomName: string }) => {
|
||||||
|
const res = await axios.delete("/api/livekit-participant", {
|
||||||
|
params: body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error("Failed to kick participant");
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Prisma, User } from "@repo/db";
|
import { Prisma, User } from "@repo/db";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const editUserAPI = async (id: string, user: Prisma.UserUpdateInput) => {
|
export const editUserAPI = async ({ id, user }: { id: string; user: Prisma.UserUpdateInput }) => {
|
||||||
const response = await axios.post<User>(`/api/user?id=${id}`, user);
|
const response = await axios.post<User>(`/api/user?id=${id}`, user);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
85
apps/dispatch/app/api/livekit-participant/route.ts
Normal file
85
apps/dispatch/app/api/livekit-participant/route.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { RoomManager } from "_helpers/LivekitRoomManager";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export const GET = async (request: NextRequest) => {
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session) return Response.json({ message: "Unauthorized" }, { status: 401 });
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.permissions.includes("AUDIO_ADMIN"))
|
||||||
|
return Response.json({ message: "Missing permissions" }, { status: 401 });
|
||||||
|
|
||||||
|
const rooms = await RoomManager.listRooms();
|
||||||
|
|
||||||
|
const roomsWithParticipants = rooms.map(async (room) => {
|
||||||
|
const participants = await RoomManager.listParticipants(room.name);
|
||||||
|
return {
|
||||||
|
room,
|
||||||
|
participants,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.json(await Promise.all(roomsWithParticipants), { status: 200 });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE = async (request: NextRequest) => {
|
||||||
|
try {
|
||||||
|
const identity = request.nextUrl.searchParams.get("identity");
|
||||||
|
const roomName = request.nextUrl.searchParams.get("roomName");
|
||||||
|
const ban = request.nextUrl.searchParams.get("ban");
|
||||||
|
|
||||||
|
if (!identity) return Response.json({ message: "Missing User identity" }, { status: 400 });
|
||||||
|
if (!roomName) return Response.json({ message: "Missing roomName" }, { status: 400 });
|
||||||
|
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session) return Response.json({ message: "Unauthorized" }, { status: 401 });
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.permissions.includes("AUDIO_ADMIN"))
|
||||||
|
return Response.json({ message: "Missing permissions" }, { status: 401 });
|
||||||
|
|
||||||
|
if (ban && !user.permissions.includes("ADMIN_USER")) {
|
||||||
|
return Response.json({ message: "Missing permissions to ban user" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ban) {
|
||||||
|
const participant = await RoomManager.getParticipant(roomName, identity);
|
||||||
|
const pUser = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: participant.attributes.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!pUser) return;
|
||||||
|
// If the user is banned, we need to remove their permissions
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
data: {
|
||||||
|
permissions: {
|
||||||
|
set: pUser.permissions.filter((p) => p !== "AUDIO"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await RoomManager.removeParticipant(roomName, identity);
|
||||||
|
return Response.json(
|
||||||
|
{ message: `User ${identity} kicked from room ${roomName}` },
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in DELETE /api/livekit-participant:", error);
|
||||||
|
return Response.json({ message: "Internal Server Error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -27,8 +27,7 @@ export const GET = async (request: NextRequest) => {
|
|||||||
|
|
||||||
const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, {
|
const at = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, {
|
||||||
identity: user.publicId,
|
identity: user.publicId,
|
||||||
// Token to expire after 10 minutes
|
ttl: "1h",
|
||||||
ttl: "1d",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
at.addGrant({
|
at.addGrant({
|
||||||
@@ -41,6 +40,8 @@ export const GET = async (request: NextRequest) => {
|
|||||||
|
|
||||||
at.attributes = {
|
at.attributes = {
|
||||||
publicId: user.publicId,
|
publicId: user.publicId,
|
||||||
|
publicUser: JSON.stringify(getPublicUser(user)),
|
||||||
|
userId: user.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = await at.toJwt();
|
const token = await at.toJwt();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Connection } from "./_components/Connection";
|
import { Connection } from "./_components/Connection";
|
||||||
/* import { ThemeSwap } from "./_components/ThemeSwap"; */
|
/* import { ThemeSwap } from "./_components/ThemeSwap"; */
|
||||||
import { Audio } from "../../../_components/Audio/Audio";
|
import { Audio } from "../../../_components/Audio/Audio";
|
||||||
@@ -9,24 +7,16 @@ import Link from "next/link";
|
|||||||
import { Settings } from "_components/navbar/Settings";
|
import { Settings } from "_components/navbar/Settings";
|
||||||
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown";
|
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown";
|
||||||
import AdminPanel from "_components/navbar/AdminPanel";
|
import AdminPanel from "_components/navbar/AdminPanel";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
|
||||||
export default function Navbar() {
|
export default async function Navbar() {
|
||||||
/* const [isDark, setIsDark] = useState(false);
|
const session = await getServerSession();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
const newTheme = !isDark;
|
|
||||||
setIsDark(newTheme);
|
|
||||||
document.documentElement.setAttribute(
|
|
||||||
"data-theme",
|
|
||||||
newTheme ? "nord" : "dark",
|
|
||||||
);
|
|
||||||
}; */
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-base-100 shadow-sm flex gap-5 justify-between">
|
<div className="navbar bg-base-100 shadow-sm flex gap-5 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ModeSwitchDropdown />
|
<ModeSwitchDropdown />
|
||||||
<AdminPanel />
|
{session?.user.permissions.includes("ADMIN_KICK") && <AdminPanel />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-5">
|
<div className="flex items-center gap-5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"migrate": "turbo migrate",
|
"migrate": "turbo migrate",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"studio": "turbo run studio",
|
"studio": "turbo run studio",
|
||||||
|
"prod-start": "docker-compose --env-file .env.prod -f 'docker-compose.prod.yml' up -d --build",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Mission } from "../../generated/client";
|
import { Mission } from "../../generated/client";
|
||||||
|
import { PublicUser } from "./User";
|
||||||
|
|
||||||
interface ValidationFailed {
|
export interface ValidationFailed {
|
||||||
type: "hpg-validation";
|
type: "hpg-validation";
|
||||||
status: "failed";
|
status: "failed";
|
||||||
message: string;
|
message: string;
|
||||||
@@ -9,7 +10,7 @@ interface ValidationFailed {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidationSuccess {
|
export interface ValidationSuccess {
|
||||||
type: "hpg-validation";
|
type: "hpg-validation";
|
||||||
status: "success";
|
status: "success";
|
||||||
message: string;
|
message: string;
|
||||||
@@ -18,4 +19,13 @@ interface ValidationSuccess {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotificationPayload = ValidationFailed | ValidationSuccess;
|
export interface AdminMessage {
|
||||||
|
type: "admin-message";
|
||||||
|
status: "kick" | "ban";
|
||||||
|
message: string;
|
||||||
|
data?: {
|
||||||
|
admin: PublicUser;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationPayload = ValidationFailed | ValidationSuccess | AdminMessage;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ enum PERMISSION {
|
|||||||
ADMIN_STATION
|
ADMIN_STATION
|
||||||
ADMIN_KEYWORD
|
ADMIN_KEYWORD
|
||||||
ADMIN_MESSAGE
|
ADMIN_MESSAGE
|
||||||
|
ADMIN_KICK
|
||||||
AUDIO
|
AUDIO
|
||||||
PILOT
|
PILOT
|
||||||
DISPO
|
DISPO
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -34,7 +34,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))
|
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':
|
'@next-auth/prisma-adapter':
|
||||||
specifier: ^1.0.7
|
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(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(@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))
|
||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@19.1.0)
|
version: 1.3.2(react@19.1.0)
|
||||||
@@ -103,7 +103,7 @@ importers:
|
|||||||
version: 15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: ^4.24.11
|
specifier: ^4.24.11
|
||||||
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)
|
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)
|
||||||
npm:
|
npm:
|
||||||
specifier: ^11.4.1
|
specifier: ^11.4.1
|
||||||
version: 11.4.1
|
version: 11.4.1
|
||||||
@@ -164,6 +164,9 @@ importers:
|
|||||||
'@socket.io/redis-adapter':
|
'@socket.io/redis-adapter':
|
||||||
specifier: ^8.3.0
|
specifier: ^8.3.0
|
||||||
version: 8.3.0(socket.io-adapter@2.5.5)
|
version: 8.3.0(socket.io-adapter@2.5.5)
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.9
|
||||||
|
version: 9.0.9
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
version: 1.9.0
|
version: 1.9.0
|
||||||
@@ -248,7 +251,7 @@ importers:
|
|||||||
version: 5.0.1(react-hook-form@7.57.0(react@19.1.0))
|
version: 5.0.1(react-hook-form@7.57.0(react@19.1.0))
|
||||||
'@next-auth/prisma-adapter':
|
'@next-auth/prisma-adapter':
|
||||||
specifier: ^1.0.7
|
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(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(@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))
|
||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@19.1.0)
|
version: 1.3.2(react@19.1.0)
|
||||||
@@ -326,7 +329,7 @@ importers:
|
|||||||
version: 15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.3.3(@babel/core@7.27.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: ^4.24.11
|
specifier: ^4.24.11
|
||||||
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)
|
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)
|
||||||
next-remove-imports:
|
next-remove-imports:
|
||||||
specifier: ^1.0.12
|
specifier: ^1.0.12
|
||||||
version: 1.0.12(webpack@5.99.9)
|
version: 1.0.12(webpack@5.99.9)
|
||||||
@@ -5164,10 +5167,10 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.9.0
|
'@tybys/wasm-util': 0.9.0
|
||||||
optional: true
|
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(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(@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))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/client': 6.8.2(prisma@6.8.2(typescript@5.8.3))(typescript@5.8.3)
|
'@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)
|
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/env@15.3.3': {}
|
'@next/env@15.3.3': {}
|
||||||
|
|
||||||
@@ -8046,7 +8049,7 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
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-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):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.4
|
'@babel/runtime': 7.27.4
|
||||||
'@panva/hkdf': 1.2.1
|
'@panva/hkdf': 1.2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user