Continue Account log
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -23,10 +23,6 @@ const page = async () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
53
apps/hub/app/(auth)/login/_components/action.ts
Normal file
53
apps/hub/app/(auth)/login/_components/action.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
22
packages/database/prisma/schema/log.prisma
Normal file
22
packages/database/prisma/schema/log.prisma
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "logs" ADD COLUMN "deviceId" TEXT;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "logs" ADD COLUMN "field" TEXT,
|
||||
ADD COLUMN "newValue" TEXT,
|
||||
ADD COLUMN "oldValue" TEXT;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user