From 0ac943c63febd99cebc3f3a28f7981f4cac78375 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:07:09 +0100 Subject: [PATCH] Discord account Linkage, penalty update --- apps/core-server/modules/chron.ts | 55 +++++++++++++++- apps/core-server/routes/helper.ts | 27 +++++++- apps/hub-server/routes/event.ts | 4 +- .../app/(app)/_components/RecentFlights.tsx | 2 +- .../admin/user/[id]/_components/forms.tsx | 62 +++++++++++++++++++ apps/hub/app/(app)/admin/user/[id]/page.tsx | 28 ++++++++- apps/hub/app/(app)/admin/user/page.tsx | 48 ++++++++++---- .../app/(app)/settings/_components/forms.tsx | 20 +++--- apps/hub/app/(app)/settings/actions.ts | 12 +++- apps/hub/app/(app)/settings/page.tsx | 21 +++++-- .../app/_components/BookingTimelineModal.tsx | 13 +++- apps/hub/app/_components/Table.tsx | 8 ++- apps/hub/app/api/discord-redirect/route.ts | 25 ++++++++ apps/hub/next.config.ts | 6 +- packages/database/package.json | 8 +-- .../migration.sql | 33 ++++++++++ .../migration.sql | 35 +++++++++++ .../migration.sql | 8 +++ packages/database/prisma/schema/user.prisma | 22 +++++-- 19 files changed, 388 insertions(+), 49 deletions(-) create mode 100644 packages/database/prisma/schema/migrations/20260105235037_added_former_discord_account_schema/migration.sql create mode 100644 packages/database/prisma/schema/migrations/20260106000534_id_handling_former_dc_acc/migration.sql create mode 100644 packages/database/prisma/schema/migrations/20260106001030_unique_user_per_discord_account/migration.sql diff --git a/apps/core-server/modules/chron.ts b/apps/core-server/modules/chron.ts index f0cfde22..32c66ee0 100644 --- a/apps/core-server/modules/chron.ts +++ b/apps/core-server/modules/chron.ts @@ -1,6 +1,7 @@ -import { MissionLog, NotificationPayload, prisma } from "@repo/db"; +import { DISCORD_ROLES, MissionLog, NotificationPayload, prisma } from "@repo/db"; import { io } from "index"; import cron from "node-cron"; +import { changeMemberRoles } from "routes/member"; const removeMission = async (id: number, reason: string) => { const log: MissionLog = { @@ -34,7 +35,6 @@ const removeMission = async (id: number, reason: string) => { console.log(`Mission ${updatedMission.id} closed due to inactivity.`); }; - const removeClosedMissions = async () => { const oldMissions = await prisma.mission.findMany({ where: { @@ -140,6 +140,57 @@ const removeConnectedAircrafts = async () => { } }); }; +const removePermissionsForBannedUsers = async () => { + const activePenalties = await prisma.penalty.findMany({ + where: { + OR: [ + { + type: "BAN", + suspended: false, + }, + { + type: "TIME_BAN", + suspended: false, + until: { + gt: new Date().toISOString(), + }, + }, + ], + }, + include: { + User: { + include: { + DiscordAccount: true, + FormerDiscordAccounts: true, + }, + }, + }, + }); + + activePenalties.forEach(async (penalty) => { + const user = penalty.User; + + if (user.DiscordAccount) { + await changeMemberRoles( + user.DiscordAccount.discordId, + [DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER], + "remove", + ); + } + + user.FormerDiscordAccounts.forEach(async (formerAccount) => { + await changeMemberRoles( + formerAccount.discordId, + [DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER], + "remove", + ); + }); + }); +}; + +cron.schedule("*/5 * * * *", async () => { + await removePermissionsForBannedUsers(); +}); cron.schedule("*/1 * * * *", async () => { try { diff --git a/apps/core-server/routes/helper.ts b/apps/core-server/routes/helper.ts index 2bd0f5c2..d712ee86 100644 --- a/apps/core-server/routes/helper.ts +++ b/apps/core-server/routes/helper.ts @@ -32,6 +32,25 @@ router.post("/set-standard-name", async (req, res) => { }, }); + const activePenaltys = await prisma.penalty.findMany({ + where: { + userId: user.id, + OR: [ + { + type: "BAN", + suspended: false, + }, + { + type: "TIME_BAN", + suspended: false, + until: { + gt: new Date().toISOString(), + }, + }, + ], + }, + }); + participant.forEach(async (p) => { if (!p.Event.discordRoleId) return; if (eventCompleted(p.Event, p)) { @@ -48,8 +67,12 @@ router.post("/set-standard-name", async (req, res) => { const isPilot = user.permissions.includes("PILOT"); const isDispatcher = user.permissions.includes("DISPO"); - await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove"); - await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove"); + if (activePenaltys.length > 0) { + await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER], "remove"); + } else { + await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove"); + await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove"); + } }); export default router; diff --git a/apps/hub-server/routes/event.ts b/apps/hub-server/routes/event.ts index 8bd3803a..ac2a7e9b 100644 --- a/apps/hub-server/routes/event.ts +++ b/apps/hub-server/routes/event.ts @@ -20,7 +20,7 @@ router.post("/handle-participant-finished", async (req, res) => { Event: true, User: { include: { - discordAccounts: true, + DiscordAccount: true, }, }, }, @@ -94,7 +94,7 @@ router.post("/handle-participant-enrolled", async (req, res) => { Event: true, User: { include: { - discordAccounts: true, + DiscordAccount: true, }, }, }, diff --git a/apps/hub/app/(app)/_components/RecentFlights.tsx b/apps/hub/app/(app)/_components/RecentFlights.tsx index d9be2342..a205d8b4 100644 --- a/apps/hub/app/(app)/_components/RecentFlights.tsx +++ b/apps/hub/app/(app)/_components/RecentFlights.tsx @@ -24,7 +24,7 @@ export const RecentFlights = () => { ({ User: { id: session.data?.user.id }, Mission: { - state: { in: ["finished", "archived"] }, + state: { in: ["finished"] }, }, }) as Prisma.MissionOnStationUsersWhereInput } 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 1b56101d..d04c5309 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx @@ -6,6 +6,7 @@ import { ConnectedAircraft, ConnectedDispatcher, DiscordAccount, + FormerDiscordAccount, Penalty, PERMISSION, Prisma, @@ -60,6 +61,7 @@ import { penaltyColumns } from "(app)/admin/penalty/columns"; import { addPenalty, editPenaltys } from "(app)/admin/penalty/actions"; import { reportColumns } from "(app)/admin/report/columns"; import { sendMailByTemplate } from "../../../../../../helper/mail"; +import Image from "next/image"; interface ProfileFormProps { user: User; @@ -566,6 +568,7 @@ interface AdminFormProps { minutes: number; lastLogin?: Date; }; + formerDiscordAccounts: (FormerDiscordAccount & { DiscordAccount: DiscordAccount | null })[]; reports: { total: number; open: number; @@ -585,6 +588,7 @@ export const AdminForm = ({ pilotTime, reports, discordAccount, + formerDiscordAccounts, openBans, openTimebans, }: AdminFormProps) => { @@ -755,6 +759,64 @@ export const AdminForm = ({

)} +

+ Frühere Discord Accounts +

+
+ + + + + + + + + + + {discordAccount && ( + + + + + + + )} + {formerDiscordAccounts.map((account) => ( + + + + + + + ))} + {!discordAccount && formerDiscordAccounts.length === 0 && ( + + + + )} + +
AvatarBenutzernameDiscord IDgetrennt am
+ Discord Avatar + {discordAccount.username}{discordAccount.discordId}N/A (Aktuell verbunden)
+ {account.DiscordAccount && ( + Discord Avatar + )} + {account.DiscordAccount?.username || "Unbekannt"}{account.DiscordAccount?.discordId || "Unbekannt"}{new Date(account.removedAt).toLocaleDateString()}
+ Keine Discord Accounts verknüpft +
+

Aktivität diff --git a/apps/hub/app/(app)/admin/user/[id]/page.tsx b/apps/hub/app/(app)/admin/user/[id]/page.tsx index 8998a5a7..76e57b3d 100644 --- a/apps/hub/app/(app)/admin/user/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/page.tsx @@ -12,16 +12,37 @@ import { getUserPenaltys } from "@repo/shared-components"; export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - const user = await prisma.user.findUnique({ + let user = await prisma.user.findUnique({ where: { id: id, }, include: { - discordAccounts: true, + DiscordAccount: true, CanonicalUser: true, Duplicates: true, }, }); + if (!user) { + user = await prisma.user.findFirst({ + where: { + publicId: id, + }, + include: { + DiscordAccount: true, + CanonicalUser: true, + Duplicates: true, + }, + }); + } + + const formerDiscordAccounts = await prisma.formerDiscordAccount.findMany({ + where: { + userId: user?.id, + }, + include: { + DiscordAccount: true, + }, + }); if (!user) return ; const dispoSessions = await prisma.connectedDispatcher.findMany({ @@ -121,11 +142,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
diff --git a/apps/hub/app/(app)/admin/user/page.tsx b/apps/hub/app/(app)/admin/user/page.tsx index cdeb8b28..5ba79384 100644 --- a/apps/hub/app/(app)/admin/user/page.tsx +++ b/apps/hub/app/(app)/admin/user/page.tsx @@ -3,7 +3,7 @@ import { User2 } from "lucide-react"; import { PaginatedTable } from "../../../_components/PaginatedTable"; import Link from "next/link"; import { ColumnDef } from "@tanstack/react-table"; -import { DiscordAccount, Prisma, User } from "@repo/db"; +import { DiscordAccount, Penalty, Prisma, User } from "@repo/db"; import { useSession } from "next-auth/react"; const AdminUserPage = () => { @@ -21,16 +21,15 @@ const AdminUserPage = () => { { firstname: { contains: searchTerm, mode: "insensitive" } }, { lastname: { contains: searchTerm, mode: "insensitive" } }, { email: { contains: searchTerm, mode: "insensitive" } }, - { - discordAccounts: { - some: { username: { contains: searchTerm, mode: "insensitive" } }, - }, - }, + { publicId: { contains: searchTerm, mode: "insensitive" } }, + { DiscordAccount: { username: { contains: searchTerm, mode: "insensitive" } } }, ], } as Prisma.UserWhereInput; }} include={{ - discordAccounts: true, + DiscordAccount: true, + ReceivedReports: true, + Penaltys: true, }} initialOrderBy={[ { @@ -55,6 +54,15 @@ const AdminUserPage = () => { { header: "Berechtigungen", cell(props) { + const activePenaltys = props.row.original.Penaltys.filter( + (penalty) => + !penalty.suspended && + (penalty.type === "BAN" || + (penalty.type === "TIME_BAN" && penalty!.until! > new Date())), + ); + if (activePenaltys.length > 0) { + return AKTIVE STRAFE; + } if (props.row.original.permissions.length === 0) { return Keine; } else if (props.row.original.permissions.includes("ADMIN_USER_ADVANCED")) { @@ -69,14 +77,26 @@ const AdminUserPage = () => { ); }, }, + { + header: "Strafen / Reports", + cell(props) { + const penaltyCount = props.row.original.Penaltys.length; + const reportCount = props.row.original.ReceivedReports.length; + return ( + + {penaltyCount} / {reportCount} + + ); + }, + }, { header: "Discord", cell(props) { - const discord = props.row.original.discordAccounts; - if (discord.length === 0) { + const discord = props.row.original.DiscordAccount; + if (!discord) { return Nicht verbunden; } - return {discord.map((d) => d.username).join(", ")}; + return {discord.username}; }, }, ...(session?.user.permissions.includes("ADMIN_USER_ADVANCED") @@ -97,7 +117,13 @@ const AdminUserPage = () => {
), }, - ] as ColumnDef[] + ] as ColumnDef< + User & { + DiscordAccount: DiscordAccount; + ReceivedReports: Report[]; + Penaltys: Penalty[]; + } + >[] } // Define the columns for the user table leftOfSearch={

diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx index 93268439..64feabec 100644 --- a/apps/hub/app/(app)/settings/_components/forms.tsx +++ b/apps/hub/app/(app)/settings/_components/forms.tsx @@ -31,7 +31,7 @@ export const ProfileForm = ({ }: { user: User; penaltys: Penalty[]; - discordAccount?: DiscordAccount; + discordAccount: DiscordAccount | null; }): React.JSX.Element => { const canEdit = penaltys.length === 0 && !user.isBanned; @@ -215,9 +215,11 @@ export const ProfileForm = ({ export const SocialForm = ({ discordAccount, user, + penaltys, }: { - discordAccount?: DiscordAccount; + discordAccount: DiscordAccount | null; user: User; + penaltys: Penalty[]; }): React.JSX.Element | null => { const [isLoading, setIsLoading] = useState(false); const [vatsimLoading, setVatsimLoading] = useState(false); @@ -235,6 +237,7 @@ export const SocialForm = ({ }, resolver: zodResolver(schema), }); + const canUnlinkDiscord = !user.isBanned && penaltys.length === 0; if (!user) return null; return ( @@ -262,7 +265,7 @@ export const SocialForm = ({

- {discordAccount ? ( + {discordAccount && canUnlinkDiscord ? (