Discord account Linkage, penalty update
This commit is contained in:
@@ -24,7 +24,7 @@ export const RecentFlights = () => {
|
||||
({
|
||||
User: { id: session.data?.user.id },
|
||||
Mission: {
|
||||
state: { in: ["finished", "archived"] },
|
||||
state: { in: ["finished"] },
|
||||
},
|
||||
}) as Prisma.MissionOnStationUsersWhereInput
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ConnectedAircraft,
|
||||
ConnectedDispatcher,
|
||||
DiscordAccount,
|
||||
FormerDiscordAccount,
|
||||
Penalty,
|
||||
PERMISSION,
|
||||
Prisma,
|
||||
@@ -60,6 +61,7 @@ import { penaltyColumns } from "(app)/admin/penalty/columns";
|
||||
import { addPenalty, editPenaltys } from "(app)/admin/penalty/actions";
|
||||
import { reportColumns } from "(app)/admin/report/columns";
|
||||
import { sendMailByTemplate } from "../../../../../../helper/mail";
|
||||
import Image from "next/image";
|
||||
|
||||
interface ProfileFormProps {
|
||||
user: User;
|
||||
@@ -566,6 +568,7 @@ interface AdminFormProps {
|
||||
minutes: number;
|
||||
lastLogin?: Date;
|
||||
};
|
||||
formerDiscordAccounts: (FormerDiscordAccount & { DiscordAccount: DiscordAccount | null })[];
|
||||
reports: {
|
||||
total: number;
|
||||
open: number;
|
||||
@@ -585,6 +588,7 @@ export const AdminForm = ({
|
||||
pilotTime,
|
||||
reports,
|
||||
discordAccount,
|
||||
formerDiscordAccounts,
|
||||
openBans,
|
||||
openTimebans,
|
||||
}: AdminFormProps) => {
|
||||
@@ -755,6 +759,64 @@ export const AdminForm = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="card-title mt-2">
|
||||
<DiscordLogoIcon className="h-5 w-5" /> Frühere Discord Accounts
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Avatar</th>
|
||||
<th>Benutzername</th>
|
||||
<th>Discord ID</th>
|
||||
<th>getrennt am</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{discordAccount && (
|
||||
<tr>
|
||||
<td>
|
||||
<Image
|
||||
src={`https://cdn.discordapp.com/avatars/${discordAccount.discordId}/${discordAccount.avatar}.png`}
|
||||
alt="Discord Avatar"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
</td>
|
||||
<td>{discordAccount.username}</td>
|
||||
<td>{discordAccount.discordId}</td>
|
||||
<td>N/A (Aktuell verbunden)</td>
|
||||
</tr>
|
||||
)}
|
||||
{formerDiscordAccounts.map((account) => (
|
||||
<tr key={account.discordId}>
|
||||
<td>
|
||||
{account.DiscordAccount && (
|
||||
<Image
|
||||
src={`https://cdn.discordapp.com/avatars/${account.DiscordAccount.discordId}/${account.DiscordAccount.avatar}.png`}
|
||||
alt="Discord Avatar"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>{account.DiscordAccount?.username || "Unbekannt"}</td>
|
||||
<td>{account.DiscordAccount?.discordId || "Unbekannt"}</td>
|
||||
<td>{new Date(account.removedAt).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!discordAccount && formerDiscordAccounts.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center text-gray-400">
|
||||
Keine Discord Accounts verknüpft
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="card-title">
|
||||
<ChartBarBigIcon className="h-5 w-5" /> Aktivität
|
||||
|
||||
@@ -12,16 +12,37 @@ import { getUserPenaltys } from "@repo/shared-components";
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const user = await prisma.user.findUnique({
|
||||
let user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
include: {
|
||||
discordAccounts: true,
|
||||
DiscordAccount: true,
|
||||
CanonicalUser: true,
|
||||
Duplicates: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
user = await prisma.user.findFirst({
|
||||
where: {
|
||||
publicId: id,
|
||||
},
|
||||
include: {
|
||||
DiscordAccount: true,
|
||||
CanonicalUser: true,
|
||||
Duplicates: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const formerDiscordAccounts = await prisma.formerDiscordAccount.findMany({
|
||||
where: {
|
||||
userId: user?.id,
|
||||
},
|
||||
include: {
|
||||
DiscordAccount: true,
|
||||
},
|
||||
});
|
||||
if (!user) return <Error statusCode={404} title="User not found" />;
|
||||
|
||||
const dispoSessions = await prisma.connectedDispatcher.findMany({
|
||||
@@ -121,11 +142,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<AdminForm
|
||||
formerDiscordAccounts={formerDiscordAccounts}
|
||||
user={user}
|
||||
dispoTime={dispoTime}
|
||||
pilotTime={pilotTime}
|
||||
reports={reports}
|
||||
discordAccount={user.discordAccounts[0]}
|
||||
discordAccount={user.DiscordAccount ?? undefined}
|
||||
openBans={openBans}
|
||||
openTimebans={openTimeban}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { User2 } from "lucide-react";
|
||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||
import Link from "next/link";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DiscordAccount, Prisma, User } from "@repo/db";
|
||||
import { DiscordAccount, Penalty, Prisma, User } from "@repo/db";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
const AdminUserPage = () => {
|
||||
@@ -21,16 +21,15 @@ const AdminUserPage = () => {
|
||||
{ firstname: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ lastname: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ email: { contains: searchTerm, mode: "insensitive" } },
|
||||
{
|
||||
discordAccounts: {
|
||||
some: { username: { contains: searchTerm, mode: "insensitive" } },
|
||||
},
|
||||
},
|
||||
{ publicId: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ DiscordAccount: { username: { contains: searchTerm, mode: "insensitive" } } },
|
||||
],
|
||||
} as Prisma.UserWhereInput;
|
||||
}}
|
||||
include={{
|
||||
discordAccounts: true,
|
||||
DiscordAccount: true,
|
||||
ReceivedReports: true,
|
||||
Penaltys: true,
|
||||
}}
|
||||
initialOrderBy={[
|
||||
{
|
||||
@@ -55,6 +54,15 @@ const AdminUserPage = () => {
|
||||
{
|
||||
header: "Berechtigungen",
|
||||
cell(props) {
|
||||
const activePenaltys = props.row.original.Penaltys.filter(
|
||||
(penalty) =>
|
||||
!penalty.suspended &&
|
||||
(penalty.type === "BAN" ||
|
||||
(penalty.type === "TIME_BAN" && penalty!.until! > new Date())),
|
||||
);
|
||||
if (activePenaltys.length > 0) {
|
||||
return <span className="font-bold text-red-600">AKTIVE STRAFE</span>;
|
||||
}
|
||||
if (props.row.original.permissions.length === 0) {
|
||||
return <span className="text-gray-700">Keine</span>;
|
||||
} else if (props.row.original.permissions.includes("ADMIN_USER_ADVANCED")) {
|
||||
@@ -69,14 +77,26 @@ const AdminUserPage = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Strafen / Reports",
|
||||
cell(props) {
|
||||
const penaltyCount = props.row.original.Penaltys.length;
|
||||
const reportCount = props.row.original.ReceivedReports.length;
|
||||
return (
|
||||
<span className="w-full text-center">
|
||||
{penaltyCount} / {reportCount}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Discord",
|
||||
cell(props) {
|
||||
const discord = props.row.original.discordAccounts;
|
||||
if (discord.length === 0) {
|
||||
const discord = props.row.original.DiscordAccount;
|
||||
if (!discord) {
|
||||
return <span className="text-gray-700">Nicht verbunden</span>;
|
||||
}
|
||||
return <span>{discord.map((d) => d.username).join(", ")}</span>;
|
||||
return <span>{discord.username}</span>;
|
||||
},
|
||||
},
|
||||
...(session?.user.permissions.includes("ADMIN_USER_ADVANCED")
|
||||
@@ -97,7 +117,13 @@ const AdminUserPage = () => {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] as ColumnDef<User & { discordAccounts: DiscordAccount[] }>[]
|
||||
] as ColumnDef<
|
||||
User & {
|
||||
DiscordAccount: DiscordAccount;
|
||||
ReceivedReports: Report[];
|
||||
Penaltys: Penalty[];
|
||||
}
|
||||
>[]
|
||||
} // Define the columns for the user table
|
||||
leftOfSearch={
|
||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ProfileForm = ({
|
||||
}: {
|
||||
user: User;
|
||||
penaltys: Penalty[];
|
||||
discordAccount?: DiscordAccount;
|
||||
discordAccount: DiscordAccount | null;
|
||||
}): React.JSX.Element => {
|
||||
const canEdit = penaltys.length === 0 && !user.isBanned;
|
||||
|
||||
@@ -215,9 +215,11 @@ export const ProfileForm = ({
|
||||
export const SocialForm = ({
|
||||
discordAccount,
|
||||
user,
|
||||
penaltys,
|
||||
}: {
|
||||
discordAccount?: DiscordAccount;
|
||||
discordAccount: DiscordAccount | null;
|
||||
user: User;
|
||||
penaltys: Penalty[];
|
||||
}): React.JSX.Element | null => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [vatsimLoading, setVatsimLoading] = useState(false);
|
||||
@@ -235,6 +237,7 @@ export const SocialForm = ({
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
const canUnlinkDiscord = !user.isBanned && penaltys.length === 0;
|
||||
|
||||
if (!user) return null;
|
||||
return (
|
||||
@@ -262,7 +265,7 @@ export const SocialForm = ({
|
||||
</h2>
|
||||
<div>
|
||||
<div>
|
||||
{discordAccount ? (
|
||||
{discordAccount && canUnlinkDiscord ? (
|
||||
<Button
|
||||
className="btn-success btn-block btn-outline hover:btn-error group transition-all duration-0"
|
||||
isLoading={isLoading}
|
||||
@@ -329,14 +332,13 @@ export const SocialForm = ({
|
||||
export const DeleteForm = ({
|
||||
user,
|
||||
penaltys,
|
||||
reports,
|
||||
}: {
|
||||
user: User;
|
||||
penaltys: Penalty[];
|
||||
reports: Report[];
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const userCanDelete = penaltys.length === 0 && !user.isBanned && reports.length === 0;
|
||||
const userCanDelete = penaltys.length === 0 && !user.isBanned;
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title mb-5">
|
||||
@@ -344,11 +346,11 @@ export const DeleteForm = ({
|
||||
</h2>
|
||||
{!userCanDelete && (
|
||||
<div className="text-left">
|
||||
<h2 className="text-warning text-lg">Du kannst dein Konto zurzeit nicht löschen!</h2>
|
||||
<h2 className="text-warning text-lg">Du kannst dein Konto nicht löschen!</h2>
|
||||
<p className="text-sm text-gray-400">
|
||||
Scheinbar hast du Strafen oder Reports in deinem Profil hinterlegt. Um unsere Community
|
||||
zu schützen kannst du deinen Account nicht löschen. Bitte erstelle ein Support-Ticket,
|
||||
wenn du Fragen dazu hast.
|
||||
Da du Strafen hast oder hattest, kannst du deinen Account nicht löschen. Um unsere
|
||||
Community zu schützen kannst du deinen Account nicht löschen. Bitte erstelle ein
|
||||
Support-Ticket, wenn du Fragen dazu hast.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,9 +4,19 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export const unlinkDiscord = async (userId: string) => {
|
||||
await prisma.discordAccount.deleteMany({
|
||||
const discordAccount = await prisma.discordAccount.update({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
data: {
|
||||
userId: null,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.formerDiscordAccount.create({
|
||||
data: {
|
||||
userId,
|
||||
discordId: discordAccount.discordId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||
import { ProfileForm, SocialForm, PasswordForm, DeleteForm } from "./_components/forms";
|
||||
import { GearIcon } from "@radix-ui/react-icons";
|
||||
import { Error } from "_components/Error";
|
||||
import { getUserPenaltys } from "@repo/shared-components";
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getServerSession();
|
||||
@@ -13,15 +14,17 @@ export default async function Page() {
|
||||
id: session.user.id,
|
||||
},
|
||||
include: {
|
||||
discordAccounts: true,
|
||||
DiscordAccount: true,
|
||||
Penaltys: true,
|
||||
},
|
||||
});
|
||||
const userPenaltys = await prisma.penalty.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
suspended: false,
|
||||
},
|
||||
});
|
||||
const activePenaltys = await getUserPenaltys(session.user.id);
|
||||
|
||||
const userReports = await prisma.report.findMany({
|
||||
where: {
|
||||
@@ -30,7 +33,7 @@ export default async function Page() {
|
||||
});
|
||||
|
||||
if (!user) return <Error statusCode={401} title="Dein Account wurde nicht gefunden" />;
|
||||
const discordAccount = user?.discordAccounts[0];
|
||||
const discordAccount = user?.DiscordAccount;
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-full">
|
||||
@@ -39,16 +42,24 @@ export default async function Page() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<ProfileForm user={user} penaltys={userPenaltys} discordAccount={discordAccount} />
|
||||
<ProfileForm
|
||||
user={user}
|
||||
discordAccount={discordAccount}
|
||||
penaltys={[...activePenaltys.openBans, ...activePenaltys.openTimeban]}
|
||||
/>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<SocialForm discordAccount={discordAccount} user={user} />
|
||||
<SocialForm
|
||||
user={user}
|
||||
discordAccount={discordAccount}
|
||||
penaltys={[...activePenaltys.openBans, ...activePenaltys.openTimeban]}
|
||||
/>
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<PasswordForm />
|
||||
</div>
|
||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||
<DeleteForm user={user} penaltys={userPenaltys} reports={userReports} />
|
||||
<DeleteForm user={user} reports={userReports} penaltys={userPenaltys} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { deleteBookingAPI, getBookingsAPI } from "(app)/_querys/bookings";
|
||||
import { Button } from "@repo/shared-components";
|
||||
import { formatTimeRange } from "../../helper/timerange";
|
||||
import toast from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
|
||||
interface BookingTimelineModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -298,7 +299,17 @@ export const BookingTimelineModal = ({
|
||||
? "LST"
|
||||
: booking.Station.bosCallsignShort || booking.Station.bosCallsign}
|
||||
</span>
|
||||
<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 className="flex items-center gap-2">
|
||||
<div className="text-right">
|
||||
|
||||
@@ -49,13 +49,13 @@ export default function SortableTable<TData>({
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table table-zebra w-full">
|
||||
<table className="table-zebra table w-full">
|
||||
<thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
|
||||
<div className="flex items-center gap-1 cursor-pointer">
|
||||
<div className="flex cursor-pointer items-center gap-1">
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{header.column.getIsSorted() === "asc" && <ChevronUp size={16} />}
|
||||
{header.column.getIsSorted() === "desc" && <ChevronDown size={16} />}
|
||||
@@ -75,7 +75,7 @@ export default function SortableTable<TData>({
|
||||
))}
|
||||
{table.getRowModel().rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center font-bold text-sm text-gray-500">
|
||||
<td colSpan={columns.length} className="text-center text-sm font-bold text-gray-500">
|
||||
Keine Daten gefunden
|
||||
</td>
|
||||
</tr>
|
||||
@@ -104,6 +104,8 @@ export const RowsPerPage = ({
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={300}>300</option>
|
||||
<option value={1000}>1000</option>
|
||||
<option value={5000}>5000</option>
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { DiscordAccount, prisma } from "@repo/db";
|
||||
import { getServerSession } from "../auth/[...nextauth]/auth";
|
||||
import { setStandardName } from "../../../helper/discord";
|
||||
import { getUserPenaltys } from "@repo/shared-components";
|
||||
import { markDuplicate } from "(app)/admin/user/action";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const session = await getServerSession();
|
||||
@@ -77,6 +79,29 @@ export const GET = async (req: NextRequest) => {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
const formerDiscordAccount = await prisma.formerDiscordAccount.findMany({
|
||||
where: {
|
||||
discordId: discordUser.id,
|
||||
userId: {
|
||||
not: session.user.id,
|
||||
},
|
||||
User: {
|
||||
canonicalUserId: null,
|
||||
},
|
||||
},
|
||||
include: { User: true },
|
||||
});
|
||||
|
||||
// Account is suspicious to multiaccounting
|
||||
if (formerDiscordAccount.length > 0) {
|
||||
formerDiscordAccount.forEach(async (account) => {
|
||||
await markDuplicate({
|
||||
duplicateUserId: session.user.id,
|
||||
canonicalPublicId: account.User!.publicId,
|
||||
reason: "Multiaccounting Verdacht, gleicher Discord Account wie ein anderer Nutzer.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_HUB_URL}/settings`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
/* const removeImports = require("next-remove-imports")(); */
|
||||
/* const nextConfig = removeImports({}); */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: ["cdn.discordapp.com"],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Reference in New Issue
Block a user