Einstellungen sind eingeschränkt wenn Nutzer gebannt ist

This commit is contained in:
PxlLoewe
2025-06-25 18:07:05 -07:00
parent 577a18d595
commit b95319d61e
6 changed files with 191 additions and 68 deletions

View File

@@ -14,7 +14,13 @@ import {
} from "@repo/db"; } from "@repo/db";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { deleteDispoHistory, deletePilotHistory, editUser, resetPassword } from "../../action"; import {
deleteDispoHistory,
deletePilotHistory,
deleteUser,
editUser,
resetPassword,
} from "../../action";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { import {
PersonIcon, PersonIcon,
@@ -41,6 +47,7 @@ import {
PlaneIcon, PlaneIcon,
RedoDot, RedoDot,
Timer, Timer,
Trash2,
X, X,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -475,6 +482,7 @@ export const AdminForm = ({
discordAccount, discordAccount,
}: AdminFormProps) => { }: AdminFormProps) => {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession();
return ( return (
<div className="card-body"> <div className="card-body">
@@ -502,6 +510,22 @@ export const AdminForm = ({
> >
<LockOpen1Icon /> Passwort zurücksetzen <LockOpen1Icon /> Passwort zurücksetzen
</Button> </Button>
{session?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<div
className="tooltip flex-1 min-w-[250px] tooltip-warning"
data-tip="Dies löscht den Nutzer sofort, außerdem alle Reports, Verbindungs-Verlauf und Chat-Nachrichten"
>
<Button
className="btn-error btn-outline btn-sm w-full"
onClick={async () => {
await deleteUser(user.id);
router.push("/admin/user");
}}
>
<Trash2 size={15} /> Nutzer löschen
</Button>
</div>
)}
{user.isBanned && ( {user.isBanned && (
<Button <Button
onClick={async () => { onClick={async () => {

View File

@@ -50,8 +50,15 @@ export const deletePilotHistory = async (id: number) => {
}, },
}); });
}; };
export const deleteUser = async (id: string) => {
return await prisma.user.delete({
where: {
id: id,
},
});
};
export const CheckEmailCode = async (code: string) => { export const checkEmailCode = async (code: string) => {
const users = await prisma.user.findMany({ const users = await prisma.user.findMany({
where: { where: {
emailVerificationToken: code, emailVerificationToken: code,

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { DiscordAccount, User } from "@repo/db"; import { DiscordAccount, Penalty, User } from "@repo/db";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -21,11 +21,19 @@ import {
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod"; import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod";
import { Bell, Plane } from "lucide-react"; import { Bell, CircleAlert, Plane, Trash2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { sendVerificationLink } from "(app)/admin/user/action"; import { deleteUser, sendVerificationLink } from "(app)/admin/user/action";
export const ProfileForm = ({
user,
penaltys,
}: {
user: User;
penaltys: Penalty[];
}): React.JSX.Element => {
const canEdit = penaltys.length === 0 && user.isBanned;
export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
const schema = z.object({ const schema = z.object({
firstname: z.string().min(2).max(30), firstname: z.string().min(2).max(30),
lastname: z.string().min(2).max(30), lastname: z.string().min(2).max(30),
@@ -59,7 +67,6 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
console.log(form.formState.errors);
return ( return (
<form <form
className="card-body" className="card-body"
@@ -70,7 +77,7 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
if (user.email !== values.email) { if (user.email !== values.email) {
await sendVerificationLink(user.id); await sendVerificationLink(user.id);
toast.success( toast.success(
"Deine E-Mail Addresse hat sich geändert, wir haben die einen Link gesendet!", "Deine E-Mail Addresse hat sich geändert, wir haben dir einen Link gesendet!",
); );
} else { } else {
toast.success("Deine Änderungen wurden gespeichert!", { toast.success("Deine Änderungen wurden gespeichert!", {
@@ -87,60 +94,81 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
<MixerHorizontalIcon className="w-5 h-5" /> Persönliche Informationen <MixerHorizontalIcon className="w-5 h-5" /> Persönliche Informationen
</h2> </h2>
<div className="text-left"> <div className="text-left">
<label className="floating-label w-full mb-5 mt-5"> {!canEdit && (
<span className="text-lg flex items-center gap-2"> <div className="text-left">
<PersonIcon /> Vorname <h2 className="text-lg text-warning card-title">
</span> Du kannst deine Stammdaten nicht bearbeiten!
<input </h2>
{...form.register("firstname")} <p className="text-sm text-gray-400">
type="text" Scheinbar hast du aktuell aktive Strafen oder dein Account ist gesperrt. Um unsere
className="input input-bordered w-full" Community zu schützen, kannst du deine persönlichen Informationen erst bearbeiten,
defaultValue={user.firstname} wenn keine Strafen mehr aktiv sind und dein Account nicht gesperrt ist.
placeholder="Vorname" </p>
/> </div>
</label>
{form.formState.errors.firstname && (
<p className="text-error">{form.formState.errors.firstname.message}</p>
)} )}
<label className="floating-label w-full mb-5"> {canEdit && (
<span className="text-lg flex items-center gap-2"> <>
<PersonIcon /> Nachname <label className="floating-label w-full mb-5 mt-5">
</span> <span className="text-lg flex items-center gap-2">
<input <PersonIcon /> Vorname
{...form.register("lastname")} </span>
type="text" <input
className="input input-bordered w-full" {...form.register("firstname")}
defaultValue={user.lastname} type="text"
placeholder="Nachname" defaultValue={user.firstname}
/> placeholder="Vorname"
</label> className="input input-bordered w-full"
{form.formState.errors.lastname && ( />
<p className="text-error">{form.formState.errors.lastname?.message}</p> </label>
)} {form.formState.errors.firstname && (
<label className="label"> <p className="text-error">{form.formState.errors.firstname.message}</p>
<input type="checkbox" {...form.register("settingsHideLastname")} className="checkbox" /> )}
Initialien des Nachnamens verstecken <label className="floating-label w-full mb-5">
</label> <span className="text-lg flex items-center gap-2">
<label className="floating-label w-full mt-4"> <PersonIcon /> Nachname
<span className="text-lg flex items-center gap-2"> </span>
<EnvelopeClosedIcon /> E-Mail <input
</span> {...form.register("lastname")}
<input type="text"
value={form.watch("email")} defaultValue={user.lastname}
type="text" placeholder="Nachname"
className="input input-bordered w-full" className="input input-bordered w-full"
onChange={(e) => { />
form.setValue("email", e.target.value.trim(), { </label>
shouldDirty: true, {form.formState.errors.lastname && (
}); <p className="text-error">{form.formState.errors.lastname?.message}</p>
form.setValue("emailVerified", false); )}
}} <label className="label">
placeholder="E-Mail" <input
/> type="checkbox"
</label> {...form.register("settingsHideLastname")}
{form.formState.errors.email && ( className="checkbox"
<p className="text-error">{form.formState.errors.email?.message}</p> />
Initialien des Nachnamens verstecken
</label>
<label className="floating-label w-full mt-4">
<span className="text-lg flex items-center gap-2">
<EnvelopeClosedIcon /> E-Mail
</span>
<input
value={form.watch("email")}
type="text"
onChange={(e) => {
form.setValue("email", e.target.value.trim(), {
shouldDirty: true,
});
form.setValue("emailVerified", false);
}}
placeholder="E-Mail"
className="input input-bordered w-full"
/>
</label>
{form.formState.errors.email && (
<p className="text-error">{form.formState.errors.email?.message}</p>
)}
</>
)} )}
<div className="card-actions justify-center pt-6"> <div className="card-actions justify-center pt-6">
<Button <Button
role="submit" role="submit"
@@ -240,7 +268,6 @@ export const SocialForm = ({
<input <input
type="number" type="number"
className="input input-bordered w-full" className="input input-bordered w-full"
placeholder="1445241"
defaultValue={user.vatsimCid as number | undefined} defaultValue={user.vatsimCid as number | undefined}
{...form.register("vatsimCid", { {...form.register("vatsimCid", {
valueAsNumber: true, valueAsNumber: true,
@@ -265,6 +292,43 @@ export const SocialForm = ({
); );
}; };
export const DeleteForm = ({ user, penaltys }: { user: User; penaltys: Penalty[] }) => {
const router = useRouter();
const userCanDelete = penaltys.length === 0 && !user.isBanned;
return (
<div className="card-body">
<h2 className="card-title mb-5">
<CircleAlert className="w-5 h-5" /> Danger-Zone
</h2>
{!userCanDelete && (
<div className="text-left">
<h2 className="text-lg text-warning">Du kannst dein Konto zurzeit nicht löschen!</h2>
<p className="text-sm text-gray-400 ">
Scheinbar hast du aktuell zurzeit aktive Strafen. Um unsere Community zu schützen kannst
du einen Account erst löschen wenn deine Strafe nicht mehr aktiv ist
</p>
</div>
)}
{userCanDelete && (
<div
className="tooltip flex-1 min-w-[250px] tooltip-warning"
data-tip="Achtung! Dies löscht deinen Account und alle zugehörigen Daten. Dieser Vorgang ist nicht rückgängig zu machen."
>
<Button
className="btn-error btn-outline btn-sm w-full"
onClick={async () => {
await deleteUser(user.id);
router.push("/login");
}}
>
<Trash2 size={15} /> Konto sofort löschen
</Button>
</div>
)}
</div>
);
};
export const PasswordForm = (): React.JSX.Element => { export const PasswordForm = (): React.JSX.Element => {
const schema = z.object({ const schema = z.object({
password: z.string().min(2).max(30), password: z.string().min(2).max(30),

View File

@@ -1,20 +1,44 @@
import { prisma } from "@repo/db"; import { prisma } from "@repo/db";
import { getServerSession } from "../../api/auth/[...nextauth]/auth"; import { getServerSession } from "../../api/auth/[...nextauth]/auth";
import { ProfileForm, SocialForm, PasswordForm, PilotForm } from "./_components/forms"; import { ProfileForm, SocialForm, PasswordForm, PilotForm, DeleteForm } from "./_components/forms";
import { GearIcon } from "@radix-ui/react-icons"; import { GearIcon } from "@radix-ui/react-icons";
import { Error } from "_components/Error";
export default async function Page() { export default async function Page() {
const session = await getServerSession(); const session = await getServerSession();
if (!session) return null; if (!session)
return <Error statusCode={401} title="Du musst angemeldet sein, um diese Seite zu sehen." />;
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
id: session.user.id, id: session.user.id,
Penaltys: {
some: {
until: {
gte: new Date(),
},
suspended: false,
},
},
}, },
include: { include: {
discordAccounts: true, discordAccounts: true,
Penaltys: true,
}, },
}); });
if (!user) return null; const userPenaltys = await prisma.penalty.findMany({
where: {
userId: session.user.id,
until: {
gte: new Date(),
},
type: {
in: ["TIME_BAN", "BAN"],
},
suspended: false,
},
});
if (!user) return <Error statusCode={401} title="Dein Account wurde nicht gefunden" />;
const discordAccount = user?.discordAccounts[0]; const discordAccount = user?.discordAccounts[0];
return ( return (
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
@@ -24,7 +48,7 @@ export default async function Page() {
</p> </p>
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ProfileForm user={user} /> <ProfileForm user={user} penaltys={userPenaltys} />
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<SocialForm discordAccount={discordAccount} user={user} /> <SocialForm discordAccount={discordAccount} user={user} />
@@ -35,6 +59,9 @@ export default async function Page() {
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<PilotForm user={user} /> <PilotForm user={user} />
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<DeleteForm user={user} penaltys={userPenaltys} />
</div>
</div> </div>
); );
} }

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { CheckEmailCode } from "(app)/admin/user/action"; import { checkEmailCode } from "(app)/admin/user/action";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
@@ -15,7 +15,7 @@ export default function Page() {
async (code: string) => { async (code: string) => {
console.log("Verifying code:", code); console.log("Verifying code:", code);
if (!code) return; if (!code) return;
const res = await CheckEmailCode(code); const res = await checkEmailCode(code);
console.log("Verification response:", res); console.log("Verification response:", res);
if (res.error) { if (res.error) {
console.log("Verification error:", res.error); console.log("Verification error:", res.error);

View File

@@ -11,6 +11,7 @@ enum BADGES {
enum PERMISSION { enum PERMISSION {
ADMIN_EVENT ADMIN_EVENT
ADMIN_USER ADMIN_USER
ADMIN_USER_ADVANCED
AUDIO_ADMIN AUDIO_ADMIN
ADMIN_STATION ADMIN_STATION
ADMIN_KEYWORD ADMIN_KEYWORD
@@ -66,7 +67,7 @@ model User {
ConnectedDispatcher ConnectedDispatcher[] ConnectedDispatcher ConnectedDispatcher[]
ConnectedAircraft ConnectedAircraft[] ConnectedAircraft ConnectedAircraft[]
PositionLog PositionLog[] PositionLog PositionLog[]
Penalty Penalty[] Penaltys Penalty[]
CreatedPenalties Penalty[] @relation("CreatedPenalties") CreatedPenalties Penalty[] @relation("CreatedPenalties")
@@map(name: "users") @@map(name: "users")