26 Commits

Author SHA1 Message Date
PxlLoewe
d4c1c816ff Merge pull request #165 from VAR-Virtual-Air-Rescue/staging 2026-02-08 23:46:22 +01:00
PxlLoewe
1b425d82e2 cron zum aktuallisieren der Discord Avatare hinzugefügt 2026-02-08 22:22:25 +01:00
PxlLoewe
5d0a36f967 Report anzeige für auto-reports verbessert 2026-02-08 21:37:10 +01:00
PxlLoewe
b46ad25bde Imports entfernt 2026-02-08 20:17:44 +01:00
PxlLoewe
d5a4118025 Admin-Panel hinzugefügt 2026-02-08 20:12:45 +01:00
PxlLoewe
aded6d1492 HPG Warnung in Dispatch Settings, Status Notification 2026-02-08 13:03:11 +01:00
PxlLoewe
8340c2408c Fixed Acc deleted Warnung im Profil 2026-02-07 13:43:37 +01:00
PxlLoewe
31c17e2dda Merge pull request #161 from VAR-Virtual-Air-Rescue/staging
admin link
2026-02-01 12:52:46 +01:00
PxlLoewe
c8eeee1452 Merge pull request #160 from VAR-Virtual-Air-Rescue/staging
Fixed Wrong IP being loged
2026-02-01 11:50:14 +01:00
PxlLoewe
3d1b83cd32 Merge pull request #159 from VAR-Virtual-Air-Rescue/staging
List headers
2026-02-01 10:26:35 +01:00
PxlLoewe
40b11d2501 Merge pull request #158 from VAR-Virtual-Air-Rescue/staging
Catch Blocks
2026-02-01 00:19:22 +01:00
PxlLoewe
9303878d8d Merge pull request #157 from VAR-Virtual-Air-Rescue/staging
fixed permacrash core-server
2026-01-31 23:42:07 +01:00
PxlLoewe
25692b66be Merge pull request #156 from VAR-Virtual-Air-Rescue/staging
v2.0.8
2026-01-31 23:08:50 +01:00
PxlLoewe
8956204a2f Dockerfile Hub 2026-01-15 23:54:51 +01:00
PxlLoewe
a5b4696644 Update Dockerfile 2026-01-15 23:46:49 +01:00
PxlLoewe
a9ecf7e7b8 Merge branch 'release' of https://github.com/VAR-Virtual-Air-Rescue/var-monorepo into release 2026-01-15 23:46:16 +01:00
PxlLoewe
547768410f Merge pull request #149 from VAR-Virtual-Air-Rescue/revert-148-revert-147-staging
Revert "Revert "PR v2.0.7""
2026-01-15 23:45:26 +01:00
PxlLoewe
2c2eca6084 Revert "Revert "PR v2.0.7"" 2026-01-15 23:45:06 +01:00
PxlLoewe
5898dc6477 Merge branch 'release' of https://github.com/VAR-Virtual-Air-Rescue/var-monorepo into release 2026-01-15 23:44:08 +01:00
PxlLoewe
c9499e08be dev 2026-01-15 23:43:53 +01:00
PxlLoewe
0429d8b770 Merge pull request #148 from VAR-Virtual-Air-Rescue/revert-147-staging
Revert "PR v2.0.7"
2026-01-15 23:35:32 +01:00
PxlLoewe
7175f6571e Revert "PR v2.0.7" 2026-01-15 23:35:14 +01:00
PxlLoewe
614b92325e Merge pull request #147 from VAR-Virtual-Air-Rescue/staging
@everyone | Wir haben eine kurze Downtime überwunden und stellen euch heute v2.0.7 vor.

In der Vergangenheit haben wir viel an der Dispositionsseite gearbeitet. Das ändert sich heute. Wir aktualisieren die Bedieneinheit für Piloten auf eine neue Softwareversion, die dem Sepura SCG22 nachempfunden ist und so tatsächlich in vielen Hubschraubern verbaut ist.

