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