@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const penalty of activePenalties) {
|
||||
const user = penalty.User;
|
||||
|
||||
if (user.DiscordAccount) {
|
||||
await changeMemberRoles(
|
||||
user.DiscordAccount.discordId,
|
||||
[DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER],
|
||||
"remove",
|
||||
);
|
||||
}
|
||||
|
||||
for (const formerAccount of user.FormerDiscordAccounts) {
|
||||
await changeMemberRoles(
|
||||
formerAccount.discordId,
|
||||
[DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER],
|
||||
"remove",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cron.schedule("*/5 * * * *", async () => {
|
||||
await removePermissionsForBannedUsers();
|
||||
});
|
||||
|
||||
cron.schedule("*/1 * * * *", async () => {
|
||||
try {
|
||||
|
||||
@@ -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");
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ export const RecentFlights = () => {
|
||||
({
|
||||
User: { id: session.data?.user.id },
|
||||
Mission: {
|
||||
state: { in: ["finished", "archived"] },
|
||||
state: { in: ["finished"] },
|
||||
},
|
||||
}) as Prisma.MissionOnStationUsersWhereInput
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
||||
title: changelog?.title || "",
|
||||
text: changelog?.text || "",
|
||||
previewImage: changelog?.previewImage || "", // Changed to accept a URL as a string
|
||||
showOnChangelogPage: changelog?.showOnChangelogPage || true,
|
||||
},
|
||||
});
|
||||
const [skipUserUpdate, setSkipUserUpdate] = useState(false);
|
||||
@@ -84,6 +85,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
||||
placeholder="Titel (vX.X.X)"
|
||||
className="input-sm"
|
||||
/>
|
||||
|
||||
<Input
|
||||
form={form}
|
||||
label="Bild-URL"
|
||||
@@ -146,6 +148,16 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
<label className="label mx-6 mt-6 w-full cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className={cn("toggle")}
|
||||
{...form.register("showOnChangelogPage", {})}
|
||||
/>
|
||||
<span className={cn("label-text w-full text-left")}>
|
||||
Auf der Changelog-Seite anzeigen
|
||||
</span>
|
||||
</label>
|
||||
<div className="card-body">
|
||||
<div className="flex w-full gap-4">
|
||||
<Button
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"use client";
|
||||
import { DatabaseBackupIcon } from "lucide-react";
|
||||
import { Check, Cross, DatabaseBackupIcon } from "lucide-react";
|
||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||
import Link from "next/link";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Keyword, Prisma } from "@repo/db";
|
||||
import { Changelog, Keyword, Prisma } from "@repo/db";
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<>
|
||||
<PaginatedTable
|
||||
stickyHeaders
|
||||
initialOrderBy={[{ id: "title", desc: true }]}
|
||||
initialOrderBy={[{ id: "createdAt", desc: true }]}
|
||||
prismaModel="changelog"
|
||||
showSearch
|
||||
getFilter={(search) =>
|
||||
({
|
||||
OR: [
|
||||
{ title: { contains: search, mode: "insensitive" } },
|
||||
{ description: { contains: search, mode: "insensitive" } },
|
||||
{ text: { contains: search, mode: "insensitive" } },
|
||||
],
|
||||
}) as Prisma.ChangelogWhereInput
|
||||
}
|
||||
@@ -27,6 +27,16 @@ export default () => {
|
||||
header: "Title",
|
||||
accessorKey: "title",
|
||||
},
|
||||
{
|
||||
header: "Auf Changelog Seite anzeigen",
|
||||
accessorKey: "showOnChangelogPage",
|
||||
cell: ({ row }) => (row.original.showOnChangelogPage ? <Check /> : <Cross />),
|
||||
},
|
||||
{
|
||||
header: "Erstellt am",
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
header: "Aktionen",
|
||||
cell: ({ row }) => (
|
||||
@@ -37,7 +47,7 @@ export default () => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as ColumnDef<Keyword>[]
|
||||
] as ColumnDef<Changelog>[]
|
||||
}
|
||||
leftOfSearch={
|
||||
<span className="flex items-center gap-2">
|
||||
|
||||
@@ -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 = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="card-title mt-2">
|
||||
<DiscordLogoIcon className="h-5 w-5" /> Frühere Discord Accounts
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Avatar</th>
|
||||
<th>Benutzername</th>
|
||||
<th>Discord ID</th>
|
||||
<th>getrennt am</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{discordAccount && (
|
||||
<tr>
|
||||
<td>
|
||||
<Image
|
||||
src={`https://cdn.discordapp.com/avatars/${discordAccount.discordId}/${discordAccount.avatar}.png`}
|
||||
alt="Discord Avatar"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
</td>
|
||||
<td>{discordAccount.username}</td>
|
||||
<td>{discordAccount.discordId}</td>
|
||||
<td>N/A (Aktuell verbunden)</td>
|
||||
</tr>
|
||||
)}
|
||||
{formerDiscordAccounts.map((account) => (
|
||||
<tr key={account.discordId}>
|
||||
<td>
|
||||
{account.DiscordAccount && (
|
||||
<Image
|
||||
src={`https://cdn.discordapp.com/avatars/${account.DiscordAccount.discordId}/${account.DiscordAccount.avatar}.png`}
|
||||
alt="Discord Avatar"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>{account.DiscordAccount?.username || "Unbekannt"}</td>
|
||||
<td>{account.DiscordAccount?.discordId || "Unbekannt"}</td>
|
||||
<td>{new Date(account.removedAt).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!discordAccount && formerDiscordAccounts.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center text-gray-400">
|
||||
Keine Discord Accounts verknüpft
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="card-title">
|
||||
<ChartBarBigIcon className="h-5 w-5" /> Aktivität
|
||||
|
||||
@@ -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 <Error statusCode={404} title="User not found" />;
|
||||
|
||||
const dispoSessions = await prisma.connectedDispatcher.findMany({
|
||||
@@ -121,11 +142,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<AdminForm
|
||||
formerDiscordAccounts={formerDiscordAccounts}
|
||||
user={user}
|
||||
dispoTime={dispoTime}
|
||||
pilotTime={pilotTime}
|
||||
reports={reports}
|
||||
discordAccount={user.discordAccounts[0]}
|
||||
discordAccount={user.DiscordAccount ?? undefined}
|
||||
openBans={openBans}
|
||||
openTimebans={openTimeban}
|
||||
/>
|
||||
|
||||
@@ -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 <span className="font-bold text-red-600">AKTIVE STRAFE</span>;
|
||||
}
|
||||
if (props.row.original.permissions.length === 0) {
|
||||
return <span className="text-gray-700">Keine</span>;
|
||||
} 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 (
|
||||
<span className="w-full text-center">
|
||||
{penaltyCount} / {reportCount}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Discord",
|
||||
cell(props) {
|
||||
const discord = props.row.original.discordAccounts;
|
||||
if (discord.length === 0) {
|
||||
const discord = props.row.original.DiscordAccount;
|
||||
if (!discord) {
|
||||
return <span className="text-gray-700">Nicht verbunden</span>;
|
||||
}
|
||||
return <span>{discord.map((d) => d.username).join(", ")}</span>;
|
||||
return <span>{discord.username}</span>;
|
||||
},
|
||||
},
|
||||
...(session?.user.permissions.includes("ADMIN_USER_ADVANCED")
|
||||
@@ -97,7 +117,13 @@ const AdminUserPage = () => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as ColumnDef<User & { discordAccounts: DiscordAccount[] }>[]
|
||||
] as ColumnDef<
|
||||
User & {
|
||||
DiscordAccount: DiscordAccount;
|
||||
ReceivedReports: Report[];
|
||||
Penaltys: Penalty[];
|
||||
}
|
||||
>[]
|
||||
} // Define the columns for the user table
|
||||
leftOfSearch={
|
||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
||||
|
||||
68
apps/hub/app/(app)/changelog/_components/Timeline.tsx
Normal file
68
apps/hub/app/(app)/changelog/_components/Timeline.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import Image from "next/image";
|
||||
|
||||
export type TimelineEntry = {
|
||||
id: number;
|
||||
title: string;
|
||||
text: string;
|
||||
previewImage?: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const formatReleaseDate = (value: string) =>
|
||||
new Intl.DateTimeFormat("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}).format(new Date(value));
|
||||
|
||||
export const ChangelogTimeline = ({ entries }: { entries: TimelineEntry[] }) => {
|
||||
if (!entries.length)
|
||||
return <p className="text-base-content/70">Es sind noch keine Changelog-Einträge vorhanden.</p>;
|
||||
|
||||
return (
|
||||
<div className="relative mt-6 pl-6">
|
||||
<div className="bg-base-300 absolute bottom-0 left-2 top-0 w-px" aria-hidden />
|
||||
<div className="space-y-8">
|
||||
{entries.map((entry, idx) => (
|
||||
<article key={entry.id ?? `${entry.title}-${idx}`} className="relative pl-4">
|
||||
<div className="bg-primary ring-base-100 absolute -left-[9px] top-3 h-4 w-4 rounded-full ring-4" />
|
||||
<div className="bg-base-200/80 rounded-xl p-5 shadow">
|
||||
<div className="flex flex-col gap-1 text-left md:flex-row md:justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold leading-tight">{entry.title}</h3>
|
||||
<p className="text-base-content/60 text-sm">
|
||||
Release Date: {formatReleaseDate(entry.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
{entry.previewImage && (
|
||||
<div className="absolute right-5 top-5 md:pl-4">
|
||||
<Image
|
||||
src={entry.previewImage}
|
||||
width={300}
|
||||
height={300}
|
||||
alt={`${entry.title} preview`}
|
||||
className="mt-3 max-w-[300px] rounded-lg object-cover md:mt-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-base-content/80 text-left" data-color-mode="dark">
|
||||
<MDEditor.Markdown
|
||||
source={entry.text}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
fontSize: "0.95rem",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
apps/hub/app/(app)/changelog/page.tsx
Normal file
26
apps/hub/app/(app)/changelog/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { prisma } from "@repo/db";
|
||||
import { ChangelogTimeline } from "./_components/Timeline";
|
||||
import { ActivityLogIcon } from "@radix-ui/react-icons";
|
||||
|
||||
export default async function Page() {
|
||||
const changelog = await prisma.changelog.findMany({
|
||||
where: { showOnChangelogPage: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
const entries = changelog.map((entry) => ({
|
||||
...entry,
|
||||
createdAt: entry.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full px-4">
|
||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
||||
<ActivityLogIcon className="h-5 w-5" /> Changelog
|
||||
</p>
|
||||
</div>
|
||||
<ChangelogTimeline entries={entries} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 = ({
|
||||
</h2>
|
||||
<div>
|
||||
<div>
|
||||
{discordAccount ? (
|
||||
{discordAccount && canUnlinkDiscord ? (
|
||||
<Button
|
||||
className="btn-success btn-block btn-outline hover:btn-error group transition-all duration-0"
|
||||
isLoading={isLoading}
|
||||
@@ -329,14 +332,13 @@ export const SocialForm = ({
|
||||
export const DeleteForm = ({
|
||||
user,
|
||||
penaltys,
|
||||
reports,
|
||||
}: {
|
||||
user: User;
|
||||
penaltys: Penalty[];
|
||||
reports: Report[];
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const userCanDelete = penaltys.length === 0 && !user.isBanned && reports.length === 0;
|
||||
const userCanDelete = penaltys.length === 0 && !user.isBanned;
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title mb-5">
|
||||
@@ -344,11 +346,11 @@ export const DeleteForm = ({
|
||||
</h2>
|
||||
{!userCanDelete && (
|
||||
<div className="text-left">
|
||||
<h2 className="text-warning text-lg">Du kannst dein Konto zurzeit nicht löschen!</h2>
|
||||
<h2 className="text-warning text-lg">Du kannst dein Konto nicht löschen!</h2>
|
||||
<p className="text-sm text-gray-400">
|
||||
Scheinbar hast du Strafen oder Reports in deinem Profil hinterlegt. Um unsere Community
|
||||
zu schützen kannst du deinen Account nicht löschen. Bitte erstelle ein Support-Ticket,
|
||||
wenn du Fragen dazu hast.
|
||||
Da du Strafen hast oder hattest, kannst du deinen Account nicht löschen. Um unsere
|
||||
Community zu schützen kannst du deinen Account nicht löschen. Bitte erstelle ein
|
||||
Support-Ticket, wenn du Fragen dazu hast.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,9 +4,19 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export const unlinkDiscord = async (userId: string) => {
|
||||
await prisma.discordAccount.deleteMany({
|
||||
const discordAccount = await prisma.discordAccount.update({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
data: {
|
||||
userId: null,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.formerDiscordAccount.create({
|
||||
data: {
|
||||
userId,
|
||||
discordId: discordAccount.discordId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||
import { ProfileForm, SocialForm, PasswordForm, DeleteForm } from "./_components/forms";
|
||||
import { GearIcon } from "@radix-ui/react-icons";
|
||||
import { Error } from "_components/Error";
|
||||
import { getUserPenaltys } from "@repo/shared-components";
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getServerSession();
|
||||
@@ -13,15 +14,17 @@ export default async function Page() {
|
||||
id: session.user.id,
|
||||
},
|
||||
include: {
|
||||
discordAccounts: true,
|
||||
DiscordAccount: true,
|
||||
Penaltys: true,
|
||||
},
|
||||
});
|
||||
const userPenaltys = await prisma.penalty.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
suspended: false,
|
||||
},
|
||||
});
|
||||
const activePenaltys = await getUserPenaltys(session.user.id);
|
||||
|
||||
const userReports = await prisma.report.findMany({
|
||||
where: {
|
||||
@@ -30,7 +33,7 @@ export default async function Page() {
|
||||
});
|
||||
|
||||
if (!user) return <Error statusCode={401} title="Dein Account wurde nicht gefunden" />;
|
||||
const discordAccount = user?.discordAccounts[0];
|
||||
const discordAccount = user?.DiscordAccount;
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-full">
|
||||
@@ -39,16 +42,24 @@ export default async function Page() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<ProfileForm user={user} penaltys={userPenaltys} discordAccount={discordAccount} />
|
||||
<ProfileForm
|
||||
user={user}
|
||||
discordAccount={discordAccount}
|
||||
penaltys={[...activePenaltys.openBans, ...activePenaltys.openTimeban]}
|
||||
/>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<SocialForm discordAccount={discordAccount} user={user} />
|
||||
<SocialForm
|
||||
user={user}
|
||||
discordAccount={discordAccount}
|
||||
penaltys={[...activePenaltys.openBans, ...activePenaltys.openTimeban]}
|
||||
/>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<PasswordForm />
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<DeleteForm user={user} penaltys={userPenaltys} reports={userReports} />
|
||||
<DeleteForm user={user} reports={userReports} penaltys={userPenaltys} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { deleteBookingAPI, getBookingsAPI } from "(app)/_querys/bookings";
|
||||
import { Button } from "@repo/shared-components";
|
||||
import { formatTimeRange } from "../../helper/timerange";
|
||||
import toast from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
|
||||
interface BookingTimelineModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -298,7 +299,17 @@ export const BookingTimelineModal = ({
|
||||
? "LST"
|
||||
: booking.Station.bosCallsignShort || booking.Station.bosCallsign}
|
||||
</span>
|
||||
|
||||
{currentUser?.permissions.includes("ADMIN_USER") ? (
|
||||
<Link
|
||||
href={`/admin/user/${booking.User.publicId}`}
|
||||
className="link link-hover text-xs opacity-70"
|
||||
>
|
||||
{booking.User.fullName}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-sm font-medium">{booking.User.fullName}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-right">
|
||||
|
||||
@@ -6,13 +6,15 @@ import {
|
||||
RocketIcon,
|
||||
ReaderIcon,
|
||||
DownloadIcon,
|
||||
UpdateIcon,
|
||||
ActivityLogIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
import { WarningAlert } from "./ui/PageAlert";
|
||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||
import { Error } from "./Error";
|
||||
import Image from "next/image";
|
||||
import { Plane, Radar, Workflow } from "lucide-react";
|
||||
import { Loader, Plane, Radar, Workflow } from "lucide-react";
|
||||
import { BookingButton } from "./BookingButton";
|
||||
|
||||
export const VerticalNav = async () => {
|
||||
@@ -22,7 +24,8 @@ export const VerticalNav = async () => {
|
||||
return p.startsWith("ADMIN");
|
||||
});
|
||||
return (
|
||||
<ul className="menu bg-base-300 w-64 flex-nowrap rounded-lg p-3 font-semibold shadow-md">
|
||||
<ul className="menu bg-base-300 flex w-64 flex-nowrap justify-between rounded-lg p-3 font-semibold shadow-md">
|
||||
<div className="border-none">
|
||||
<li>
|
||||
<Link href="/">
|
||||
<HomeIcon /> Dashboard
|
||||
@@ -109,6 +112,13 @@ export const VerticalNav = async () => {
|
||||
</details>
|
||||
</li>
|
||||
)}
|
||||
</div>
|
||||
<li>
|
||||
<Link href="/changelog">
|
||||
<ActivityLogIcon />
|
||||
Changelog
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,13 +49,13 @@ export default function SortableTable<TData>({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-zebra w-full">
|
||||
<table className="table-zebra table w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
|
||||
<div className="flex items-center gap-1 cursor-pointer">
|
||||
<div className="flex cursor-pointer items-center gap-1">
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.column.getIsSorted() === "asc" && <ChevronUp size={16} />}
|
||||
{header.column.getIsSorted() === "desc" && <ChevronDown size={16} />}
|
||||
@@ -75,7 +75,7 @@ export default function SortableTable<TData>({
|
||||
))}
|
||||
{table.getRowModel().rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center font-bold text-sm text-gray-500">
|
||||
<td colSpan={columns.length} className="text-center text-sm font-bold text-gray-500">
|
||||
Keine Daten gefunden
|
||||
</td>
|
||||
</tr>
|
||||
@@ -104,6 +104,8 @@ export const RowsPerPage = ({
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={300}>300</option>
|
||||
<option value={1000}>1000</option>
|
||||
<option value={5000}>5000</option>
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { DiscordAccount, prisma } from "@repo/db";
|
||||
import { getServerSession } from "../auth/[...nextauth]/auth";
|
||||
import { setStandardName } from "../../../helper/discord";
|
||||
import { getUserPenaltys } from "@repo/shared-components";
|
||||
import { markDuplicate } from "(app)/admin/user/action";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const session = await getServerSession();
|
||||
@@ -77,6 +79,29 @@ export const GET = async (req: NextRequest) => {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
const formerDiscordAccount = await prisma.formerDiscordAccount.findMany({
|
||||
where: {
|
||||
discordId: discordUser.id,
|
||||
userId: {
|
||||
not: session.user.id,
|
||||
},
|
||||
User: {
|
||||
canonicalUserId: null,
|
||||
},
|
||||
},
|
||||
include: { User: true },
|
||||
});
|
||||
|
||||
// Account is suspicious to multiaccounting
|
||||
if (formerDiscordAccount.length > 0) {
|
||||
formerDiscordAccount.forEach(async (account) => {
|
||||
await markDuplicate({
|
||||
duplicateUserId: session.user.id,
|
||||
canonicalPublicId: account.User!.publicId,
|
||||
reason: "Multiaccounting Verdacht, gleicher Discord Account wie ein anderer Nutzer.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_HUB_URL}/settings`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
/* const removeImports = require("next-remove-imports")(); */
|
||||
/* const nextConfig = removeImports({}); */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: ["cdn.discordapp.com", "nextcloud.virtualairrescue.com"],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"schema": "./prisma/schema/"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "npx prisma generate && npx prisma generate zod",
|
||||
"migrate": "npx prisma migrate dev",
|
||||
"deploy": "npx prisma migrate deploy",
|
||||
"dev": "npx prisma studio --browser none"
|
||||
"generate": "npx prisma@6.12.0 generate && npx prisma@6.12.0 generate zod",
|
||||
"migrate": "npx prisma@6.12.0 migrate dev",
|
||||
"deploy": "npx prisma@6.12.0 migrate deploy",
|
||||
"dev": "npx prisma@6.12.0 studio --browser none"
|
||||
},
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
|
||||
@@ -4,4 +4,5 @@ model Changelog {
|
||||
previewImage String?
|
||||
text String
|
||||
createdAt DateTime @default(now())
|
||||
showOnChangelogPage Boolean @default(true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `user_id` on the `discord_accounts` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "discord_accounts" DROP CONSTRAINT "discord_accounts_user_id_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "discord_accounts" RENAME COLUMN "user_id" TO "userId";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "former_discord_accounts" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"discord_id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"removed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "former_discord_accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "former_discord_accounts_discord_id_key" ON "former_discord_accounts"("discord_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "former_discord_accounts" ADD CONSTRAINT "former_discord_accounts_discord_id_fkey" FOREIGN KEY ("discord_id") REFERENCES "discord_accounts"("discord_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "former_discord_accounts" ADD CONSTRAINT "former_discord_accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "discord_accounts" ADD CONSTRAINT "discord_accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `former_discord_accounts` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "former_discord_accounts" DROP CONSTRAINT "former_discord_accounts_discord_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "former_discord_accounts" DROP CONSTRAINT "former_discord_accounts_user_id_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "discord_accounts" ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "former_discord_accounts";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "FormerDiscordAccount" (
|
||||
"discord_id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"removed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "FormerDiscordAccount_pkey" PRIMARY KEY ("discord_id","user_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "FormerDiscordAccount_discord_id_key" ON "FormerDiscordAccount"("discord_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FormerDiscordAccount" ADD CONSTRAINT "FormerDiscordAccount_discord_id_fkey" FOREIGN KEY ("discord_id") REFERENCES "discord_accounts"("discord_id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "FormerDiscordAccount" ADD CONSTRAINT "FormerDiscordAccount_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[userId]` on the table `discord_accounts` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "discord_accounts_userId_key" ON "discord_accounts"("userId");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Changelog" ADD COLUMN "showOnChangelogPage" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -68,7 +68,6 @@ model User {
|
||||
duplicateReason String? @map(name: "duplicate_reason")
|
||||
// relations:
|
||||
oauthTokens OAuthToken[]
|
||||
discordAccounts DiscordAccount[]
|
||||
participants Participant[]
|
||||
EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser")
|
||||
EventAppointment EventAppointment[]
|
||||
@@ -86,13 +85,26 @@ model User {
|
||||
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
||||
Bookings Booking[]
|
||||
|
||||
DiscordAccount DiscordAccount?
|
||||
FormerDiscordAccounts FormerDiscordAccount[]
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model FormerDiscordAccount {
|
||||
discordId String @unique @map(name: "discord_id")
|
||||
userId String @map(name: "user_id")
|
||||
removedAt DateTime @default(now()) @map(name: "removed_at")
|
||||
|
||||
DiscordAccount DiscordAccount? @relation(fields: [discordId], references: [discordId], onDelete: SetNull)
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([discordId, userId])
|
||||
}
|
||||
|
||||
model DiscordAccount {
|
||||
id Int @id @default(autoincrement())
|
||||
discordId String @unique @map(name: "discord_id")
|
||||
userId String @map(name: "user_id")
|
||||
email String @map(name: "email")
|
||||
username String @map(name: "username")
|
||||
avatar String? @map(name: "avatar")
|
||||
@@ -104,8 +116,10 @@ model DiscordAccount {
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
// relations:
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade) // Beziehung zu User
|
||||
// Related User
|
||||
userId String? @unique
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
formerDiscordAccount FormerDiscordAccount?
|
||||
|
||||
@@map(name: "discord_accounts")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user