Continue Account log

This commit is contained in:
PxlLoewe
2026-01-30 00:25:51 +01:00
parent 005509598c
commit e4aae9804b
15 changed files with 224 additions and 24 deletions

View File

@@ -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}
/>
</div>
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
<PaginatedTable
prismaModel={"log"}
columns={
[
{
header: "Zeitstempel",
accessorKey: "timestamp",
cell: (info) => new Date(info.getValue<string>()).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<Log>[]
}
/>
</div>
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
<UserReports user={user} />
</div>

View File

@@ -23,10 +23,6 @@ const page = async () => {
},
},
},
orderBy: {
id: "desc",
},
});
return (

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
},
});
};

View File

@@ -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,

View File

@@ -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
}

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -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;

View File

@@ -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";

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "logs" ADD COLUMN "deviceId" TEXT;

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "logs" ADD COLUMN "field" TEXT,
ADD COLUMN "newValue" TEXT,
ADD COLUMN "oldValue" TEXT;

View File

@@ -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
}

View File

@@ -13,5 +13,5 @@ model Report {
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[]
Penalties Penalty[]
}

View File

@@ -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")
}