diff --git a/apps/core-server/modules/chron.ts b/apps/core-server/modules/chron.ts index ea3eb3ca..7979350e 100644 --- a/apps/core-server/modules/chron.ts +++ b/apps/core-server/modules/chron.ts @@ -3,6 +3,7 @@ import { io } from "index"; import cron from "node-cron"; import { setUserStandardNamePermissions } from "routes/helper"; import { changeMemberRoles } from "routes/member"; +import client from "./discord"; const removeMission = async (id: number, reason: string) => { const log: MissionLog = { @@ -121,6 +122,37 @@ const removeClosedMissions = async () => { return removeMission(mission.id, "dem freimelden aller Stationen"); }); }; + +const syncDiscordImgUrls = async () => { + try { + const discordAccounts = await prisma.discordAccount.findMany({ + where: { + updatedAt: { + lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + }, + User: { + isNot: null, + }, + }, + }); + for (const account of discordAccounts) { + client.users.fetch(account.discordId).then((discordUser) => { + const nextAvatar = discordUser?.avatar ?? null; + if (typeof nextAvatar !== "string") return; + if (nextAvatar === account.avatar) return; + prisma.discordAccount.update({ + where: { + id: account.id, + }, + data: { + avatar: nextAvatar, + }, + }); + }); + } + } catch (error) {} +}; + const removeConnectedAircrafts = async () => { const connectedAircrafts = await prisma.connectedAircraft.findMany({ where: { @@ -216,7 +248,13 @@ const removePermissionsForBannedUsers = async () => { } }; -removePermissionsForBannedUsers(); +cron.schedule("0 0 * * *", async () => { + try { + await syncDiscordImgUrls(); + } catch (error) { + console.error("Error on daily cron job:", error); + } +}); cron.schedule("*/1 * * * *", async () => { try { diff --git a/apps/core-server/routes/member.ts b/apps/core-server/routes/member.ts index 37ef0d76..4aa7f1e9 100644 --- a/apps/core-server/routes/member.ts +++ b/apps/core-server/routes/member.ts @@ -45,7 +45,7 @@ router.post("/rename", async (req: Request, res: Response) => { console.log(`Member ${member.id} renamed to ${newName}`); res.status(200).json({ message: "Member renamed successfully" }); } catch (error) { - console.error("Error renaming member:", error); + console.error("Error renaming member:", (error as Error).message); res.status(500).json({ error: "Failed to rename member" }); } }); @@ -84,7 +84,7 @@ const handleRoleChange = (action: "add" | "remove") => async (req: Request, res: const result = await changeMemberRoles(memberId, roleIds, action); res.status(200).json(result); } catch (error) { - console.error(`Error ${action}ing roles:`, error); + console.error(`Error ${action}ing roles:`, (error as Error).message); res.status(500).json({ error: `Failed to ${action} roles` }); } }; diff --git a/apps/core-server/routes/report.ts b/apps/core-server/routes/report.ts index a37c912c..b9f2ef95 100644 --- a/apps/core-server/routes/report.ts +++ b/apps/core-server/routes/report.ts @@ -43,14 +43,20 @@ router.post("/admin-embed", async (req, res) => { { name: "angemeldet als", value: report.reportedUserRole, inline: true }, { name: "gemeldet von", - value: `${report.Sender?.firstname} ${report.Sender?.lastname} (${report.Sender?.publicId})`, + value: report.Sender + ? `${report.Sender?.firstname} ${report.Sender?.lastname} (${report.Sender?.publicId})` + : "System", }, ) .setFooter({ text: "Bitte reagiere mit 🫡, wenn du den Report bearbeitet hast, oder mit ✅, wenn er abgeschlossen ist.", }) - .setTimestamp(new Date(report.timestamp)) - .setColor("DarkRed"); + .setTimestamp(new Date(report.timestamp)); + if (report.reviewed) { + embed.setColor("DarkGreen"); + } else { + embed.setColor("DarkRed"); + } const reportsChannel = await client.channels.fetch(process.env.DISCORD_REPORT_CHANNEL!); if (!reportsChannel || !reportsChannel.isSendable()) { @@ -59,7 +65,9 @@ router.post("/admin-embed", async (req, res) => { } const message = await reportsChannel.send({ embeds: [embed] }); message.react("🫡").catch(console.error); - message.react("✅").catch(console.error); + if (!report.reviewed) { + message.react("✅").catch(console.error); + } res.json({ message: "Report embed sent to Discord channel", }); diff --git a/apps/dispatch-server/modules/discord.ts b/apps/dispatch-server/modules/discord.ts index e8a0fb4e..311c3d9f 100644 --- a/apps/dispatch-server/modules/discord.ts +++ b/apps/dispatch-server/modules/discord.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { AxiosError } from "axios"; const discordAxiosClient = axios.create({ baseURL: process.env.CORE_SERVER_URL, @@ -11,7 +11,10 @@ export const renameMember = async (memberId: string, newName: string) => { newName, }) .catch((error) => { - console.error("Error renaming member:", error); + console.error( + "Error renaming member:", + (error as AxiosError<{ error: string }>).response?.data.error || error.message, + ); }); }; @@ -22,7 +25,10 @@ export const addRolesToMember = async (memberId: string, roleIds: string[]) => { roleIds, }) .catch((error) => { - console.error("Error adding roles to member:", error); + console.error( + "Error adding roles to member:", + (error as AxiosError<{ error: string }>).response?.data.error || error.message, + ); }); }; @@ -33,7 +39,10 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[]) roleIds, }) .catch((error) => { - console.error("Error removing roles from member:", error); + console.error( + "Error removing roles from member:", + (error as AxiosError<{ error: string }>).response?.data.error || error.message, + ); }); }; @@ -43,6 +52,9 @@ export const sendReportEmbed = async (reportId: number) => { reportId, }) .catch((error) => { - console.error("Error removing roles from member:", error); + console.error( + "Error removing roles from member:", + (error as AxiosError<{ error: string }>).response?.data.error || error.message, + ); }); }; diff --git a/apps/dispatch-server/routes/aircraft.ts b/apps/dispatch-server/routes/aircraft.ts index fa665438..b1f4677b 100644 --- a/apps/dispatch-server/routes/aircraft.ts +++ b/apps/dispatch-server/routes/aircraft.ts @@ -154,6 +154,15 @@ router.post("/:id/send-sds-message", async (req, res) => { }, }); + const user = await prisma.user.findFirst({ + where: { publicId: sdsMessage.data.user.publicId, firstname: sdsMessage.data.user.firstname }, + }); + + if (!user) { + res.status(404).json({ error: "User not found" }); + return; + } + io.to( sdsMessage.data.direction === "to-lst" ? "dispatchers" : `station:${sdsMessage.data.stationId}`, ).emit(sdsMessage.data.direction === "to-lst" ? "notification" : "sds-status", { @@ -163,6 +172,7 @@ router.post("/:id/send-sds-message", async (req, res) => { data: { aircraftId: parseInt(id), stationId: sdsMessage.data.stationId, + userId: user.id, }, } as NotificationPayload); diff --git a/apps/dispatch/app/(app)/_components/Navbar.tsx b/apps/dispatch/app/(app)/_components/Navbar.tsx index 0bcb448c..4b1ab66a 100644 --- a/apps/dispatch/app/(app)/_components/Navbar.tsx +++ b/apps/dispatch/app/(app)/_components/Navbar.tsx @@ -3,8 +3,11 @@ import Link from "next/link"; import { prisma } from "@repo/db"; import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper"; import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown"; +import { getServerSession } from "api/auth/[...nextauth]/auth"; +import AdminPanel from "_components/navbar/AdminPanel"; export default async function Navbar({ children }: { children: React.ReactNode }) { + const session = await getServerSession(); const latestChangelog = await prisma.changelog.findFirst({ orderBy: { createdAt: "desc", @@ -17,6 +20,7 @@ export default async function Navbar({ children }: { children: React.ReactNode }

VAR Operations Center

+ {session?.user.permissions.includes("ADMIN_KICK") && }
{children} diff --git a/apps/dispatch/app/(app)/dispatch/_components/navbar/Settings.tsx b/apps/dispatch/app/(app)/dispatch/_components/navbar/Settings.tsx index d2a7402d..04936372 100644 --- a/apps/dispatch/app/(app)/dispatch/_components/navbar/Settings.tsx +++ b/apps/dispatch/app/(app)/dispatch/_components/navbar/Settings.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; import { GearIcon } from "@radix-ui/react-icons"; -import { SettingsIcon, Volume2 } from "lucide-react"; +import { Info, SettingsIcon, Volume2 } from "lucide-react"; import MicVolumeBar from "_components/MicVolumeIndication"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { editUserAPI, getUserAPI } from "_querys/user"; @@ -27,7 +27,6 @@ export const SettingsBtn = () => { onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] }); }, - }); useEffect(() => { @@ -82,10 +81,14 @@ export const SettingsBtn = () => { useEffect(() => { const setDevices = async () => { if (typeof navigator !== "undefined" && navigator.mediaDevices?.enumerateDevices) { - const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); - const devices = await navigator.mediaDevices.enumerateDevices(); - setInputDevices(devices.filter((d) => d.kind === "audioinput")); - stream.getTracks().forEach((track) => track.stop()); + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); + const devices = await navigator.mediaDevices.enumerateDevices(); + setInputDevices(devices.filter((d) => d.kind === "audioinput")); + stream.getTracks().forEach((track) => track.stop()); + } catch (error) { + console.error("Error accessing media devices.", error); + } } }; @@ -214,7 +217,18 @@ export const SettingsBtn = () => { setSettingsPartial({ useHPGAsDispatcher: e.target.checked }); }} /> - HPG als Disponent verwenden + HPG Validierung verwenden{" "} +
+ +
diff --git a/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx b/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx index a3d3b64a..beb7ff06 100644 --- a/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx +++ b/apps/dispatch/app/_components/customToasts/StationStatusToast.tsx @@ -78,6 +78,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) => }); }, }); + console.log("Audio Room:", audioRoom, participants, livekitUser, event); useEffect(() => { let soundRef: React.RefObject | null = null; @@ -113,7 +114,6 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) => }; }, [event.status, livekitUser?.roomName, audioRoom, t.id]); - console.log(connectedAircraft, station); if (!connectedAircraft || !station || !session.data) return null; return ( diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index 72aabd9f..77768838 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -201,11 +201,23 @@ export const useAudioStore = create((set, get) => ({ }); } - const inputStream = await navigator.mediaDevices.getUserMedia({ - audio: { - deviceId: get().settings.micDeviceId ?? undefined, - }, - }); + let inputStream = await navigator.mediaDevices + .getUserMedia({ + audio: { + deviceId: get().settings.micDeviceId ?? undefined, + }, + }) + .catch((e) => { + console.error("Konnte das Audio-Gerät nicht öffnen:", e); + return null; + }); + if (!inputStream) { + inputStream = await navigator.mediaDevices.getUserMedia({ audio: true }).catch((e) => { + console.error("Konnte das Audio-Gerät nicht öffnen:", e); + return null; + }); + } + if (!inputStream) throw new Error("Konnte das Audio-Gerät nicht öffnen"); // Funk-Effekt anwenden const radioStream = getRadioStream(inputStream, get().settings.micVolume); if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen"); diff --git a/apps/dispatch/next-env.d.ts b/apps/dispatch/next-env.d.ts index c4b7818f..1b3be084 100644 --- a/apps/dispatch/next-env.d.ts +++ b/apps/dispatch/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/hub-server/modules/chron.ts b/apps/hub-server/modules/chron.ts index 3b8e4f2b..92401849 100644 --- a/apps/hub-server/modules/chron.ts +++ b/apps/hub-server/modules/chron.ts @@ -84,6 +84,7 @@ const updateParticipantMoodleResults = async () => { }; CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true }); + CronJob.from({ cronTime: "*/1 * * * *", onTick: async () => { diff --git a/apps/hub-server/modules/mail.ts b/apps/hub-server/modules/mail.ts index d8d0beeb..fabf48fa 100644 --- a/apps/hub-server/modules/mail.ts +++ b/apps/hub-server/modules/mail.ts @@ -5,6 +5,7 @@ import { renderPasswordChanged } from "./mail-templates/PasswordChanged"; import { renderVerificationCode } from "./mail-templates/ConfirmEmail"; import { renderBannNotice } from "modules/mail-templates/Bann"; import { renderTimeBanNotice } from "modules/mail-templates/TimeBann"; +import Mail from "nodemailer/lib/mailer"; let transporter: nodemailer.Transporter | null = null; @@ -23,8 +24,9 @@ const initTransporter = () => { pass: process.env.MAIL_PASSWORD, }, }); + transporter.on("error", (err) => { - console.error("Mail occurred:", err); + console.error("Mail error:", err); }); transporter.on("idle", () => { console.log("Mail Idle"); diff --git a/apps/hub/app/(app)/admin/report/_components/form.tsx b/apps/hub/app/(app)/admin/report/_components/form.tsx index 38e3ca81..c57853d0 100644 --- a/apps/hub/app/(app)/admin/report/_components/form.tsx +++ b/apps/hub/app/(app)/admin/report/_components/form.tsx @@ -4,6 +4,7 @@ import { editReport } from "(app)/admin/report/actions"; import { zodResolver } from "@hookform/resolvers/zod"; import { Report as IReport, Prisma, User } from "@repo/db"; import { ReportSchema, Report as IReportZod } from "@repo/db/zod"; +import { cn } from "@repo/shared-components"; import { PaginatedTable } from "_components/PaginatedTable"; import { Button } from "_components/ui/Button"; import { Switch } from "_components/ui/Switch"; @@ -31,14 +32,23 @@ export const ReportSenderInfo = ({ {report.reportedUserRole} -
{report.text}
- - gemeldet von Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}) am{" "} - {new Date(report.timestamp).toLocaleString()} - +
{report.text}
+ {Sender ? ( + + gemeldet von Nutzer {Sender.firstname} {Sender.lastname} ({Sender.publicId}) am{" "} + {new Date(report.timestamp).toLocaleString()} + + ) : ( +

+ gemeldet vom System am {new Date(report.timestamp).toLocaleString()} +

+ )}
); }; diff --git a/apps/hub/app/(app)/admin/report/columns.tsx b/apps/hub/app/(app)/admin/report/columns.tsx index e0ad7bbf..207ff792 100644 --- a/apps/hub/app/(app)/admin/report/columns.tsx +++ b/apps/hub/app/(app)/admin/report/columns.tsx @@ -25,8 +25,12 @@ export const reportColumns: ColumnDef { const user = row.original.Sender; - if (!user) return "Unbekannt"; - return `${user.firstname} ${user.lastname} (${user.publicId})`; + if (!user) return

System

; + return ( + {`${user.firstname} ${user.lastname} (${user.publicId})`} + ); }, }, { diff --git a/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx b/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx index a12a4cc5..2b934160 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx @@ -47,30 +47,42 @@ export const AccountLog = ({ sameIPLogs, userId }: { sameIPLogs: Log[]; userId: } getFilter={(searchTerm) => { - return { - AND: [ - onlyImportant - ? { - type: { - in: ["REGISTER", "PROFILE_CHANGE"], - }, - } - : {}, - { - AND: [ - { ip: { contains: searchTerm } }, - { browser: { contains: searchTerm } }, - { - id: { - in: sameIPLogs - .filter((log) => log.id.toString().includes(searchTerm)) - .map((log) => log.id), - }, + if (onlyImportant) { + return { + AND: [ + { ip: { contains: searchTerm } }, + { browser: { contains: searchTerm } }, + { + id: { + in: sameIPLogs + .filter((log) => log.id.toString().includes(searchTerm)) + .map((log) => log.id), }, - ], - }, - ], - } as Prisma.LogWhereInput; + }, + ], + } as Prisma.LogWhereInput; + } else { + return { + OR: [ + { + AND: [ + { ip: { contains: searchTerm } }, + { browser: { contains: searchTerm } }, + { + id: { + in: sameIPLogs + .filter((log) => log.id.toString().includes(searchTerm)) + .map((log) => log.id), + }, + }, + ], + }, + { + userId: userId, + }, + ], + } as Prisma.LogWhereInput; + } }} include={{ User: true, 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 4eae9184..27ccd5a7 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx @@ -704,23 +704,15 @@ export const AdminForm = ({
- {openBans.map((ban) => ( -
-

Account gelöscht

-
- ))} - {openTimebans.map((timeban) => ( -
-

- Dieser Account ist als gelöscht markiert, der Nutzer kann sich nicht mehr - anmelden. -

-
- ))} +
+

Account gelöscht

+
+
+

+ Dieser Account ist als gelöscht markiert, der Nutzer kann sich nicht mehr anmelden. +

+
-

- Achtung! Die Strafe(n) sind aktiv, die Rechte des Nutzers müssen nicht angepasst werden! -

)} {(user.CanonicalUser || (user.Duplicates && user.Duplicates.length > 0)) && ( diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx index e1739cab..580de74c 100644 --- a/apps/hub/app/(app)/settings/_components/forms.tsx +++ b/apps/hub/app/(app)/settings/_components/forms.tsx @@ -407,8 +407,11 @@ export const DeleteForm = ({ export const PasswordForm = (): React.JSX.Element => { const schema = z.object({ password: z.string().min(2).max(30), - newPassword: z.string().min(2).max(30), - newPasswordConfirm: z.string().min(2).max(30), + newPassword: z + .string() + .min(8, { message: "Das Passwort muss mindestens 8 Zeichen lang sein" }) + .max(30), + newPasswordConfirm: z.string().min(8).max(30), }); const [isLoading, setIsLoading] = useState(false); type IFormInput = z.infer; diff --git a/apps/hub/app/(auth)/login/_components/action.ts b/apps/hub/app/(auth)/login/_components/action.ts index d32c300b..40392332 100644 --- a/apps/hub/app/(auth)/login/_components/action.ts +++ b/apps/hub/app/(auth)/login/_components/action.ts @@ -55,18 +55,45 @@ export const logAction = async ( }, }); if (existingLogs.length > 0 && user?.user.id) { - // Möglicherweise ein doppelter Account, Report erstellen - const report = await prisma.report.create({ - data: { - text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs - .map((log) => `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${log.timestamp}`) - .join("\n")}`, - reportedUserId: user?.user.id, - reportedUserRole: "LOGIN - Doppelter Account Verdacht", + const existingReport = await prisma.report.findFirst({ + where: { + reportedUserId: user.user.id, + reportedUserRole: { + contains: "Doppelter Account Verdacht", + }, }, }); + // keine Doppel-Reports für denselben Nutzer erstellen + if (!existingReport) { + // Möglicherweise ein doppelter Account, Report erstellen + const report = await prisma.report.create({ + data: { + text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs + .map((log) => `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${log.timestamp}`) + .join("\n")}`, + reportedUserId: user?.user.id, + reportedUserRole: "LOGIN - Doppelter Account Verdacht", + }, + }); - await sendReportEmbed(report.id); + await sendReportEmbed(report.id); + } else { + // Update report and send it again to Discord + const updatedReport = await prisma.report.update({ + where: { + id: existingReport.id, + }, + data: { + text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs + .map( + (log) => + `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${new Date(log.timestamp).toLocaleString("de-DE")}`, + ) + .join("\n")}`, + }, + }); + await sendReportEmbed(updatedReport.id); + } } } diff --git a/apps/hub/app/(auth)/register/_components/Register.tsx b/apps/hub/app/(auth)/register/_components/Register.tsx index 11aad2b8..8c99aa67 100644 --- a/apps/hub/app/(auth)/register/_components/Register.tsx +++ b/apps/hub/app/(auth)/register/_components/Register.tsx @@ -49,8 +49,8 @@ export const Register = () => { .refine((val) => val.length === 0 || val.includes(" ") || /^[A-ZÄÖÜ]/.test(val), { message: "Der Nachname muss mit einem Großbuchstaben beginnen", }), - password: z.string().min(12, { - message: "Das Passwort muss mindestens 12 Zeichen lang sein", + password: z.string().min(8, { + message: "Das Passwort muss mindestens 8 Zeichen lang sein", }), passwordConfirm: z.string(), }) diff --git a/apps/hub/app/api/discord-redirect/route.ts b/apps/hub/app/api/discord-redirect/route.ts index 52b12204..4bbfc924 100644 --- a/apps/hub/app/api/discord-redirect/route.ts +++ b/apps/hub/app/api/discord-redirect/route.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { NextRequest, NextResponse } from "next/server"; -import { DiscordAccount, prisma } from "@repo/db"; +import { prisma } from "@repo/db"; import { getServerSession } from "../auth/[...nextauth]/auth"; import { setStandardName } from "../../../helper/discord"; import { getUserPenaltys } from "@repo/shared-components"; @@ -52,8 +52,7 @@ export const GET = async (req: NextRequest) => { }, }); - const discordObject = { - userId: session.user.id, + const discordData = { accessToken: authData.access_token, refreshToken: authData.refresh_token, discordId: discordUser.id, @@ -63,12 +62,19 @@ export const GET = async (req: NextRequest) => { globalName: discordUser.global_name || discordUser.username, verified: discordUser.verified, tokenType: authData.token_type, - } as DiscordAccount; + }; await prisma.discordAccount.upsert({ where: { discordId: discordUser.id }, - update: discordObject, // Updates if found - create: discordObject, // Creates if not found + update: discordData, // Updates if found + create: { + ...discordData, + User: { + connect: { + id: session.user.id, + }, + }, + }, // Creates if not found }); const user = await prisma.user.findUnique({ where: { id: session.user.id },