diff --git a/apps/hub/app/(app)/admin/report/columns.tsx b/apps/hub/app/(app)/admin/report/columns.tsx index 6594c32c..e0ad7bbf 100644 --- a/apps/hub/app/(app)/admin/report/columns.tsx +++ b/apps/hub/app/(app)/admin/report/columns.tsx @@ -12,9 +12,9 @@ export const reportColumns: ColumnDef {row.getValue("reviewed") ? ( - + ) : ( - + )} ); @@ -31,13 +31,13 @@ export const reportColumns: ColumnDef { const role = row.getValue("reportedUserRole") as string | undefined; const Icon = role ? (role.startsWith("LST") ? Workflow : Plane) : ShieldQuestion; return ( - + {role || "Unbekannt"} ); @@ -62,7 +62,7 @@ export const reportColumns: ColumnDef ( ), diff --git a/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx b/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx new file mode 100644 index 00000000..d39c4493 --- /dev/null +++ b/apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx @@ -0,0 +1,137 @@ +"use client"; +import { Log, Prisma, User } from "@repo/db"; +import { cn } from "@repo/shared-components"; +import { ColumnDef } from "@tanstack/react-table"; +import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable"; +import { Printer } from "lucide-react"; +import Link from "next/link"; +import { useRef, useState } from "react"; + +export const AccountLog = ({ sameIPLogs, userId }: { sameIPLogs: Log[]; userId: string }) => { + const [onlyImportant, setOnlyImportant] = useState(true); + const tableRef = useRef(null); + return ( +
+
+

+ Account Log +

+

+ Hier werden Logs angezeigt, die dem Nutzer zugeordnet sind oder von der selben IP stammen. +

+
+ + { + setOnlyImportant(!onlyImportant); + tableRef.current?.refresh(); + }} + className="checkbox checkbox-sm" + /> + +
+ } + getFilter={(searchTerm) => { + return { + AND: [ + { + OR: [ + { ip: { contains: searchTerm } }, + { browser: { contains: searchTerm } }, + { + id: { + in: sameIPLogs + .filter((log) => log.id.toString().includes(searchTerm)) + .map((log) => log.id), + }, + }, + ], + }, + onlyImportant + ? { + OR: [ + { + id: { + in: sameIPLogs + .filter((log) => log.id.toString().includes(searchTerm)) + .map((log) => log.id), + }, + }, + { + type: { + in: ["REGISTER", "PROFILE_CHANGE"], + }, + }, + ], + } + : {}, + ], + } as Prisma.LogWhereInput; + }} + include={{ + User: true, + }} + prismaModel={"log"} + columns={ + [ + { + header: "Zeitstempel", + accessorKey: "timestamp", + cell: (info) => new Date(info.getValue()).toLocaleString("de-DE"), + }, + { + header: "Aktion", + accessorKey: "action", + cell: ({ row }) => { + const action = row.original.type; + + if (action !== "PROFILE_CHANGE") { + return {action}; + } else { + return ( + {`${row.original.field} von "${row.original.oldValue}" zu "${row.original.newValue}"`} + ); + } + }, + }, + { + header: "IP-Adresse", + accessorKey: "ip", + }, + { + header: "Browser", + accessorKey: "browser", + }, + { + header: "Benutzer", + accessorKey: "userId", + cell: ({ row }) => { + return ( + + {row.original.User + ? `${row.original.User.firstname} ${row.original.User.lastname} - ${row.original.User.publicId}` + : "Unbekannt"} + + ); + }, + }, + ] as ColumnDef[] + } + /> + + ); +}; diff --git a/apps/hub/app/(app)/admin/user/[id]/page.tsx b/apps/hub/app/(app)/admin/user/[id]/page.tsx index ca619cb6..0d789227 100644 --- a/apps/hub/app/(app)/admin/user/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/page.tsx @@ -11,6 +11,7 @@ import { Error } from "../../../../_components/Error"; import { getUserPenaltys } from "@repo/shared-components"; import { PaginatedTable } from "_components/PaginatedTable"; import { ColumnDef } from "@tanstack/react-table"; +import { AccountLog } from "./_components/AccountLog"; export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; @@ -175,44 +176,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }> />
- new Date(info.getValue()).toLocaleString("de-DE"), - }, - { - header: "Aktion", - accessorKey: "action", - cell: ({ row }) => { - const action = row.original.type; - - if (action !== "PROFILE_CHANGE") { - return action; - } else { - return `${row.original.field} von "${row.original.oldValue}" zu "${row.original.newValue}"`; - } - }, - }, - { - header: "IP-Adresse", - accessorKey: "ip", - }, - { - header: "Gerät", - accessorKey: "browser", - }, - ] as ColumnDef[] - } - /> +
-
+
-
+
diff --git a/apps/hub/app/(auth)/login/_components/action.ts b/apps/hub/app/(auth)/login/_components/action.ts index fa38dd87..87af797e 100644 --- a/apps/hub/app/(auth)/login/_components/action.ts +++ b/apps/hub/app/(auth)/login/_components/action.ts @@ -3,6 +3,7 @@ import { LOG_TYPE, prisma } from "@repo/db"; import { getServerSession } from "api/auth/[...nextauth]/auth"; import { randomUUID } from "crypto"; import { cookies, headers } from "next/headers"; +import { sendReportEmbed } from "../../../../helper/discord"; export async function getOrSetDeviceId() { const store = await cookies(); @@ -33,9 +34,44 @@ export const logAction = async ( const headersList = await headers(); const user = await getServerSession(); - console.log("headers"); + const ip = + headersList.get("X-Forwarded-For") || + headersList.get("Forwarded") || + headersList.get("X-Real-IP"); const deviceId = await getOrSetDeviceId(); + if (type == "LOGIN") { + const existingLogs = await prisma.log.findMany({ + where: { + type: "LOGIN", + userId: { + not: user?.user.id, + }, + OR: [ + { + ip: ip, + }, + { + deviceId: deviceId, + }, + ], + }, + }); + if (existingLogs.length > 0 && user?.user.id) { + // Möglicherweise ein doppelter Account, Report erstellen + const report = await prisma.report.create({ + data: { + text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs + .map((log) => `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${log.timestamp}`) + .join("\n")}`, + reportedUserId: user?.user.id, + reportedUserRole: "LOGIN - Doppelter Account Verdacht", + }, + }); + + await sendReportEmbed(report.id); + } + } await prisma.log.create({ data: { @@ -43,10 +79,7 @@ export const logAction = async ( browser: headersList.get("user-agent") || "unknown", userId: user?.user.id, deviceId: deviceId, - ip: - headersList.get("X-Forwarded-For") || - headersList.get("Forwarded") || - headersList.get("X-Real-IP"), + ip, ...otherValues, }, }); diff --git a/apps/hub/app/_components/PaginatedTable.tsx b/apps/hub/app/_components/PaginatedTable.tsx index af4aedc0..b2be55f8 100644 --- a/apps/hub/app/_components/PaginatedTable.tsx +++ b/apps/hub/app/_components/PaginatedTable.tsx @@ -21,6 +21,7 @@ interface PaginatedTableProps leftOfSearch?: React.ReactNode; rightOfSearch?: React.ReactNode; leftOfPagination?: React.ReactNode; + rightOfPagination?: React.ReactNode; supressQuery?: boolean; ref?: Ref; } @@ -37,6 +38,7 @@ export function PaginatedTable({ leftOfSearch, rightOfSearch, leftOfPagination, + rightOfPagination, supressQuery, ...restProps }: PaginatedTableProps) { @@ -159,10 +161,9 @@ export function PaginatedTable({
{leftOfPagination} - <> - - - + + {rightOfPagination} +
); diff --git a/apps/hub/app/_components/Table.tsx b/apps/hub/app/_components/Table.tsx index e8ee6618..f2a51155 100644 --- a/apps/hub/app/_components/Table.tsx +++ b/apps/hub/app/_components/Table.tsx @@ -95,7 +95,7 @@ export const RowsPerPage = ({ }) => { return ( setPage(Number(e.target.value))} > @@ -137,11 +141,11 @@ export const Pagination = ({ ))}
); diff --git a/packages/database/prisma/json/User.ts b/packages/database/prisma/json/User.ts index 3dbbc5d6..098e3ea3 100644 --- a/packages/database/prisma/json/User.ts +++ b/packages/database/prisma/json/User.ts @@ -41,6 +41,7 @@ export const getPublicUser = ( user: User, options = { ignorePrivacy: false, + fullLastName: false, }, ): PublicUser => { const lastName = user.lastname @@ -49,6 +50,17 @@ export const getPublicUser = ( .map((part) => `${part[0] || ""}.`) .join(" "); + if (options.fullLastName) { + return { + firstname: user.firstname, + lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : user.lastname, + fullName: + `${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : user.lastname}`.trim(), + publicId: user.publicId, + badges: user.badges, + }; + } + return { firstname: user.firstname, lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name diff --git a/packages/database/prisma/schema/penalty.prisma b/packages/database/prisma/schema/penalty.prisma index 9820bf1b..743d16c2 100644 --- a/packages/database/prisma/schema/penalty.prisma +++ b/packages/database/prisma/schema/penalty.prisma @@ -1,12 +1,11 @@ -model AuditLog { +model Penalty { id Int @id @default(autoincrement()) userId String createdUserId String? reportId Int? - // Generalized action type to cover penalties and user history events - action AuditLogAction? - reason String? + type PenaltyType + reason String until DateTime? suspended Boolean @default(false) @@ -19,13 +18,9 @@ model AuditLog { Report Report? @relation(fields: [reportId], references: [id]) } -enum AuditLogAction { - // Penalty actions +enum PenaltyType { KICK TIME_BAN PERMISSIONS_REVOCED BAN - // User history events - USER_DELETED - USER_PROFILE_UPDATED }