Continue Account log
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { PersonIcon } from "@radix-ui/react-icons";
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
import { prisma } from "@repo/db";
|
import { Log, prisma } from "@repo/db";
|
||||||
import {
|
import {
|
||||||
AdminForm,
|
AdminForm,
|
||||||
ConnectionHistory,
|
ConnectionHistory,
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
} from "./_components/forms";
|
} from "./_components/forms";
|
||||||
import { Error } from "../../../../_components/Error";
|
import { Error } from "../../../../_components/Error";
|
||||||
import { getUserPenaltys } from "@repo/shared-components";
|
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 }> }) {
|
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
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({
|
const formerDiscordAccounts = await prisma.formerDiscordAccount.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
@@ -152,6 +174,41 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||||||
openTimebans={openTimeban}
|
openTimebans={openTimeban}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
|
||||||
<UserReports user={user} />
|
<UserReports user={user} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ const page = async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
orderBy: {
|
|
||||||
id: "desc",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import toast from "react-hot-toast";
|
|||||||
import { CircleAlert, Trash2 } from "lucide-react";
|
import { CircleAlert, Trash2 } from "lucide-react";
|
||||||
import { deleteUser, sendVerificationLink } from "(app)/admin/user/action";
|
import { deleteUser, sendVerificationLink } from "(app)/admin/user/action";
|
||||||
import { setStandardName } from "../../../../helper/discord";
|
import { setStandardName } from "../../../../helper/discord";
|
||||||
|
import { logAction } from "(auth)/login/_components/action";
|
||||||
|
|
||||||
export const ProfileForm = ({
|
export const ProfileForm = ({
|
||||||
user,
|
user,
|
||||||
@@ -101,6 +102,28 @@ export const ProfileForm = ({
|
|||||||
userId: user.id,
|
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);
|
form.reset(values);
|
||||||
if (user.email !== values.email) {
|
if (user.email !== values.email) {
|
||||||
await sendVerificationLink(user.id);
|
await sendVerificationLink(user.id);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Toaster, toast } from "react-hot-toast";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Button } from "../../../_components/ui/Button";
|
import { Button } from "../../../_components/ui/Button";
|
||||||
import { useErrorBoundary } from "react-error-boundary";
|
import { useErrorBoundary } from "react-error-boundary";
|
||||||
|
import { logAction } from "./action";
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const { showBoundary } = useErrorBoundary();
|
const { showBoundary } = useErrorBoundary();
|
||||||
@@ -46,6 +47,10 @@ export const Login = () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
|
await logAction("LOGIN");
|
||||||
redirect(searchParams.get("redirect") || "/");
|
redirect(searchParams.get("redirect") || "/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showBoundary(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 ({
|
export const setStandardName = async ({
|
||||||
memberId,
|
memberId,
|
||||||
userId,
|
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())
|
id Int @id @default(autoincrement())
|
||||||
userId String
|
userId String
|
||||||
createdUserId String?
|
createdUserId String?
|
||||||
reportId Int?
|
reportId Int?
|
||||||
|
|
||||||
// Generalized action type to cover penalties and user history events
|
type PenaltyType
|
||||||
action AuditLogAction?
|
reason String
|
||||||
reason String?
|
|
||||||
until DateTime?
|
until DateTime?
|
||||||
|
|
||||||
suspended Boolean @default(false)
|
suspended Boolean @default(false)
|
||||||
@@ -14,18 +13,14 @@ model AuditLog {
|
|||||||
timestamp DateTime @default(now())
|
timestamp DateTime @default(now())
|
||||||
|
|
||||||
// relations:
|
// 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])
|
CreatedUser User? @relation("CreatedPenalties", fields: [createdUserId], references: [id])
|
||||||
Report Report? @relation(fields: [reportId], references: [id])
|
Report Report? @relation(fields: [reportId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuditLogAction {
|
enum PenaltyType {
|
||||||
// Penalty actions
|
|
||||||
KICK
|
KICK
|
||||||
TIME_BAN
|
TIME_BAN
|
||||||
PERMISSIONS_REVOCED
|
PERMISSIONS_REVOCED
|
||||||
BAN
|
BAN
|
||||||
// User history events
|
|
||||||
USER_DELETED
|
|
||||||
USER_PROFILE_UPDATED
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ model Report {
|
|||||||
Sender User? @relation("SentReports", fields: [senderUserId], references: [id])
|
Sender User? @relation("SentReports", fields: [senderUserId], references: [id])
|
||||||
Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade)
|
Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade)
|
||||||
Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
|
Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
|
||||||
AuditLog AuditLog[]
|
Penalties Penalty[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,14 +82,13 @@ model User {
|
|||||||
ConnectedDispatcher ConnectedDispatcher[]
|
ConnectedDispatcher ConnectedDispatcher[]
|
||||||
ConnectedAircraft ConnectedAircraft[]
|
ConnectedAircraft ConnectedAircraft[]
|
||||||
PositionLog PositionLog[]
|
PositionLog PositionLog[]
|
||||||
Penaltys AuditLog[]
|
Penaltys Penalty[] @relation("User")
|
||||||
CreatedAuditLogEntrys AuditLog[] @relation("CreatedAuditLogEntrys")
|
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
||||||
Bookings Booking[]
|
Logs Log[]
|
||||||
auditLogs AuditLog[]
|
|
||||||
|
|
||||||
|
Bookings Booking[]
|
||||||
DiscordAccount DiscordAccount?
|
DiscordAccount DiscordAccount?
|
||||||
FormerDiscordAccounts FormerDiscordAccount[]
|
FormerDiscordAccounts FormerDiscordAccount[]
|
||||||
auditLogs AuditLog[]
|
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user