diff --git a/package-lock.json b/package-lock.json index 08512fc..b77e7c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1788,6 +1788,35 @@ "react-transition-group": "^4.4.0" } }, + "@material-ui/data-grid": { + "version": "4.0.0-alpha.26", + "resolved": "https://registry.npmjs.org/@material-ui/data-grid/-/data-grid-4.0.0-alpha.26.tgz", + "integrity": "sha512-mA2mncqQAISRLl7FR5NSmlus0wLQ5MSbIjg7WJyGKoOojNwEhK46nmYPvukljXMRoaKQpiv4U+ULI5CuuxlXgQ==", + "requires": { + "@material-ui/utils": "^5.0.0-alpha.14", + "prop-types": "^15.7.2", + "reselect": "^4.0.0" + }, + "dependencies": { + "@material-ui/utils": { + "version": "5.0.0-alpha.31", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-5.0.0-alpha.31.tgz", + "integrity": "sha512-4OzVD12+HbfWMftwiHCBforgjkhzbWMdK9GTQLQcekjdG2qpi41BGvanPpHjlxegzou0A2MEaULBvWqsKrUP9A==", + "requires": { + "@babel/runtime": "^7.4.4", + "@types/prop-types": "^15.7.3", + "@types/react-is": "^16.7.1 || ^17.0.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.0" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "@material-ui/icons": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", @@ -2432,6 +2461,14 @@ "@types/react": "*" } }, + "@types/react-is": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.0.tgz", + "integrity": "sha512-A0DQ1YWZ0RG2+PV7neAotNCIh8gZ3z7tQnDJyS2xRPDNtAtSPcJ9YyfMP8be36Ha0kQRzbZCrrTMznA4blqO5g==", + "requires": { + "@types/react": "*" + } + }, "@types/react-router": { "version": "5.1.14", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.14.tgz", @@ -13187,6 +13224,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", diff --git a/package.json b/package.json index f699481..01a4d55 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@material-ui/core": "^4.11.4", + "@material-ui/data-grid": "^4.0.0-alpha.26", "@material-ui/icons": "^4.11.2", "@testing-library/jest-dom": "^5.12.0", "@testing-library/react": "^11.2.6", diff --git a/src/helpers/AccountHelper.ts b/src/helpers/AccountHelper.ts index 397473c..facbb65 100644 --- a/src/helpers/AccountHelper.ts +++ b/src/helpers/AccountHelper.ts @@ -17,7 +17,7 @@ export interface AdminAccount { name: string; email: string; time_create: number; - roles: Array<"manage_admins" | "manage_users" | "access_full_admin_logs">; + roles: Array<"manage_admins" | "manage_users" | "access_all_admin_logs">; } export interface NewAdminGeneralSettings { diff --git a/src/helpers/AdminLogsHelper.ts b/src/helpers/AdminLogsHelper.ts new file mode 100644 index 0000000..c1f0772 --- /dev/null +++ b/src/helpers/AdminLogsHelper.ts @@ -0,0 +1,30 @@ +import { serverRequest } from "./APIHelper"; + +export interface AdminLogMessage { + id: number; + admin_id: number; + ip: string; + time: number; + action: string; + args: any; + format: string; +} + +export class AdminLogsHelper { + /** + * Get the list of admin log actions from the server + */ + static async GetLogs(): Promise { + return (await serverRequest("logs/list")).map((el: any) => { + if (typeof el.action === "string") return el; + + for (let key in el.action) { + if (!el.action.hasOwnProperty(key)) continue; + + el.args = el.action[key]; + el.action = key; + } + return el; + }); + } +} diff --git a/src/ui/routes/AccountLogsRoute.tsx b/src/ui/routes/AccountLogsRoute.tsx new file mode 100644 index 0000000..85e7609 --- /dev/null +++ b/src/ui/routes/AccountLogsRoute.tsx @@ -0,0 +1,143 @@ +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@material-ui/core"; +import React from "react"; +import { AccountHelper, AdminAccount } from "../../helpers/AccountHelper"; +import { + AdminLogMessage, + AdminLogsHelper, +} from "../../helpers/AdminLogsHelper"; +import { AsyncWidget } from "../widgets/AsyncWidget"; +import { PageTitle } from "../widgets/PageTitle"; +import { TimestampWidget } from "../widgets/TimestampWidget"; + +/** + * View admin logs + * + * @author Pierre Hubert + */ +export class AccountLogsRoute extends React.Component< + {}, + { logs: AdminLogMessage[]; admins: AdminAccount[] } +> { + constructor(p: any) { + super(p); + + this.load = this.load.bind(this); + this.build = this.build.bind(this); + } + + async load() { + const admins = await AccountHelper.GetAdminsList(); + const logs = await AdminLogsHelper.GetLogs(); + + this.setState({ + admins: admins, + logs: logs, + }); + } + + getAdminName(id: number): string { + const admin = this.state.admins.find((a) => a.id === id); + return admin ? admin.name : "Unknown admin"; + } + + build() { + return ( +
+ + + + + + + Admin + Time + IP address + Action + Details + + + + {this.state.logs.reverse().map((message) => { + let formattedMessage = message.format; + for (let arg in message.args) { + if (message.args.hasOwnProperty(arg)) + formattedMessage = + formattedMessage.replace( + "{" + arg + "}", + message.args[arg] + ); + } + + // Handle [admin] tag + const regex: any = + "\\[admin\\][0-9]+\\[/admin\\]"; + const adminsReferences = Array.from( + formattedMessage.matchAll(regex) + ); + + for (let entry of adminsReferences) { + const pattern = entry[0]; + const adminId = Number( + entry[0].split("]")[1].split("[")[0] + ); + const adminName = + this.getAdminName(adminId); + formattedMessage = formattedMessage.replace( + pattern, + adminName + ); + } + + return ( + + + {this.getAdminName( + message.admin_id + )} + + + + + + {message.ip} + + + {message.action} + + + {formattedMessage} + + + ); + })} + +
+
+

+ Note: your old activity records are automatically deleted + after a period of time. +

+
+ ); + } + + render() { + return ( + + ); + } +} diff --git a/src/ui/routes/MainRoute.tsx b/src/ui/routes/MainRoute.tsx index a5aab9f..f7cb5e4 100644 --- a/src/ui/routes/MainRoute.tsx +++ b/src/ui/routes/MainRoute.tsx @@ -20,6 +20,7 @@ import { import { Home, Person } from "@material-ui/icons"; import GroupIcon from "@material-ui/icons/Group"; import CloseSharpIcon from "@material-ui/icons/CloseSharp"; +import HistoryIcon from "@material-ui/icons/History"; import React from "react"; import { BrowserRouter as Router, @@ -33,6 +34,7 @@ import { AccountSettingsRoute } from "./AccountSettingsRoute"; import { HomeRoute } from "./HomeRoute"; import { NotFoundRoute } from "./NotFoundRoute"; import { AccountsListRoute } from "./AccountsListRoute"; +import { AccountLogsRoute } from "./AccountLogsRoute"; const useStyles = makeStyles((theme) => ({ root: { @@ -93,6 +95,11 @@ function Menu() { icon={} uri="/accounts" /> + } + uri="/logs" + /> - + + + + +