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,6 +94,20 @@ 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">
{!canEdit && (
<div className="text-left">
<h2 className="text-lg text-warning card-title">
Du kannst deine Stammdaten nicht bearbeiten!
</h2>
<p className="text-sm text-gray-400">
Scheinbar hast du aktuell aktive Strafen oder dein Account ist gesperrt. Um unsere
Community zu schützen, kannst du deine persönlichen Informationen erst bearbeiten,
wenn keine Strafen mehr aktiv sind und dein Account nicht gesperrt ist.
</p>
</div>
)}
{canEdit && (
<>
<label className="floating-label w-full mb-5 mt-5"> <label className="floating-label w-full mb-5 mt-5">
<span className="text-lg flex items-center gap-2"> <span className="text-lg flex items-center gap-2">
<PersonIcon /> Vorname <PersonIcon /> Vorname
@@ -94,9 +115,9 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
<input <input
{...form.register("firstname")} {...form.register("firstname")}
type="text" type="text"
className="input input-bordered w-full"
defaultValue={user.firstname} defaultValue={user.firstname}
placeholder="Vorname" placeholder="Vorname"
className="input input-bordered w-full"
/> />
</label> </label>
{form.formState.errors.firstname && ( {form.formState.errors.firstname && (
@@ -109,16 +130,20 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
<input <input
{...form.register("lastname")} {...form.register("lastname")}
type="text" type="text"
className="input input-bordered w-full"
defaultValue={user.lastname} defaultValue={user.lastname}
placeholder="Nachname" placeholder="Nachname"
className="input input-bordered w-full"
/> />
</label> </label>
{form.formState.errors.lastname && ( {form.formState.errors.lastname && (
<p className="text-error">{form.formState.errors.lastname?.message}</p> <p className="text-error">{form.formState.errors.lastname?.message}</p>
)} )}
<label className="label"> <label className="label">
<input type="checkbox" {...form.register("settingsHideLastname")} className="checkbox" /> <input
type="checkbox"
{...form.register("settingsHideLastname")}
className="checkbox"
/>
Initialien des Nachnamens verstecken Initialien des Nachnamens verstecken
</label> </label>
<label className="floating-label w-full mt-4"> <label className="floating-label w-full mt-4">
@@ -128,7 +153,6 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
<input <input
value={form.watch("email")} value={form.watch("email")}
type="text" type="text"
className="input input-bordered w-full"
onChange={(e) => { onChange={(e) => {
form.setValue("email", e.target.value.trim(), { form.setValue("email", e.target.value.trim(), {
shouldDirty: true, shouldDirty: true,
@@ -136,11 +160,15 @@ export const ProfileForm = ({ user }: { user: User }): React.JSX.Element => {
form.setValue("emailVerified", false); form.setValue("emailVerified", false);
}} }}
placeholder="E-Mail" placeholder="E-Mail"
className="input input-bordered w-full"
/> />
</label> </label>
{form.formState.errors.email && ( {form.formState.errors.email && (
<p className="text-error">{form.formState.errors.email?.message}</p> <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")