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 (