diff --git a/apps/hub/app/(app)/admin/log/layout.tsx b/apps/hub/app/(app)/admin/log/layout.tsx
new file mode 100644
index 00000000..6a96b8ef
--- /dev/null
+++ b/apps/hub/app/(app)/admin/log/layout.tsx
@@ -0,0 +1,19 @@
+import { Error } from "_components/Error";
+import { getServerSession } from "api/auth/[...nextauth]/auth";
+
+const AdminAccountLogLayout = async ({ children }: { children: React.ReactNode }) => {
+ const session = await getServerSession();
+
+ if (!session) return ;
+
+ const user = session.user;
+
+ if (!user?.permissions.includes("ADMIN_USER_ADVANCED"))
+ return ;
+
+ return <>{children}>;
+};
+
+AdminAccountLogLayout.displayName = "AdminAccountLogLayout";
+
+export default AdminAccountLogLayout;
diff --git a/apps/hub/app/(app)/admin/log/page.tsx b/apps/hub/app/(app)/admin/log/page.tsx
new file mode 100644
index 00000000..e233d1ef
--- /dev/null
+++ b/apps/hub/app/(app)/admin/log/page.tsx
@@ -0,0 +1,91 @@
+"use client";
+import { LogsIcon } from "lucide-react";
+import { PaginatedTable } from "../../../_components/PaginatedTable";
+import Link from "next/link";
+import { ColumnDef } from "@tanstack/react-table";
+import { Log, Prisma, User } from "@repo/db";
+
+export default () => {
+ return (
+ <>
+
+ ({
+ OR: [
+ {
+ User: {
+ firstname: { contains: searchTerm, mode: "insensitive" },
+ lastname: { contains: searchTerm, mode: "insensitive" },
+ publicId: { contains: searchTerm, mode: "insensitive" },
+ },
+ },
+ { deviceId: { contains: searchTerm, mode: "insensitive" } },
+ { ip: { contains: searchTerm, mode: "insensitive" } },
+ ],
+ }) as Prisma.LogWhereInput
+ }
+ columns={
+ [
+ {
+ header: "ID",
+ accessorKey: "id",
+ },
+ {
+ 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",
+ accessorKey: "ip",
+ },
+ {
+ header: "Browser-ID",
+ accessorKey: "deviceId",
+ },
+ {
+ header: "Zeitstempel",
+ accessorKey: "timestamp",
+ cell: (info) => new Date(info.getValue()).toLocaleString("de-DE"),
+ },
+ {
+ 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[]
+ }
+ leftOfSearch={
+
+ Account Log
+
+ }
+ />
+ >
+ );
+};
diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx
index 15399821..e1739cab 100644
--- a/apps/hub/app/(app)/settings/_components/forms.tsx
+++ b/apps/hub/app/(app)/settings/_components/forms.tsx
@@ -102,8 +102,13 @@ export const ProfileForm = ({
userId: user.id,
});
}
+ const ip = await fetch("https://api.ipify.org/?format=json")
+ .then((res) => res.json())
+ .then((data) => data.ip);
+
if (user.firstname !== values.firstname) {
await logAction("PROFILE_CHANGE", {
+ ip,
field: "firstname",
oldValue: user.firstname,
newValue: values.firstname,
@@ -111,6 +116,7 @@ export const ProfileForm = ({
}
if (user.lastname !== values.lastname) {
await logAction("PROFILE_CHANGE", {
+ ip,
field: "lastname",
oldValue: user.lastname,
newValue: values.lastname,
@@ -118,6 +124,7 @@ export const ProfileForm = ({
}
if (user.email !== values.email) {
await logAction("PROFILE_CHANGE", {
+ ip,
field: "email",
oldValue: user.email,
newValue: values.email,
diff --git a/apps/hub/app/(auth)/login/_components/Login.tsx b/apps/hub/app/(auth)/login/_components/Login.tsx
index f22db1d0..4cd85f48 100644
--- a/apps/hub/app/(auth)/login/_components/Login.tsx
+++ b/apps/hub/app/(auth)/login/_components/Login.tsx
@@ -48,9 +48,11 @@ export const Login = () => {
return;
}
- console.log("data", data);
+ const ip = await fetch("https://api.ipify.org/?format=json")
+ .then((res) => res.json())
+ .then((data) => data.ip);
- await logAction("LOGIN");
+ await logAction("LOGIN", { ip });
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
index dc120a02..d32c300b 100644
--- a/apps/hub/app/(auth)/login/_components/action.ts
+++ b/apps/hub/app/(auth)/login/_components/action.ts
@@ -26,6 +26,7 @@ export async function getOrSetDeviceId() {
export const logAction = async (
type: LOG_TYPE,
otherValues?: {
+ ip: string;
field?: string;
oldValue?: string;
newValue?: string;
@@ -35,13 +36,6 @@ export const logAction = async (
const headersList = await headers();
const user = await getServerSession();
- console.log(Array.from(headersList.entries()));
-
- const ip =
- headersList.get("X-Forwarded-For") ||
- headersList.get("Forwarded") ||
- headersList.get("X-Real-IP");
-
const deviceId = await getOrSetDeviceId();
if (type == "LOGIN" || type == "REGISTER") {
const existingLogs = await prisma.log.findMany({
@@ -52,7 +46,7 @@ export const logAction = async (
},
OR: [
{
- ip: ip,
+ ip: otherValues?.ip,
},
{
deviceId: deviceId,
@@ -82,7 +76,7 @@ export const logAction = async (
browser: headersList.get("user-agent") || "unknown",
userId: user?.user.id || otherValues?.userId,
deviceId: deviceId,
- ip,
+ ip: otherValues?.ip,
...otherValues,
},
});
diff --git a/apps/hub/app/(auth)/register/_components/Register.tsx b/apps/hub/app/(auth)/register/_components/Register.tsx
index 0c67eb6e..11aad2b8 100644
--- a/apps/hub/app/(auth)/register/_components/Register.tsx
+++ b/apps/hub/app/(auth)/register/_components/Register.tsx
@@ -94,7 +94,12 @@ export const Register = () => {
return;
}
await sendVerificationLink(user.id);
+ const ip = await fetch("https://api.ipify.org/?format=json")
+ .then((res) => res.json())
+ .then((data) => data.ip);
+
await logAction("REGISTER", {
+ ip: ip,
userId: user.id,
});
await signIn("credentials", {
diff --git a/apps/hub/app/_components/Nav.tsx b/apps/hub/app/_components/Nav.tsx
index fbff5816..35790d48 100644
--- a/apps/hub/app/_components/Nav.tsx
+++ b/apps/hub/app/_components/Nav.tsx
@@ -6,7 +6,6 @@ import {
RocketIcon,
ReaderIcon,
DownloadIcon,
- UpdateIcon,
ActivityLogIcon,
} from "@radix-ui/react-icons";
import Link from "next/link";
@@ -14,7 +13,7 @@ import { WarningAlert } from "./ui/PageAlert";
import { getServerSession } from "api/auth/[...nextauth]/auth";
import { Error } from "./Error";
import Image from "next/image";
-import { Loader, Plane, Radar, Workflow } from "lucide-react";
+import { Plane, Radar, Workflow } from "lucide-react";
import { BookingButton } from "./BookingButton";
export const VerticalNav = async () => {
@@ -103,6 +102,11 @@ export const VerticalNav = async () => {
Audit-Log
)}
+ {session.user.permissions.includes("ADMIN_USER_ADVANCED") && (
+
+ Account Log
+
+ )}
{session.user.permissions.includes("ADMIN_CHANGELOG") && (
Changelog