### Neue Features:
- Ladescreen beim Einschalten
- Wechseln der Rufgruppe direkt im Gerät
- Status senden/empfangen
- SDS-Text bzw. senden/empfangen
- Nachtmodus ab 22:00 Uhr
- Popup bei Funkverkehr auf der Rufgruppe

Eine entsprechende Dokumentation findet ihr[ in den Docs](https://docs.virtualairrescue.com/allgemein/var-systeme/leitstelle/pilot.html).

### Weiteres:
- kleinere Bugfixes
- Performanceupgrades durch verbessertes Backup-Handling
2026-01-15 22:59:21 +01:00
PxlLoewe
b1e508ef36 release v2.0.6
v2.0.6
2026-01-06 13:58:24 +01:00
PxlLoewe
6e8884f3fb Merge pull request #141 from VAR-Virtual-Air-Rescue/staging
v2.0.5
2025-12-27 16:23:33 +01:00
PxlLoewe
dde52bde39 Release v2.0.4
Release v2.0.4
2025-12-08 19:48:53 +01:00
20 changed files with 249 additions and 95 deletions

View File

@@ -3,6 +3,7 @@ import { io } from "index";
import cron from "node-cron"; import cron from "node-cron";
import { setUserStandardNamePermissions } from "routes/helper"; import { setUserStandardNamePermissions } from "routes/helper";
import { changeMemberRoles } from "routes/member"; import { changeMemberRoles } from "routes/member";
import client from "./discord";
const removeMission = async (id: number, reason: string) => { const removeMission = async (id: number, reason: string) => {
const log: MissionLog = { const log: MissionLog = {
@@ -121,6 +122,37 @@ const removeClosedMissions = async () => {
return removeMission(mission.id, "dem freimelden aller Stationen"); 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 removeConnectedAircrafts = async () => {
const connectedAircrafts = await prisma.connectedAircraft.findMany({ const connectedAircrafts = await prisma.connectedAircraft.findMany({
where: { 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 () => { cron.schedule("*/1 * * * *", async () => {
try { try {

View File

@@ -45,7 +45,7 @@ router.post("/rename", async (req: Request, res: Response) => {
console.log(`Member ${member.id} renamed to ${newName}`); console.log(`Member ${member.id} renamed to ${newName}`);
res.status(200).json({ message: "Member renamed successfully" }); res.status(200).json({ message: "Member renamed successfully" });
} catch (error) { } 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" }); 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); const result = await changeMemberRoles(memberId, roleIds, action);
res.status(200).json(result); res.status(200).json(result);
} catch (error) { } 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` }); res.status(500).json({ error: `Failed to ${action} roles` });
} }
}; };

View File

@@ -43,14 +43,20 @@ router.post("/admin-embed", async (req, res) => {
{ name: "angemeldet als", value: report.reportedUserRole, inline: true }, { name: "angemeldet als", value: report.reportedUserRole, inline: true },
{ {
name: "gemeldet von", 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({ .setFooter({
text: "Bitte reagiere mit 🫡, wenn du den Report bearbeitet hast, oder mit ✅, wenn er abgeschlossen ist.", text: "Bitte reagiere mit 🫡, wenn du den Report bearbeitet hast, oder mit ✅, wenn er abgeschlossen ist.",
}) })
.setTimestamp(new Date(report.timestamp)) .setTimestamp(new Date(report.timestamp));
.setColor("DarkRed"); if (report.reviewed) {
embed.setColor("DarkGreen");
} else {
embed.setColor("DarkRed");
}
const reportsChannel = await client.channels.fetch(process.env.DISCORD_REPORT_CHANNEL!); const reportsChannel = await client.channels.fetch(process.env.DISCORD_REPORT_CHANNEL!);
if (!reportsChannel || !reportsChannel.isSendable()) { if (!reportsChannel || !reportsChannel.isSendable()) {
@@ -59,7 +65,9 @@ router.post("/admin-embed", async (req, res) => {
} }
const message = await reportsChannel.send({ embeds: [embed] }); const message = await reportsChannel.send({ embeds: [embed] });
message.react("🫡").catch(console.error); message.react("🫡").catch(console.error);
message.react("✅").catch(console.error); if (!report.reviewed) {
message.react("✅").catch(console.error);
}
res.json({ res.json({
message: "Report embed sent to Discord channel", message: "Report embed sent to Discord channel",
}); });

View File

@@ -1,4 +1,4 @@
import axios from "axios"; import axios, { AxiosError } from "axios";
const discordAxiosClient = axios.create({ const discordAxiosClient = axios.create({
baseURL: process.env.CORE_SERVER_URL, baseURL: process.env.CORE_SERVER_URL,
@@ -11,7 +11,10 @@ export const renameMember = async (memberId: string, newName: string) => {
newName, newName,
}) })
.catch((error) => { .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, roleIds,
}) })
.catch((error) => { .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, roleIds,
}) })
.catch((error) => { .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, reportId,
}) })
.catch((error) => { .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,
);
}); });
}; };

View File

@@ -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( io.to(
sdsMessage.data.direction === "to-lst" ? "dispatchers" : `station:${sdsMessage.data.stationId}`, sdsMessage.data.direction === "to-lst" ? "dispatchers" : `station:${sdsMessage.data.stationId}`,
).emit(sdsMessage.data.direction === "to-lst" ? "notification" : "sds-status", { ).emit(sdsMessage.data.direction === "to-lst" ? "notification" : "sds-status", {
@@ -163,6 +172,7 @@ router.post("/:id/send-sds-message", async (req, res) => {
data: { data: {
aircraftId: parseInt(id), aircraftId: parseInt(id),
stationId: sdsMessage.data.stationId, stationId: sdsMessage.data.stationId,
userId: user.id,
}, },
} as NotificationPayload); } as NotificationPayload);

View File

@@ -3,8 +3,11 @@ import Link from "next/link";
import { prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper"; import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper";
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown"; 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 }) { export default async function Navbar({ children }: { children: React.ReactNode }) {
const session = await getServerSession();
const latestChangelog = await prisma.changelog.findFirst({ const latestChangelog = await prisma.changelog.findFirst({
orderBy: { orderBy: {
createdAt: "desc", createdAt: "desc",
@@ -17,6 +20,7 @@ export default async function Navbar({ children }: { children: React.ReactNode }
<p className="text-xl font-semibold normal-case">VAR Operations Center</p> <p className="text-xl font-semibold normal-case">VAR Operations Center</p>
<ChangelogWrapper latestChangelog={latestChangelog} /> <ChangelogWrapper latestChangelog={latestChangelog} />
</div> </div>
{session?.user.permissions.includes("ADMIN_KICK") && <AdminPanel />}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{children} {children}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { GearIcon } from "@radix-ui/react-icons"; 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 MicVolumeBar from "_components/MicVolumeIndication";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { editUserAPI, getUserAPI } from "_querys/user"; import { editUserAPI, getUserAPI } from "_querys/user";
@@ -27,7 +27,6 @@ export const SettingsBtn = () => {
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] }); await queryClient.invalidateQueries({ queryKey: ["user", session.data?.user.id] });
}, },
}); });
useEffect(() => { useEffect(() => {
@@ -82,10 +81,14 @@ export const SettingsBtn = () => {
useEffect(() => { useEffect(() => {
const setDevices = async () => { const setDevices = async () => {
if (typeof navigator !== "undefined" && navigator.mediaDevices?.enumerateDevices) { if (typeof navigator !== "undefined" && navigator.mediaDevices?.enumerateDevices) {
const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); try {
const devices = await navigator.mediaDevices.enumerateDevices(); const stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
setInputDevices(devices.filter((d) => d.kind === "audioinput")); const devices = await navigator.mediaDevices.enumerateDevices();
stream.getTracks().forEach((track) => track.stop()); 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 }); setSettingsPartial({ useHPGAsDispatcher: e.target.checked });
}} }}
/> />
HPG als Disponent verwenden HPG Validierung verwenden{" "}
<div
className="tooltip tooltip-warning"
data-tip="Achtung! Mit der Client Version v2.0.1.0 werden Einsätze auch ohne Validierung an die
HPG gesendet. Die Validierung über die HPG kann zu Verzögerungen bei der
Einsatzübermittlung führen, insbesondere wenn das HPG script den Einsatz nicht sofort
validieren kann. Es wird empfohlen, diese Option nur zu aktivieren, wenn es Probleme
mit der Einsatzübermittlung gibt oder wenn die HPG Validierung ausdrücklich gewünscht
wird."
>
<Info className="text-error" size={16} />
</div>
</div> </div>
<div className="modal-action flex justify-between"> <div className="modal-action flex justify-between">

View File

@@ -78,6 +78,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
}); });
}, },
}); });
console.log("Audio Room:", audioRoom, participants, livekitUser, event);
useEffect(() => { useEffect(() => {
let soundRef: React.RefObject<HTMLAudioElement | null> | null = null; let soundRef: React.RefObject<HTMLAudioElement | null> | null = null;
@@ -113,7 +114,6 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
}; };
}, [event.status, livekitUser?.roomName, audioRoom, t.id]); }, [event.status, livekitUser?.roomName, audioRoom, t.id]);
console.log(connectedAircraft, station);
if (!connectedAircraft || !station || !session.data) return null; if (!connectedAircraft || !station || !session.data) return null;
return ( return (
<BaseNotification> <BaseNotification>

View File

@@ -201,11 +201,23 @@ export const useAudioStore = create<TalkState>((set, get) => ({
}); });
} }
const inputStream = await navigator.mediaDevices.getUserMedia({ let inputStream = await navigator.mediaDevices
audio: { .getUserMedia({
deviceId: get().settings.micDeviceId ?? undefined, 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 // Funk-Effekt anwenden
const radioStream = getRadioStream(inputStream, get().settings.micVolume); const radioStream = getRadioStream(inputStream, get().settings.micVolume);
if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen"); if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen");

View File

@@ -1,6 +1,5 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -84,6 +84,7 @@ const updateParticipantMoodleResults = async () => {
}; };
CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true }); CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true });
CronJob.from({ CronJob.from({
cronTime: "*/1 * * * *", cronTime: "*/1 * * * *",
onTick: async () => { onTick: async () => {

View File

@@ -5,6 +5,7 @@ import { renderPasswordChanged } from "./mail-templates/PasswordChanged";
import { renderVerificationCode } from "./mail-templates/ConfirmEmail"; import { renderVerificationCode } from "./mail-templates/ConfirmEmail";
import { renderBannNotice } from "modules/mail-templates/Bann"; import { renderBannNotice } from "modules/mail-templates/Bann";
import { renderTimeBanNotice } from "modules/mail-templates/TimeBann"; import { renderTimeBanNotice } from "modules/mail-templates/TimeBann";
import Mail from "nodemailer/lib/mailer";
let transporter: nodemailer.Transporter | null = null; let transporter: nodemailer.Transporter | null = null;
@@ -23,8 +24,9 @@ const initTransporter = () => {
pass: process.env.MAIL_PASSWORD, pass: process.env.MAIL_PASSWORD,
}, },
}); });
transporter.on("error", (err) => { transporter.on("error", (err) => {
console.error("Mail occurred:", err); console.error("Mail error:", err);
}); });
transporter.on("idle", () => { transporter.on("idle", () => {
console.log("Mail Idle"); console.log("Mail Idle");

View File

@@ -4,6 +4,7 @@ import { editReport } from "(app)/admin/report/actions";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Report as IReport, Prisma, User } from "@repo/db"; import { Report as IReport, Prisma, User } from "@repo/db";
import { ReportSchema, Report as IReportZod } from "@repo/db/zod"; import { ReportSchema, Report as IReportZod } from "@repo/db/zod";
import { cn } from "@repo/shared-components";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable } from "_components/PaginatedTable";
import { Button } from "_components/ui/Button"; import { Button } from "_components/ui/Button";
import { Switch } from "_components/ui/Switch"; import { Switch } from "_components/ui/Switch";
@@ -31,14 +32,23 @@ export const ReportSenderInfo = ({
</Link> </Link>
<span className="text-primary">{report.reportedUserRole}</span> <span className="text-primary">{report.reportedUserRole}</span>
</h2> </h2>
<div className="textarea w-full text-left">{report.text}</div> <div className="textarea w-full whitespace-pre-wrap text-left">{report.text}</div>
<Link {Sender ? (
href={`/admin/user/${Sender?.id}`} <Link
className="link link-hover text-right text-sm text-gray-600" href={`/admin/user/${Sender?.id}`}
> className={cn(
gemeldet von Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}) am{" "} "link link-hover text-right text-sm text-gray-600",
{new Date(report.timestamp).toLocaleString()} !Sender && "text-green-300",
</Link> )}
>
gemeldet von Nutzer {Sender.firstname} {Sender.lastname} ({Sender.publicId}) am{" "}
{new Date(report.timestamp).toLocaleString()}
</Link>
) : (
<p className="text-right text-sm text-green-300">
gemeldet vom System am {new Date(report.timestamp).toLocaleString()}
</p>
)}
</div> </div>
); );
}; };

View File

@@ -25,8 +25,12 @@ export const reportColumns: ColumnDef<Report & { Sender?: User; Reported: User }
header: "Sender", header: "Sender",
cell: ({ row }) => { cell: ({ row }) => {
const user = row.original.Sender; const user = row.original.Sender;
if (!user) return "Unbekannt"; if (!user) return <p className="text-green-300">System</p>;
return `${user.firstname} ${user.lastname} (${user.publicId})`; return (
<Link
href={`/admin/user/${user.id}`}
>{`${user.firstname} ${user.lastname} (${user.publicId})`}</Link>
);
}, },
}, },
{ {

View File

@@ -47,30 +47,42 @@ export const AccountLog = ({ sameIPLogs, userId }: { sameIPLogs: Log[]; userId:
</div> </div>
} }
getFilter={(searchTerm) => { getFilter={(searchTerm) => {
return { if (onlyImportant) {
AND: [ return {
onlyImportant AND: [
? { { ip: { contains: searchTerm } },
type: { { browser: { contains: searchTerm } },
in: ["REGISTER", "PROFILE_CHANGE"], {
}, id: {
} in: sameIPLogs
: {}, .filter((log) => log.id.toString().includes(searchTerm))
{ .map((log) => log.id),
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={{ include={{
User: true, User: true,

View File

@@ -704,23 +704,15 @@ export const AdminForm = ({
<div role="alert" className="alert alert-warning alert-outline flex flex-col"> <div role="alert" className="alert alert-warning alert-outline flex flex-col">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TriangleAlert /> <TriangleAlert />
{openBans.map((ban) => ( <div>
<div key={ban.id}> <h3 className="text-lg font-semibold">Account gelöscht</h3>
<h3 className="text-lg font-semibold">Account gelöscht</h3> </div>
</div> <div>
))} <h3 className="text-lg font-semibold">
{openTimebans.map((timeban) => ( Dieser Account ist als gelöscht markiert, der Nutzer kann sich nicht mehr anmelden.
<div key={timeban.id}> </h3>
<h3 className="text-lg font-semibold"> </div>
Dieser Account ist als gelöscht markiert, der Nutzer kann sich nicht mehr
anmelden.
</h3>
</div>
))}
</div> </div>
<p className="text-sm text-gray-400">
Achtung! Die Strafe(n) sind aktiv, die Rechte des Nutzers müssen nicht angepasst werden!
</p>
</div> </div>
)} )}
{(user.CanonicalUser || (user.Duplicates && user.Duplicates.length > 0)) && ( {(user.CanonicalUser || (user.Duplicates && user.Duplicates.length > 0)) && (

View File

@@ -407,8 +407,11 @@ export const DeleteForm = ({
export const PasswordForm = (): React.JSX.Element => { export const PasswordForm = (): React.JSX.Element => {
const schema = z.object({ const schema = z.object({
password: z.string().min(2).max(30), password: z.string().min(2).max(30),
newPassword: z.string().min(2).max(30), newPassword: z
newPasswordConfirm: z.string().min(2).max(30), .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); const [isLoading, setIsLoading] = useState(false);
type IFormInput = z.infer<typeof schema>; type IFormInput = z.infer<typeof schema>;

View File

@@ -55,18 +55,45 @@ export const logAction = async (
}, },
}); });
if (existingLogs.length > 0 && user?.user.id) { if (existingLogs.length > 0 && user?.user.id) {
// Möglicherweise ein doppelter Account, Report erstellen const existingReport = await prisma.report.findFirst({
const report = await prisma.report.create({ where: {
data: { reportedUserId: user.user.id,
text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs reportedUserRole: {
.map((log) => `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${log.timestamp}`) contains: "Doppelter Account Verdacht",
.join("\n")}`, },
reportedUserId: user?.user.id,
reportedUserRole: "LOGIN - 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);
}
} }
} }

View File

@@ -49,8 +49,8 @@ export const Register = () => {
.refine((val) => val.length === 0 || val.includes(" ") || /^[A-ZÄÖÜ]/.test(val), { .refine((val) => val.length === 0 || val.includes(" ") || /^[A-ZÄÖÜ]/.test(val), {
message: "Der Nachname muss mit einem Großbuchstaben beginnen", message: "Der Nachname muss mit einem Großbuchstaben beginnen",
}), }),
password: z.string().min(12, { password: z.string().min(8, {
message: "Das Passwort muss mindestens 12 Zeichen lang sein", message: "Das Passwort muss mindestens 8 Zeichen lang sein",
}), }),
passwordConfirm: z.string(), passwordConfirm: z.string(),
}) })

View File

@@ -1,6 +1,6 @@
import axios from "axios"; import axios from "axios";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { DiscordAccount, prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { getServerSession } from "../auth/[...nextauth]/auth"; import { getServerSession } from "../auth/[...nextauth]/auth";
import { setStandardName } from "../../../helper/discord"; import { setStandardName } from "../../../helper/discord";
import { getUserPenaltys } from "@repo/shared-components"; import { getUserPenaltys } from "@repo/shared-components";
@@ -52,8 +52,7 @@ export const GET = async (req: NextRequest) => {
}, },
}); });
const discordObject = { const discordData = {
userId: session.user.id,
accessToken: authData.access_token, accessToken: authData.access_token,
refreshToken: authData.refresh_token, refreshToken: authData.refresh_token,
discordId: discordUser.id, discordId: discordUser.id,
@@ -63,12 +62,19 @@ export const GET = async (req: NextRequest) => {
globalName: discordUser.global_name || discordUser.username, globalName: discordUser.global_name || discordUser.username,
verified: discordUser.verified, verified: discordUser.verified,
tokenType: authData.token_type, tokenType: authData.token_type,
} as DiscordAccount; };
await prisma.discordAccount.upsert({ await prisma.discordAccount.upsert({
where: { discordId: discordUser.id }, where: { discordId: discordUser.id },
update: discordObject, // Updates if found update: discordData, // Updates if found
create: discordObject, // Creates if not found create: {
...discordData,
User: {
connect: {
id: session.user.id,
},
},
}, // Creates if not found
}); });
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: session.user.id }, where: { id: session.user.id },