Einstellungen sind eingeschränkt wenn Nutzer gebannt ist
This commit is contained in:
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user