From bdc35ea6b3cb59d15d7cd90f865505ad860bd5bd Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:38:55 +0100 Subject: [PATCH 1/5] Include Profile log in renamed penalty model -> Audit log --- apps/hub/app/(app)/admin/user/action.ts | 5 ++++- apps/hub/app/(app)/events/page.tsx | 3 +++ apps/hub/app/api/auth/[...nextauth]/auth.ts | 2 ++ packages/database/prisma/json/User.ts | 22 +++++++++++++++++++ .../database/prisma/schema/penalty.prisma | 13 +++++++---- packages/database/prisma/schema/report.prisma | 8 +++---- packages/database/prisma/schema/user.prisma | 19 +++++++++------- 7 files changed, 55 insertions(+), 17 deletions(-) diff --git a/apps/hub/app/(app)/admin/user/action.ts b/apps/hub/app/(app)/admin/user/action.ts index 8de43981..201a8ba8 100644 --- a/apps/hub/app/(app)/admin/user/action.ts +++ b/apps/hub/app/(app)/admin/user/action.ts @@ -57,10 +57,13 @@ export const deletePilotHistory = async (id: number) => { }); }; export const deleteUser = async (id: string) => { - return await prisma.user.delete({ + return await prisma.user.update({ where: { id: id, }, + data: { + isDeleted: true, + }, }); }; diff --git a/apps/hub/app/(app)/events/page.tsx b/apps/hub/app/(app)/events/page.tsx index fbd72617..22b2c86f 100644 --- a/apps/hub/app/(app)/events/page.tsx +++ b/apps/hub/app/(app)/events/page.tsx @@ -10,6 +10,9 @@ const page = async () => { if (!user) return null; const events = await prisma.event.findMany({ + orderBy: { + id: "desc", + }, where: { hidden: false, }, diff --git a/apps/hub/app/api/auth/[...nextauth]/auth.ts b/apps/hub/app/api/auth/[...nextauth]/auth.ts index 07201dec..8a4e530d 100644 --- a/apps/hub/app/api/auth/[...nextauth]/auth.ts +++ b/apps/hub/app/api/auth/[...nextauth]/auth.ts @@ -23,6 +23,7 @@ export const options: AuthOptions = { contains: credentials.email, mode: "insensitive", }, + isDeleted: false, }, }); const v1User = (oldUser as OldUser[]).find( @@ -87,6 +88,7 @@ export const options: AuthOptions = { const dbUser = await prisma.user.findUnique({ where: { id: token?.sub, + isDeleted: false, }, }); if (!dbUser) { diff --git a/packages/database/prisma/json/User.ts b/packages/database/prisma/json/User.ts index 1756f152..3dbbc5d6 100644 --- a/packages/database/prisma/json/User.ts +++ b/packages/database/prisma/json/User.ts @@ -1,5 +1,27 @@ import { User } from "../../generated/client"; +// USer History + +interface UserDeletedEvent { + type: "USER_DELETED"; + reason: string; + date: string; + by: string; +} + +interface UserProfileUpdatedEvent { + type: "USER_PROFILE_UPDATED"; + changes: { + field: string; + oldValue: string; + newValue: string; + }; + date: string; + by: string; +} + +export type UserHistoryEvent = UserDeletedEvent | UserProfileUpdatedEvent; + export interface PublicUser { firstname: string; lastname: string; diff --git a/packages/database/prisma/schema/penalty.prisma b/packages/database/prisma/schema/penalty.prisma index 27a163f4..8a208b9a 100644 --- a/packages/database/prisma/schema/penalty.prisma +++ b/packages/database/prisma/schema/penalty.prisma @@ -1,11 +1,12 @@ -model Penalty { +model AuditLog { id Int @id @default(autoincrement()) userId String createdUserId String? reportId Int? - type PenaltyType - reason String + // Generalized action type to cover penalties and user history events + action AuditLogAction? + reason String? until DateTime? suspended Boolean @default(false) @@ -18,9 +19,13 @@ model Penalty { Report Report? @relation(fields: [reportId], references: [id]) } -enum PenaltyType { +enum AuditLogAction { + // Penalty actions KICK TIME_BAN PERMISSIONS_REVOCED BAN + // User history events + USER_DELETED + USER_PROFILE_UPDATED } diff --git a/packages/database/prisma/schema/report.prisma b/packages/database/prisma/schema/report.prisma index 7bcd0bdd..33c0e9a8 100644 --- a/packages/database/prisma/schema/report.prisma +++ b/packages/database/prisma/schema/report.prisma @@ -10,8 +10,8 @@ model Report { reviewerUserId String? // relations: - Sender User? @relation("SentReports", fields: [senderUserId], references: [id]) - Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade) - Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id]) - Penalty Penalty[] + Sender User? @relation("SentReports", fields: [senderUserId], references: [id]) + Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade) + Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id]) + AuditLog AuditLog[] } diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index 1f09f3f0..6998cbd3 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -54,12 +54,14 @@ model User { emailVerificationExpiresAt DateTime? @map(name: "email_verification_expires_at") emailVerified Boolean @default(false) - image String? - badges BADGES[] @default([]) - permissions PERMISSION[] @default([]) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - isBanned Boolean @default(false) @map(name: "is_banned") + image String? + badges BADGES[] @default([]) + permissions PERMISSION[] @default([]) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + isBanned Boolean @default(false) @map(name: "is_banned") + isDeleted Boolean @default(false) @map(name: "is_deleted") + // relations: oauthTokens OAuthToken[] discordAccounts DiscordAccount[] @@ -76,9 +78,10 @@ model User { ConnectedDispatcher ConnectedDispatcher[] ConnectedAircraft ConnectedAircraft[] PositionLog PositionLog[] - Penaltys Penalty[] - CreatedPenalties Penalty[] @relation("CreatedPenalties") + Penaltys AuditLog[] + CreatedAuditLogEntrys AuditLog[] @relation("CreatedAuditLogEntrys") Bookings Booking[] + auditLogs AuditLog[] @@map(name: "users") } From 005509598c26d9643f1443de982e00f19bd207b3 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:38:55 +0100 Subject: [PATCH 2/5] Include Profile log in renamed penalty model -> Audit log --- apps/hub/app/(app)/admin/user/action.ts | 5 ++- apps/hub/app/(app)/events/page.tsx | 3 ++ apps/hub/app/api/auth/[...nextauth]/auth.ts | 2 ++ packages/database/prisma/json/User.ts | 22 +++++++++++++ .../database/prisma/schema/penalty.prisma | 13 +++++--- packages/database/prisma/schema/report.prisma | 8 ++--- packages/database/prisma/schema/user.prisma | 31 +++++++++++-------- 7 files changed, 62 insertions(+), 22 deletions(-) diff --git a/apps/hub/app/(app)/admin/user/action.ts b/apps/hub/app/(app)/admin/user/action.ts index 5b623291..b3b9613e 100644 --- a/apps/hub/app/(app)/admin/user/action.ts +++ b/apps/hub/app/(app)/admin/user/action.ts @@ -58,10 +58,13 @@ export const deletePilotHistory = async (id: number) => { }); }; export const deleteUser = async (id: string) => { - return await prisma.user.delete({ + return await prisma.user.update({ where: { id: id, }, + data: { + isDeleted: true, + }, }); }; diff --git a/apps/hub/app/(app)/events/page.tsx b/apps/hub/app/(app)/events/page.tsx index cdfbd3d4..fb67df39 100644 --- a/apps/hub/app/(app)/events/page.tsx +++ b/apps/hub/app/(app)/events/page.tsx @@ -10,6 +10,9 @@ const page = async () => { if (!user) return null; const events = await prisma.event.findMany({ + orderBy: { + id: "desc", + }, where: { hidden: false, }, diff --git a/apps/hub/app/api/auth/[...nextauth]/auth.ts b/apps/hub/app/api/auth/[...nextauth]/auth.ts index 07201dec..8a4e530d 100644 --- a/apps/hub/app/api/auth/[...nextauth]/auth.ts +++ b/apps/hub/app/api/auth/[...nextauth]/auth.ts @@ -23,6 +23,7 @@ export const options: AuthOptions = { contains: credentials.email, mode: "insensitive", }, + isDeleted: false, }, }); const v1User = (oldUser as OldUser[]).find( @@ -87,6 +88,7 @@ export const options: AuthOptions = { const dbUser = await prisma.user.findUnique({ where: { id: token?.sub, + isDeleted: false, }, }); if (!dbUser) { diff --git a/packages/database/prisma/json/User.ts b/packages/database/prisma/json/User.ts index 1756f152..3dbbc5d6 100644 --- a/packages/database/prisma/json/User.ts +++ b/packages/database/prisma/json/User.ts @@ -1,5 +1,27 @@ import { User } from "../../generated/client"; +// USer History + +interface UserDeletedEvent { + type: "USER_DELETED"; + reason: string; + date: string; + by: string; +} + +interface UserProfileUpdatedEvent { + type: "USER_PROFILE_UPDATED"; + changes: { + field: string; + oldValue: string; + newValue: string; + }; + date: string; + by: string; +} + +export type UserHistoryEvent = UserDeletedEvent | UserProfileUpdatedEvent; + export interface PublicUser { firstname: string; lastname: string; diff --git a/packages/database/prisma/schema/penalty.prisma b/packages/database/prisma/schema/penalty.prisma index 27a163f4..8a208b9a 100644 --- a/packages/database/prisma/schema/penalty.prisma +++ b/packages/database/prisma/schema/penalty.prisma @@ -1,11 +1,12 @@ -model Penalty { +model AuditLog { id Int @id @default(autoincrement()) userId String createdUserId String? reportId Int? - type PenaltyType - reason String + // Generalized action type to cover penalties and user history events + action AuditLogAction? + reason String? until DateTime? suspended Boolean @default(false) @@ -18,9 +19,13 @@ model Penalty { Report Report? @relation(fields: [reportId], references: [id]) } -enum PenaltyType { +enum AuditLogAction { + // Penalty actions KICK TIME_BAN PERMISSIONS_REVOCED BAN + // User history events + USER_DELETED + USER_PROFILE_UPDATED } diff --git a/packages/database/prisma/schema/report.prisma b/packages/database/prisma/schema/report.prisma index 7bcd0bdd..33c0e9a8 100644 --- a/packages/database/prisma/schema/report.prisma +++ b/packages/database/prisma/schema/report.prisma @@ -10,8 +10,8 @@ model Report { reviewerUserId String? // relations: - Sender User? @relation("SentReports", fields: [senderUserId], references: [id]) - Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade) - Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id]) - Penalty Penalty[] + Sender User? @relation("SentReports", fields: [senderUserId], references: [id]) + Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade) + Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id]) + AuditLog AuditLog[] } diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index 244b0c62..f8023cb3 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -54,18 +54,21 @@ model User { emailVerificationExpiresAt DateTime? @map(name: "email_verification_expires_at") emailVerified Boolean @default(false) - image String? - badges BADGES[] @default([]) - permissions PERMISSION[] @default([]) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @map(name: "updated_at") - isBanned Boolean @default(false) @map(name: "is_banned") + image String? + badges BADGES[] @default([]) + permissions PERMISSION[] @default([]) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + isBanned Boolean @default(false) @map(name: "is_banned") // Duplicate handling: - canonicalUserId String? @map(name: "canonical_user_id") - CanonicalUser User? @relation("CanonicalUser", fields: [canonicalUserId], references: [id]) - Duplicates User[] @relation("CanonicalUser") - duplicateDetectedAt DateTime? @map(name: "duplicate_detected_at") - duplicateReason String? @map(name: "duplicate_reason") + canonicalUserId String? @map(name: "canonical_user_id") + CanonicalUser User? @relation("CanonicalUser", fields: [canonicalUserId], references: [id]) + Duplicates User[] @relation("CanonicalUser") + duplicateDetectedAt DateTime? @map(name: "duplicate_detected_at") + duplicateReason String? @map(name: "duplicate_reason") + + isDeleted Boolean @default(false) @map(name: "is_deleted") + // relations: oauthTokens OAuthToken[] participants Participant[] @@ -79,12 +82,14 @@ model User { ConnectedDispatcher ConnectedDispatcher[] ConnectedAircraft ConnectedAircraft[] PositionLog PositionLog[] - Penaltys Penalty[] - CreatedPenalties Penalty[] @relation("CreatedPenalties") + Penaltys AuditLog[] + CreatedAuditLogEntrys AuditLog[] @relation("CreatedAuditLogEntrys") Bookings Booking[] + auditLogs AuditLog[] DiscordAccount DiscordAccount? FormerDiscordAccounts FormerDiscordAccount[] + auditLogs AuditLog[] @@map(name: "users") } From e4aae9804b28529d4f0770f934dc55c055d7c88c Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:25:51 +0100 Subject: [PATCH 3/5] Continue Account log --- apps/hub/app/(app)/admin/user/[id]/page.tsx | 59 ++++++++++++++++++- apps/hub/app/(app)/events/page.tsx | 4 -- .../app/(app)/settings/_components/forms.tsx | 23 ++++++++ .../app/(auth)/login/_components/Login.tsx | 5 ++ .../app/(auth)/login/_components/action.ts | 53 +++++++++++++++++ apps/hub/helper/discord.ts | 10 ++++ packages/database/prisma/schema/log.prisma | 22 +++++++ .../migration.sql | 2 + .../20260129221239_user_log/migration.sql | 16 +++++ .../20260129222950_add_browser/migration.sql | 16 +++++ .../20260129224931_ce_id/migration.sql | 2 + .../migration.sql | 4 ++ .../database/prisma/schema/penalty.prisma | 15 ++--- packages/database/prisma/schema/report.prisma | 8 +-- packages/database/prisma/schema/user.prisma | 9 ++- 15 files changed, 224 insertions(+), 24 deletions(-) create mode 100644 apps/hub/app/(auth)/login/_components/action.ts create mode 100644 packages/database/prisma/schema/log.prisma create mode 100644 packages/database/prisma/schema/migrations/20260129214442_fix_penalty_user_relation/migration.sql create mode 100644 packages/database/prisma/schema/migrations/20260129221239_user_log/migration.sql create mode 100644 packages/database/prisma/schema/migrations/20260129222950_add_browser/migration.sql create mode 100644 packages/database/prisma/schema/migrations/20260129224931_ce_id/migration.sql create mode 100644 packages/database/prisma/schema/migrations/20260129230237_profile_values/migration.sql diff --git a/apps/hub/app/(app)/admin/user/[id]/page.tsx b/apps/hub/app/(app)/admin/user/[id]/page.tsx index 76e57b3d..ca619cb6 100644 --- a/apps/hub/app/(app)/admin/user/[id]/page.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/page.tsx @@ -1,5 +1,5 @@ import { PersonIcon } from "@radix-ui/react-icons"; -import { prisma } from "@repo/db"; +import { Log, prisma } from "@repo/db"; import { AdminForm, ConnectionHistory, @@ -9,6 +9,8 @@ import { } from "./_components/forms"; import { Error } from "../../../../_components/Error"; import { getUserPenaltys } from "@repo/shared-components"; +import { PaginatedTable } from "_components/PaginatedTable"; +import { ColumnDef } from "@tanstack/react-table"; export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; @@ -35,6 +37,26 @@ export default async function Page({ params }: { params: Promise<{ id: string }> }); } + const userLog = await prisma.log.findMany({ + where: { + userId: user?.id, + }, + }); + + const sameIpLogs = await prisma.log.findMany({ + where: { + ip: { + in: userLog.map((log) => log.ip).filter((ip): ip is string => ip !== null), + }, + userId: { + not: user?.id, + }, + }, + include: { + User: true, + }, + }); + const formerDiscordAccounts = await prisma.formerDiscordAccount.findMany({ where: { userId: user?.id, @@ -152,6 +174,41 @@ export default async function Page({ params }: { params: Promise<{ id: string }> openTimebans={openTimeban} /> +
+ 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/(app)/events/page.tsx b/apps/hub/app/(app)/events/page.tsx index fb67df39..2aa8a479 100644 --- a/apps/hub/app/(app)/events/page.tsx +++ b/apps/hub/app/(app)/events/page.tsx @@ -23,10 +23,6 @@ const page = async () => { }, }, }, - - orderBy: { - id: "desc", - }, }); return ( diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx index 64feabec..15399821 100644 --- a/apps/hub/app/(app)/settings/_components/forms.tsx +++ b/apps/hub/app/(app)/settings/_components/forms.tsx @@ -23,6 +23,7 @@ import toast from "react-hot-toast"; import { CircleAlert, Trash2 } from "lucide-react"; import { deleteUser, sendVerificationLink } from "(app)/admin/user/action"; import { setStandardName } from "../../../../helper/discord"; +import { logAction } from "(auth)/login/_components/action"; export const ProfileForm = ({ user, @@ -101,6 +102,28 @@ export const ProfileForm = ({ userId: user.id, }); } + if (user.firstname !== values.firstname) { + await logAction("PROFILE_CHANGE", { + field: "firstname", + oldValue: user.firstname, + newValue: values.firstname, + }); + } + if (user.lastname !== values.lastname) { + await logAction("PROFILE_CHANGE", { + field: "lastname", + oldValue: user.lastname, + newValue: values.lastname, + }); + } + if (user.email !== values.email) { + await logAction("PROFILE_CHANGE", { + field: "email", + oldValue: user.email, + newValue: values.email, + }); + } + form.reset(values); if (user.email !== values.email) { await sendVerificationLink(user.id); diff --git a/apps/hub/app/(auth)/login/_components/Login.tsx b/apps/hub/app/(auth)/login/_components/Login.tsx index df02a2c2..f22db1d0 100644 --- a/apps/hub/app/(auth)/login/_components/Login.tsx +++ b/apps/hub/app/(auth)/login/_components/Login.tsx @@ -9,6 +9,7 @@ import { Toaster, toast } from "react-hot-toast"; import { z } from "zod"; import { Button } from "../../../_components/ui/Button"; import { useErrorBoundary } from "react-error-boundary"; +import { logAction } from "./action"; export const Login = () => { const { showBoundary } = useErrorBoundary(); @@ -46,6 +47,10 @@ export const Login = () => { }); return; } + + console.log("data", data); + + await logAction("LOGIN"); redirect(searchParams.get("redirect") || "/"); } catch (error) { showBoundary(error); diff --git a/apps/hub/app/(auth)/login/_components/action.ts b/apps/hub/app/(auth)/login/_components/action.ts new file mode 100644 index 00000000..fa38dd87 --- /dev/null +++ b/apps/hub/app/(auth)/login/_components/action.ts @@ -0,0 +1,53 @@ +"use server"; +import { LOG_TYPE, prisma } from "@repo/db"; +import { getServerSession } from "api/auth/[...nextauth]/auth"; +import { randomUUID } from "crypto"; +import { cookies, headers } from "next/headers"; + +export async function getOrSetDeviceId() { + const store = await cookies(); + let deviceId = store.get("device_id")?.value; + + if (!deviceId) { + deviceId = randomUUID(); + store.set("device_id", deviceId, { + httpOnly: true, + secure: true, + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 365, // 1 Jahr + }); + } + + return deviceId; +} + +export const logAction = async ( + type: LOG_TYPE, + otherValues?: { + field: string; + oldValue: string; + newValue: string; + }, +) => { + const headersList = await headers(); + const user = await getServerSession(); + + console.log("headers"); + + const deviceId = await getOrSetDeviceId(); + + await prisma.log.create({ + data: { + type, + 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"), + ...otherValues, + }, + }); +}; diff --git a/apps/hub/helper/discord.ts b/apps/hub/helper/discord.ts index a9a6b9d4..b432906b 100644 --- a/apps/hub/helper/discord.ts +++ b/apps/hub/helper/discord.ts @@ -38,6 +38,16 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[]) }); }; +export const sendReportEmbed = async (reportId: number) => { + discordAxiosClient + .post("/report/admin-embed", { + reportId, + }) + .catch((error) => { + console.error("Error sending report embed:", error); + }); +}; + export const setStandardName = async ({ memberId, userId, diff --git a/packages/database/prisma/schema/log.prisma b/packages/database/prisma/schema/log.prisma new file mode 100644 index 00000000..59911faf --- /dev/null +++ b/packages/database/prisma/schema/log.prisma @@ -0,0 +1,22 @@ +model Log { + id Int @id @default(autoincrement()) + type LOG_TYPE + userId String? + browser String? + deviceId String? + ip String? + field String? + oldValue String? + newValue String? + timestamp DateTime @default(now()) + + + User User? @relation(fields: [userId], references: [id], onDelete: Cascade) + @@map(name: "logs") +} + +enum LOG_TYPE { + LOGIN + PROFILE_CHANGE + REGISTER +} \ No newline at end of file diff --git a/packages/database/prisma/schema/migrations/20260129214442_fix_penalty_user_relation/migration.sql b/packages/database/prisma/schema/migrations/20260129214442_fix_penalty_user_relation/migration.sql new file mode 100644 index 00000000..3360abb2 --- /dev/null +++ b/packages/database/prisma/schema/migrations/20260129214442_fix_penalty_user_relation/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/database/prisma/schema/migrations/20260129221239_user_log/migration.sql b/packages/database/prisma/schema/migrations/20260129221239_user_log/migration.sql new file mode 100644 index 00000000..4cd2e37e --- /dev/null +++ b/packages/database/prisma/schema/migrations/20260129221239_user_log/migration.sql @@ -0,0 +1,16 @@ +-- CreateEnum +CREATE TYPE "LOG_TYLE" AS ENUM ('LOGIN', 'PROFILE_CHANGE'); + +-- CreateTable +CREATE TABLE "logs" ( + "id" SERIAL NOT NULL, + "type" "LOG_TYLE" NOT NULL, + "userId" TEXT, + "ip" TEXT, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "logs_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "logs" ADD CONSTRAINT "logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/schema/migrations/20260129222950_add_browser/migration.sql b/packages/database/prisma/schema/migrations/20260129222950_add_browser/migration.sql new file mode 100644 index 00000000..3a0795ea --- /dev/null +++ b/packages/database/prisma/schema/migrations/20260129222950_add_browser/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - Changed the type of `type` on the `logs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- CreateEnum +CREATE TYPE "LOG_TYPE" AS ENUM ('LOGIN', 'PROFILE_CHANGE', 'REGISTER'); + +-- AlterTable +ALTER TABLE "logs" ADD COLUMN "browser" TEXT, +DROP COLUMN "type", +ADD COLUMN "type" "LOG_TYPE" NOT NULL; + +-- DropEnum +DROP TYPE "LOG_TYLE"; diff --git a/packages/database/prisma/schema/migrations/20260129224931_ce_id/migration.sql b/packages/database/prisma/schema/migrations/20260129224931_ce_id/migration.sql new file mode 100644 index 00000000..a29ac825 --- /dev/null +++ b/packages/database/prisma/schema/migrations/20260129224931_ce_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "logs" ADD COLUMN "deviceId" TEXT; diff --git a/packages/database/prisma/schema/migrations/20260129230237_profile_values/migration.sql b/packages/database/prisma/schema/migrations/20260129230237_profile_values/migration.sql new file mode 100644 index 00000000..fdeea722 --- /dev/null +++ b/packages/database/prisma/schema/migrations/20260129230237_profile_values/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "logs" ADD COLUMN "field" TEXT, +ADD COLUMN "newValue" TEXT, +ADD COLUMN "oldValue" TEXT; diff --git a/packages/database/prisma/schema/penalty.prisma b/packages/database/prisma/schema/penalty.prisma index 8a208b9a..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) @@ -14,18 +13,14 @@ model AuditLog { timestamp DateTime @default(now()) // relations: - User User @relation(fields: [userId], references: [id], onDelete: Cascade) + User User @relation("User", fields: [userId], references: [id], onDelete: Cascade) CreatedUser User? @relation("CreatedPenalties", fields: [createdUserId], references: [id]) 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 } diff --git a/packages/database/prisma/schema/report.prisma b/packages/database/prisma/schema/report.prisma index 33c0e9a8..9264f1e6 100644 --- a/packages/database/prisma/schema/report.prisma +++ b/packages/database/prisma/schema/report.prisma @@ -10,8 +10,8 @@ model Report { reviewerUserId String? // relations: - Sender User? @relation("SentReports", fields: [senderUserId], references: [id]) - Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade) - Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id]) - AuditLog AuditLog[] + Sender User? @relation("SentReports", fields: [senderUserId], references: [id]) + Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade) + Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id]) + Penalties Penalty[] } diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index f8023cb3..45cf2206 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -82,14 +82,13 @@ model User { ConnectedDispatcher ConnectedDispatcher[] ConnectedAircraft ConnectedAircraft[] PositionLog PositionLog[] - Penaltys AuditLog[] - CreatedAuditLogEntrys AuditLog[] @relation("CreatedAuditLogEntrys") - Bookings Booking[] - auditLogs AuditLog[] + Penaltys Penalty[] @relation("User") + CreatedPenalties Penalty[] @relation("CreatedPenalties") + Logs Log[] + Bookings Booking[] DiscordAccount DiscordAccount? FormerDiscordAccounts FormerDiscordAccount[] - auditLogs AuditLog[] @@map(name: "users") } From 2154684223f968f8251041fd5ca82bccfe707488 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:19:00 +0100 Subject: [PATCH 4/5] completed Account Log --- apps/hub/app/(app)/admin/report/columns.tsx | 10 +- .../user/[id]/_components/AccountLog.tsx | 137 ++++++++++++++++++ apps/hub/app/(app)/admin/user/[id]/page.tsx | 39 +---- .../app/(auth)/login/_components/action.ts | 43 +++++- apps/hub/app/_components/PaginatedTable.tsx | 9 +- apps/hub/app/_components/Table.tsx | 16 +- packages/database/prisma/json/User.ts | 12 ++ .../database/prisma/schema/penalty.prisma | 13 +- 8 files changed, 215 insertions(+), 64 deletions(-) create mode 100644 apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx 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 } From 10af6bf71a5e4fe6ee733d308a50d22b3a286abe Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:56:22 +0100 Subject: [PATCH 5/5] Added Account log for registration --- apps/hub/app/(auth)/login/_components/action.ts | 11 ++++++----- apps/hub/app/(auth)/register/_components/Register.tsx | 4 ++++ apps/hub/app/(auth)/register/action.ts | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/hub/app/(auth)/login/_components/action.ts b/apps/hub/app/(auth)/login/_components/action.ts index 87af797e..155a1ebc 100644 --- a/apps/hub/app/(auth)/login/_components/action.ts +++ b/apps/hub/app/(auth)/login/_components/action.ts @@ -26,9 +26,10 @@ export async function getOrSetDeviceId() { export const logAction = async ( type: LOG_TYPE, otherValues?: { - field: string; - oldValue: string; - newValue: string; + field?: string; + oldValue?: string; + newValue?: string; + userId?: string; }, ) => { const headersList = await headers(); @@ -40,7 +41,7 @@ export const logAction = async ( headersList.get("X-Real-IP"); const deviceId = await getOrSetDeviceId(); - if (type == "LOGIN") { + if (type == "LOGIN" || type == "REGISTER") { const existingLogs = await prisma.log.findMany({ where: { type: "LOGIN", @@ -77,7 +78,7 @@ export const logAction = async ( data: { type, browser: headersList.get("user-agent") || "unknown", - userId: user?.user.id, + userId: user?.user.id || otherValues?.userId, deviceId: deviceId, ip, ...otherValues, diff --git a/apps/hub/app/(auth)/register/_components/Register.tsx b/apps/hub/app/(auth)/register/_components/Register.tsx index 2990cdeb..0c67eb6e 100644 --- a/apps/hub/app/(auth)/register/_components/Register.tsx +++ b/apps/hub/app/(auth)/register/_components/Register.tsx @@ -9,6 +9,7 @@ import { useState } from "react"; import { Button } from "../../../_components/ui/Button"; import { sendVerificationLink } from "(app)/admin/user/action"; import toast from "react-hot-toast"; +import { logAction } from "(auth)/login/_components/action"; export const Register = () => { const schema = z @@ -93,6 +94,9 @@ export const Register = () => { return; } await sendVerificationLink(user.id); + await logAction("REGISTER", { + userId: user.id, + }); await signIn("credentials", { callbackUrl: "/", email: user.email, diff --git a/apps/hub/app/(auth)/register/action.ts b/apps/hub/app/(auth)/register/action.ts index 5404372b..6ba13948 100644 --- a/apps/hub/app/(auth)/register/action.ts +++ b/apps/hub/app/(auth)/register/action.ts @@ -53,5 +53,6 @@ export const register = async ({ password, ...user }: Omit