Admin-tool improvements #165
@@ -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 {
|
||||||
|
|||||||
@@ -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` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
1
apps/dispatch/next-env.d.ts
vendored
1
apps/dispatch/next-env.d.ts
vendored
@@ -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.
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)) && (
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user