v2.0.6 #142
@@ -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 { io } from "index";
|
||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
|
import { changeMemberRoles } from "routes/member";
|
||||||
|
|
||||||
const removeMission = async (id: number, reason: string) => {
|
const removeMission = async (id: number, reason: string) => {
|
||||||
const log: MissionLog = {
|
const log: MissionLog = {
|
||||||
@@ -34,7 +35,6 @@ const removeMission = async (id: number, reason: string) => {
|
|||||||
|
|
||||||
console.log(`Mission ${updatedMission.id} closed due to inactivity.`);
|
console.log(`Mission ${updatedMission.id} closed due to inactivity.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeClosedMissions = async () => {
|
const removeClosedMissions = async () => {
|
||||||
const oldMissions = await prisma.mission.findMany({
|
const oldMissions = await prisma.mission.findMany({
|
||||||
where: {
|
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 () => {
|
cron.schedule("*/1 * * * *", async () => {
|
||||||
try {
|
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) => {
|
participant.forEach(async (p) => {
|
||||||
if (!p.Event.discordRoleId) return;
|
if (!p.Event.discordRoleId) return;
|
||||||
if (eventCompleted(p.Event, p)) {
|
if (eventCompleted(p.Event, p)) {
|
||||||
@@ -48,8 +67,12 @@ router.post("/set-standard-name", async (req, res) => {
|
|||||||
const isPilot = user.permissions.includes("PILOT");
|
const isPilot = user.permissions.includes("PILOT");
|
||||||
const isDispatcher = user.permissions.includes("DISPO");
|
const isDispatcher = user.permissions.includes("DISPO");
|
||||||
|
|
||||||
await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove");
|
if (activePenaltys.length > 0) {
|
||||||
await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove");
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ router.post("/handle-participant-finished", async (req, res) => {
|
|||||||
Event: true,
|
Event: true,
|
||||||
User: {
|
User: {
|
||||||
include: {
|
include: {
|
||||||
discordAccounts: true,
|
DiscordAccount: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -94,7 +94,7 @@ router.post("/handle-participant-enrolled", async (req, res) => {
|
|||||||
Event: true,
|
Event: true,
|
||||||
User: {
|
User: {
|
||||||
include: {
|
include: {
|
||||||
discordAccounts: true,
|
DiscordAccount: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const RecentFlights = () => {
|
|||||||
({
|
({
|
||||||
User: { id: session.data?.user.id },
|
User: { id: session.data?.user.id },
|
||||||
Mission: {
|
Mission: {
|
||||||
state: { in: ["finished", "archived"] },
|
state: { in: ["finished"] },
|
||||||
},
|
},
|
||||||
}) as Prisma.MissionOnStationUsersWhereInput
|
}) as Prisma.MissionOnStationUsersWhereInput
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
title: changelog?.title || "",
|
title: changelog?.title || "",
|
||||||
text: changelog?.text || "",
|
text: changelog?.text || "",
|
||||||
previewImage: changelog?.previewImage || "", // Changed to accept a URL as a string
|
previewImage: changelog?.previewImage || "", // Changed to accept a URL as a string
|
||||||
|
showOnChangelogPage: changelog?.showOnChangelogPage || true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [skipUserUpdate, setSkipUserUpdate] = useState(false);
|
const [skipUserUpdate, setSkipUserUpdate] = useState(false);
|
||||||
@@ -84,6 +85,7 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
placeholder="Titel (vX.X.X)"
|
placeholder="Titel (vX.X.X)"
|
||||||
className="input-sm"
|
className="input-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
form={form}
|
form={form}
|
||||||
label="Bild-URL"
|
label="Bild-URL"
|
||||||
@@ -146,6 +148,16 @@ export const ChangelogForm = ({ changelog }: { changelog?: Changelog }) => {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</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="card-body">
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { DatabaseBackupIcon } from "lucide-react";
|
import { Check, Cross, DatabaseBackupIcon } from "lucide-react";
|
||||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Keyword, Prisma } from "@repo/db";
|
import { Changelog, Keyword, Prisma } from "@repo/db";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
stickyHeaders
|
stickyHeaders
|
||||||
initialOrderBy={[{ id: "title", desc: true }]}
|
initialOrderBy={[{ id: "createdAt", desc: true }]}
|
||||||
prismaModel="changelog"
|
prismaModel="changelog"
|
||||||
showSearch
|
showSearch
|
||||||
getFilter={(search) =>
|
getFilter={(search) =>
|
||||||
({
|
({
|
||||||
OR: [
|
OR: [
|
||||||
{ title: { contains: search, mode: "insensitive" } },
|
{ title: { contains: search, mode: "insensitive" } },
|
||||||
{ description: { contains: search, mode: "insensitive" } },
|
{ text: { contains: search, mode: "insensitive" } },
|
||||||
],
|
],
|
||||||
}) as Prisma.ChangelogWhereInput
|
}) as Prisma.ChangelogWhereInput
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,16 @@ export default () => {
|
|||||||
header: "Title",
|
header: "Title",
|
||||||
accessorKey: "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",
|
header: "Aktionen",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@@ -37,7 +47,7 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
] as ColumnDef<Keyword>[]
|
] as ColumnDef<Changelog>[]
|
||||||
}
|
}
|
||||||
leftOfSearch={
|
leftOfSearch={
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ConnectedAircraft,
|
ConnectedAircraft,
|
||||||
ConnectedDispatcher,
|
ConnectedDispatcher,
|
||||||
DiscordAccount,
|
DiscordAccount,
|
||||||
|
FormerDiscordAccount,
|
||||||
Penalty,
|
Penalty,
|
||||||
PERMISSION,
|
PERMISSION,
|
||||||
Prisma,
|
Prisma,
|
||||||
@@ -60,6 +61,7 @@ import { penaltyColumns } from "(app)/admin/penalty/columns";
|
|||||||
import { addPenalty, editPenaltys } from "(app)/admin/penalty/actions";
|
import { addPenalty, editPenaltys } from "(app)/admin/penalty/actions";
|
||||||
import { reportColumns } from "(app)/admin/report/columns";
|
import { reportColumns } from "(app)/admin/report/columns";
|
||||||
import { sendMailByTemplate } from "../../../../../../helper/mail";
|
import { sendMailByTemplate } from "../../../../../../helper/mail";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
interface ProfileFormProps {
|
interface ProfileFormProps {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -566,6 +568,7 @@ interface AdminFormProps {
|
|||||||
minutes: number;
|
minutes: number;
|
||||||
lastLogin?: Date;
|
lastLogin?: Date;
|
||||||
};
|
};
|
||||||
|
formerDiscordAccounts: (FormerDiscordAccount & { DiscordAccount: DiscordAccount | null })[];
|
||||||
reports: {
|
reports: {
|
||||||
total: number;
|
total: number;
|
||||||
open: number;
|
open: number;
|
||||||
@@ -585,6 +588,7 @@ export const AdminForm = ({
|
|||||||
pilotTime,
|
pilotTime,
|
||||||
reports,
|
reports,
|
||||||
discordAccount,
|
discordAccount,
|
||||||
|
formerDiscordAccounts,
|
||||||
openBans,
|
openBans,
|
||||||
openTimebans,
|
openTimebans,
|
||||||
}: AdminFormProps) => {
|
}: AdminFormProps) => {
|
||||||
@@ -755,6 +759,64 @@ export const AdminForm = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<h2 className="card-title">
|
||||||
<ChartBarBigIcon className="h-5 w-5" /> Aktivität
|
<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 }> }) {
|
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const user = await prisma.user.findUnique({
|
let user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
discordAccounts: true,
|
DiscordAccount: true,
|
||||||
CanonicalUser: true,
|
CanonicalUser: true,
|
||||||
Duplicates: 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" />;
|
if (!user) return <Error statusCode={404} title="User not found" />;
|
||||||
|
|
||||||
const dispoSessions = await prisma.connectedDispatcher.findMany({
|
const dispoSessions = await prisma.connectedDispatcher.findMany({
|
||||||
@@ -121,11 +142,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||||
<AdminForm
|
<AdminForm
|
||||||
|
formerDiscordAccounts={formerDiscordAccounts}
|
||||||
user={user}
|
user={user}
|
||||||
dispoTime={dispoTime}
|
dispoTime={dispoTime}
|
||||||
pilotTime={pilotTime}
|
pilotTime={pilotTime}
|
||||||
reports={reports}
|
reports={reports}
|
||||||
discordAccount={user.discordAccounts[0]}
|
discordAccount={user.DiscordAccount ?? undefined}
|
||||||
openBans={openBans}
|
openBans={openBans}
|
||||||
openTimebans={openTimeban}
|
openTimebans={openTimeban}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { User2 } from "lucide-react";
|
|||||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
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";
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
const AdminUserPage = () => {
|
const AdminUserPage = () => {
|
||||||
@@ -21,16 +21,15 @@ const AdminUserPage = () => {
|
|||||||
{ firstname: { contains: searchTerm, mode: "insensitive" } },
|
{ firstname: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{ lastname: { contains: searchTerm, mode: "insensitive" } },
|
{ lastname: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{ email: { contains: searchTerm, mode: "insensitive" } },
|
{ email: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{
|
{ publicId: { contains: searchTerm, mode: "insensitive" } },
|
||||||
discordAccounts: {
|
{ DiscordAccount: { username: { contains: searchTerm, mode: "insensitive" } } },
|
||||||
some: { username: { contains: searchTerm, mode: "insensitive" } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as Prisma.UserWhereInput;
|
} as Prisma.UserWhereInput;
|
||||||
}}
|
}}
|
||||||
include={{
|
include={{
|
||||||
discordAccounts: true,
|
DiscordAccount: true,
|
||||||
|
ReceivedReports: true,
|
||||||
|
Penaltys: true,
|
||||||
}}
|
}}
|
||||||
initialOrderBy={[
|
initialOrderBy={[
|
||||||
{
|
{
|
||||||
@@ -55,6 +54,15 @@ const AdminUserPage = () => {
|
|||||||
{
|
{
|
||||||
header: "Berechtigungen",
|
header: "Berechtigungen",
|
||||||
cell(props) {
|
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) {
|
if (props.row.original.permissions.length === 0) {
|
||||||
return <span className="text-gray-700">Keine</span>;
|
return <span className="text-gray-700">Keine</span>;
|
||||||
} else if (props.row.original.permissions.includes("ADMIN_USER_ADVANCED")) {
|
} 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",
|
header: "Discord",
|
||||||
cell(props) {
|
cell(props) {
|
||||||
const discord = props.row.original.discordAccounts;
|
const discord = props.row.original.DiscordAccount;
|
||||||
if (discord.length === 0) {
|
if (!discord) {
|
||||||
return <span className="text-gray-700">Nicht verbunden</span>;
|
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")
|
...(session?.user.permissions.includes("ADMIN_USER_ADVANCED")
|
||||||
@@ -97,7 +117,13 @@ const AdminUserPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
] as ColumnDef<User & { discordAccounts: DiscordAccount[] }>[]
|
] as ColumnDef<
|
||||||
|
User & {
|
||||||
|
DiscordAccount: DiscordAccount;
|
||||||
|
ReceivedReports: Report[];
|
||||||
|
Penaltys: Penalty[];
|
||||||
|
}
|
||||||
|
>[]
|
||||||
} // Define the columns for the user table
|
} // Define the columns for the user table
|
||||||
leftOfSearch={
|
leftOfSearch={
|
||||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
<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;
|
user: User;
|
||||||
penaltys: Penalty[];
|
penaltys: Penalty[];
|
||||||
discordAccount?: DiscordAccount;
|
discordAccount: DiscordAccount | null;
|
||||||
}): React.JSX.Element => {
|
}): React.JSX.Element => {
|
||||||
const canEdit = penaltys.length === 0 && !user.isBanned;
|
const canEdit = penaltys.length === 0 && !user.isBanned;
|
||||||
|
|
||||||
@@ -215,9 +215,11 @@ export const ProfileForm = ({
|
|||||||
export const SocialForm = ({
|
export const SocialForm = ({
|
||||||
discordAccount,
|
discordAccount,
|
||||||
user,
|
user,
|
||||||
|
penaltys,
|
||||||
}: {
|
}: {
|
||||||
discordAccount?: DiscordAccount;
|
discordAccount: DiscordAccount | null;
|
||||||
user: User;
|
user: User;
|
||||||
|
penaltys: Penalty[];
|
||||||
}): React.JSX.Element | null => {
|
}): React.JSX.Element | null => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [vatsimLoading, setVatsimLoading] = useState(false);
|
const [vatsimLoading, setVatsimLoading] = useState(false);
|
||||||
@@ -235,6 +237,7 @@ export const SocialForm = ({
|
|||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
const canUnlinkDiscord = !user.isBanned && penaltys.length === 0;
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
return (
|
return (
|
||||||
@@ -262,7 +265,7 @@ export const SocialForm = ({
|
|||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{discordAccount ? (
|
{discordAccount && canUnlinkDiscord ? (
|
||||||
<Button
|
<Button
|
||||||
className="btn-success btn-block btn-outline hover:btn-error group transition-all duration-0"
|
className="btn-success btn-block btn-outline hover:btn-error group transition-all duration-0"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -329,14 +332,13 @@ export const SocialForm = ({
|
|||||||
export const DeleteForm = ({
|
export const DeleteForm = ({
|
||||||
user,
|
user,
|
||||||
penaltys,
|
penaltys,
|
||||||
reports,
|
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
penaltys: Penalty[];
|
penaltys: Penalty[];
|
||||||
reports: Report[];
|
reports: Report[];
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userCanDelete = penaltys.length === 0 && !user.isBanned && reports.length === 0;
|
const userCanDelete = penaltys.length === 0 && !user.isBanned;
|
||||||
return (
|
return (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title mb-5">
|
<h2 className="card-title mb-5">
|
||||||
@@ -344,11 +346,11 @@ export const DeleteForm = ({
|
|||||||
</h2>
|
</h2>
|
||||||
{!userCanDelete && (
|
{!userCanDelete && (
|
||||||
<div className="text-left">
|
<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">
|
<p className="text-sm text-gray-400">
|
||||||
Scheinbar hast du Strafen oder Reports in deinem Profil hinterlegt. Um unsere Community
|
Da du Strafen hast oder hattest, kannst du deinen Account nicht löschen. Um unsere
|
||||||
zu schützen kannst du deinen Account nicht löschen. Bitte erstelle ein Support-Ticket,
|
Community zu schützen kannst du deinen Account nicht löschen. Bitte erstelle ein
|
||||||
wenn du Fragen dazu hast.
|
Support-Ticket, wenn du Fragen dazu hast.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,9 +4,19 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
export const unlinkDiscord = async (userId: string) => {
|
export const unlinkDiscord = async (userId: string) => {
|
||||||
await prisma.discordAccount.deleteMany({
|
const discordAccount = await prisma.discordAccount.update({
|
||||||
where: {
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
userId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.formerDiscordAccount.create({
|
||||||
|
data: {
|
||||||
userId,
|
userId,
|
||||||
|
discordId: discordAccount.discordId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
|||||||
import { ProfileForm, SocialForm, PasswordForm, DeleteForm } from "./_components/forms";
|
import { ProfileForm, SocialForm, PasswordForm, DeleteForm } from "./_components/forms";
|
||||||
import { GearIcon } from "@radix-ui/react-icons";
|
import { GearIcon } from "@radix-ui/react-icons";
|
||||||
import { Error } from "_components/Error";
|
import { Error } from "_components/Error";
|
||||||
|
import { getUserPenaltys } from "@repo/shared-components";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
@@ -13,15 +14,17 @@ export default async function Page() {
|
|||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
discordAccounts: true,
|
DiscordAccount: true,
|
||||||
Penaltys: true,
|
Penaltys: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const userPenaltys = await prisma.penalty.findMany({
|
const userPenaltys = await prisma.penalty.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
|
suspended: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const activePenaltys = await getUserPenaltys(session.user.id);
|
||||||
|
|
||||||
const userReports = await prisma.report.findMany({
|
const userReports = await prisma.report.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -30,7 +33,7 @@ export default async function Page() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return <Error statusCode={401} title="Dein Account wurde nicht gefunden" />;
|
if (!user) return <Error statusCode={401} title="Dein Account wurde nicht gefunden" />;
|
||||||
const discordAccount = user?.discordAccounts[0];
|
const discordAccount = user?.DiscordAccount;
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
@@ -39,16 +42,24 @@ export default async function Page() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
<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>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
<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>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||||
<PasswordForm />
|
<PasswordForm />
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { deleteBookingAPI, getBookingsAPI } from "(app)/_querys/bookings";
|
|||||||
import { Button } from "@repo/shared-components";
|
import { Button } from "@repo/shared-components";
|
||||||
import { formatTimeRange } from "../../helper/timerange";
|
import { formatTimeRange } from "../../helper/timerange";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface BookingTimelineModalProps {
|
interface BookingTimelineModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -298,7 +299,17 @@ export const BookingTimelineModal = ({
|
|||||||
? "LST"
|
? "LST"
|
||||||
: booking.Station.bosCallsignShort || booking.Station.bosCallsign}
|
: booking.Station.bosCallsignShort || booking.Station.bosCallsign}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium">{booking.User.fullName}</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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import {
|
|||||||
RocketIcon,
|
RocketIcon,
|
||||||
ReaderIcon,
|
ReaderIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
UpdateIcon,
|
||||||
|
ActivityLogIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { WarningAlert } from "./ui/PageAlert";
|
import { WarningAlert } from "./ui/PageAlert";
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
import { Error } from "./Error";
|
import { Error } from "./Error";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Plane, Radar, Workflow } from "lucide-react";
|
import { Loader, Plane, Radar, Workflow } from "lucide-react";
|
||||||
import { BookingButton } from "./BookingButton";
|
import { BookingButton } from "./BookingButton";
|
||||||
|
|
||||||
export const VerticalNav = async () => {
|
export const VerticalNav = async () => {
|
||||||
@@ -22,93 +24,101 @@ export const VerticalNav = async () => {
|
|||||||
return p.startsWith("ADMIN");
|
return p.startsWith("ADMIN");
|
||||||
});
|
});
|
||||||
return (
|
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">
|
||||||
<li>
|
<div className="border-none">
|
||||||
<Link href="/">
|
|
||||||
<HomeIcon /> Dashboard
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/events">
|
|
||||||
<RocketIcon />
|
|
||||||
Events & Kurse
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/logbook">
|
|
||||||
<ReaderIcon />
|
|
||||||
Einsatzhistorie
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/settings">
|
|
||||||
<GearIcon />
|
|
||||||
Einstellungen
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/resources">
|
|
||||||
<DownloadIcon />
|
|
||||||
Downloads / Links
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{viewAdminMenu && (
|
|
||||||
<li>
|
<li>
|
||||||
<details open>
|
<Link href="/">
|
||||||
<summary>
|
<HomeIcon /> Dashboard
|
||||||
<LockClosedIcon />
|
</Link>
|
||||||
Admin
|
|
||||||
</summary>
|
|
||||||
<ul>
|
|
||||||
{session.user.permissions.includes("ADMIN_USER") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/user">Benutzer</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_STATION") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/station">Stationen</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_KEYWORD") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/keyword">Stichworte</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_HELIPORT") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/heliport">Heliports</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_EVENT") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/event">Events</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_MESSAGE") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/config">Config</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_USER") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/report">Reports</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_USER") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/penalty">Audit-Log</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{session.user.permissions.includes("ADMIN_CHANGELOG") && (
|
|
||||||
<li>
|
|
||||||
<Link href="/admin/changelog">Changelog</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</li>
|
</li>
|
||||||
)}
|
<li>
|
||||||
|
<Link href="/events">
|
||||||
|
<RocketIcon />
|
||||||
|
Events & Kurse
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/logbook">
|
||||||
|
<ReaderIcon />
|
||||||
|
Einsatzhistorie
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/settings">
|
||||||
|
<GearIcon />
|
||||||
|
Einstellungen
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/resources">
|
||||||
|
<DownloadIcon />
|
||||||
|
Downloads / Links
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{viewAdminMenu && (
|
||||||
|
<li>
|
||||||
|
<details open>
|
||||||
|
<summary>
|
||||||
|
<LockClosedIcon />
|
||||||
|
Admin
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
{session.user.permissions.includes("ADMIN_USER") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/user">Benutzer</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_STATION") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/station">Stationen</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_KEYWORD") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/keyword">Stichworte</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_HELIPORT") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/heliport">Heliports</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_EVENT") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/event">Events</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_MESSAGE") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/config">Config</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_USER") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/report">Reports</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_USER") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/penalty">Audit-Log</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{session.user.permissions.includes("ADMIN_CHANGELOG") && (
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/changelog">Changelog</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<li>
|
||||||
|
<Link href="/changelog">
|
||||||
|
<ActivityLogIcon />
|
||||||
|
Changelog
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ export default function SortableTable<TData>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="table table-zebra w-full">
|
<table className="table-zebra table w-full">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
|
<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())}
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
{header.column.getIsSorted() === "asc" && <ChevronUp size={16} />}
|
{header.column.getIsSorted() === "asc" && <ChevronUp size={16} />}
|
||||||
{header.column.getIsSorted() === "desc" && <ChevronDown size={16} />}
|
{header.column.getIsSorted() === "desc" && <ChevronDown size={16} />}
|
||||||
@@ -75,7 +75,7 @@ export default function SortableTable<TData>({
|
|||||||
))}
|
))}
|
||||||
{table.getRowModel().rows.length === 0 && (
|
{table.getRowModel().rows.length === 0 && (
|
||||||
<tr>
|
<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
|
Keine Daten gefunden
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -104,6 +104,8 @@ export const RowsPerPage = ({
|
|||||||
<option value={50}>50</option>
|
<option value={50}>50</option>
|
||||||
<option value={100}>100</option>
|
<option value={100}>100</option>
|
||||||
<option value={300}>300</option>
|
<option value={300}>300</option>
|
||||||
|
<option value={1000}>1000</option>
|
||||||
|
<option value={5000}>5000</option>
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { DiscordAccount, prisma } from "@repo/db";
|
import { DiscordAccount, 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 { markDuplicate } from "(app)/admin/user/action";
|
||||||
|
|
||||||
export const GET = async (req: NextRequest) => {
|
export const GET = async (req: NextRequest) => {
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
@@ -77,6 +79,29 @@ export const GET = async (req: NextRequest) => {
|
|||||||
userId: user.id,
|
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`);
|
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_HUB_URL}/settings`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
/* const removeImports = require("next-remove-imports")(); */
|
/* const removeImports = require("next-remove-imports")(); */
|
||||||
/* const nextConfig = removeImports({}); */
|
/* const nextConfig = removeImports({}); */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
domains: ["cdn.discordapp.com", "nextcloud.virtualairrescue.com"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
"schema": "./prisma/schema/"
|
"schema": "./prisma/schema/"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate": "npx prisma generate && npx prisma generate zod",
|
"generate": "npx prisma@6.12.0 generate && npx prisma@6.12.0 generate zod",
|
||||||
"migrate": "npx prisma migrate dev",
|
"migrate": "npx prisma@6.12.0 migrate dev",
|
||||||
"deploy": "npx prisma migrate deploy",
|
"deploy": "npx prisma@6.12.0 migrate deploy",
|
||||||
"dev": "npx prisma studio --browser none"
|
"dev": "npx prisma@6.12.0 studio --browser none"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
model Changelog {
|
model Changelog {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title String
|
||||||
previewImage String?
|
previewImage String?
|
||||||
text String
|
text String
|
||||||
createdAt DateTime @default(now())
|
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")
|
duplicateReason String? @map(name: "duplicate_reason")
|
||||||
// relations:
|
// relations:
|
||||||
oauthTokens OAuthToken[]
|
oauthTokens OAuthToken[]
|
||||||
discordAccounts DiscordAccount[]
|
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser")
|
EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser")
|
||||||
EventAppointment EventAppointment[]
|
EventAppointment EventAppointment[]
|
||||||
@@ -86,13 +85,26 @@ model User {
|
|||||||
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
||||||
Bookings Booking[]
|
Bookings Booking[]
|
||||||
|
|
||||||
|
DiscordAccount DiscordAccount?
|
||||||
|
FormerDiscordAccounts FormerDiscordAccount[]
|
||||||
|
|
||||||
@@map(name: "users")
|
@@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 {
|
model DiscordAccount {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
discordId String @unique @map(name: "discord_id")
|
discordId String @unique @map(name: "discord_id")
|
||||||
userId String @map(name: "user_id")
|
|
||||||
email String @map(name: "email")
|
email String @map(name: "email")
|
||||||
username String @map(name: "username")
|
username String @map(name: "username")
|
||||||
avatar String? @map(name: "avatar")
|
avatar String? @map(name: "avatar")
|
||||||
@@ -104,8 +116,10 @@ model DiscordAccount {
|
|||||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||||
|
|
||||||
// relations:
|
// Related User
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade) // Beziehung zu User
|
userId String? @unique
|
||||||
|
User User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
|
formerDiscordAccount FormerDiscordAccount?
|
||||||
|
|
||||||
@@map(name: "discord_accounts")
|
@@map(name: "discord_accounts")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user