Skip to content

Commit 2b88f56

Browse files
committed
Audit log table and modal
1 parent e44748e commit 2b88f56

File tree

24 files changed

+425
-12
lines changed

24 files changed

+425
-12
lines changed

backend/internal/audit-log.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ const internalAuditLog = {
3636
return await query;
3737
},
3838

39+
/**
40+
* @param {Access} access
41+
* @param {Object} [data]
42+
* @param {Integer} [data.id] Defaults to the token user
43+
* @param {Array} [data.expand]
44+
* @return {Promise}
45+
*/
46+
get: async (access, data) => {
47+
await access.can("auditlog:list");
48+
49+
const query = auditLogModel
50+
.query()
51+
.andWhere("id", data.id)
52+
.allowGraph("[user]")
53+
.first();
54+
55+
if (typeof data.expand !== "undefined" && data.expand !== null) {
56+
query.withGraphFetched(`[${data.expand.join(", ")}]`);
57+
}
58+
59+
const row = await query;
60+
61+
if (!row?.id) {
62+
throw new errs.ItemNotFoundError(data.id);
63+
}
64+
65+
return row;
66+
},
67+
3968
/**
4069
* This method should not be publicly used, it doesn't check certain things. It will be assumed
4170
* that permission to add to audit log is already considered, however the access token is used for

backend/routes/audit-log.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,56 @@ router
5252
}
5353
});
5454

55+
/**
56+
* Specific audit log entry
57+
*
58+
* /api/audit-log/123
59+
*/
60+
router
61+
.route("/:event_id")
62+
.options((_, res) => {
63+
res.sendStatus(204);
64+
})
65+
.all(jwtdecode())
66+
67+
/**
68+
* GET /api/audit-log/123
69+
*
70+
* Retrieve a specific entry
71+
*/
72+
.get(async (req, res, next) => {
73+
try {
74+
const data = await validator(
75+
{
76+
required: ["event_id"],
77+
additionalProperties: false,
78+
properties: {
79+
event_id: {
80+
$ref: "common#/properties/id",
81+
},
82+
expand: {
83+
$ref: "common#/properties/expand",
84+
},
85+
},
86+
},
87+
{
88+
event_id: req.params.event_id,
89+
expand:
90+
typeof req.query.expand === "string"
91+
? req.query.expand.split(",")
92+
: null,
93+
},
94+
);
95+
96+
const item = await internalAuditLog.get(res.locals.access, {
97+
id: data.event_id,
98+
expand: data.expand,
99+
});
100+
res.status(200).send(item);
101+
} catch (err) {
102+
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
103+
next(err);
104+
}
105+
});
106+
55107
export default router;

frontend/src/api/backend/getAuditLog.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as api from "./base";
2+
import type { AuditLogExpansion } from "./getAuditLogs";
23
import type { AuditLog } from "./models";
34

4-
export async function getAuditLog(expand?: string[], params = {}): Promise<AuditLog[]> {
5+
export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> {
56
return await api.get({
6-
url: "/audit-log",
7+
url: `/audit-log/${id}`,
78
params: {
89
expand: expand?.join(","),
910
...params,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as api from "./base";
2+
import type { AuditLog } from "./models";
3+
4+
export type AuditLogExpansion = "user";
5+
6+
export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> {
7+
return await api.get({
8+
url: "/audit-log",
9+
params: {
10+
expand: expand?.join(","),
11+
...params,
12+
},
13+
});
14+
}

frontend/src/api/backend/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from "./downloadCertificate";
1616
export * from "./getAccessList";
1717
export * from "./getAccessLists";
1818
export * from "./getAuditLog";
19+
export * from "./getAuditLogs";
1920
export * from "./getCertificate";
2021
export * from "./getCertificates";
2122
export * from "./getDeadHost";

frontend/src/api/backend/models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface AuditLog {
4040
objectId: number;
4141
action: string;
4242
meta: Record<string, any>;
43+
// Expansions:
44+
user?: User;
4345
}
4446

4547
export interface AccessList {

frontend/src/components/Table/Formatter/DomainsFormatter.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { intlFormat, parseISO } from "date-fns";
2-
import { intl } from "src/locale";
1+
import { DateTimeFormat, intl } from "src/locale";
32

43
interface Props {
54
domains: string[];
@@ -17,7 +16,7 @@ export function DomainsFormatter({ domains, createdOn }: Props) {
1716
</div>
1817
{createdOn ? (
1918
<div className="text-secondary mt-1">
20-
{intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })}
19+
{intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })}
2120
</div>
2221
) : null}
2322
</div>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { IconUser } from "@tabler/icons-react";
2+
import type { AuditLog } from "src/api/backend";
3+
import { DateTimeFormat, intl } from "src/locale";
4+
5+
const getEventTitle = (event: AuditLog) => (
6+
<span>{intl.formatMessage({ id: `event.${event.action}-${event.objectType}` })}</span>
7+
);
8+
9+
const getEventValue = (event: AuditLog) => {
10+
switch (event.objectType) {
11+
case "user":
12+
return event.meta?.name;
13+
default:
14+
return `UNKNOWN EVENT TYPE: ${event.objectType}`;
15+
}
16+
};
17+
18+
const getColorForAction = (action: string) => {
19+
switch (action) {
20+
case "created":
21+
return "text-lime";
22+
case "deleted":
23+
return "text-red";
24+
default:
25+
return "text-blue";
26+
}
27+
};
28+
29+
const getIcon = (row: AuditLog) => {
30+
const c = getColorForAction(row.action);
31+
let ico = null;
32+
switch (row.objectType) {
33+
case "user":
34+
ico = <IconUser size={16} className={c} />;
35+
break;
36+
}
37+
38+
return ico;
39+
};
40+
41+
interface Props {
42+
row: AuditLog;
43+
}
44+
export function EventFormatter({ row }: Props) {
45+
return (
46+
<div className="flex-fill">
47+
<div className="font-weight-medium">
48+
{getIcon(row)} {getEventTitle(row)} &mdash; <span className="badge">{getEventValue(row)}</span>
49+
</div>
50+
<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div>
51+
</div>
52+
);
53+
}

frontend/src/components/Table/Formatter/ValueWithDateFormatter.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { intlFormat, parseISO } from "date-fns";
2-
import { intl } from "src/locale";
1+
import { DateTimeFormat, intl } from "src/locale";
32

43
interface Props {
54
value: string;
@@ -16,7 +15,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
1615
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
1716
{disabled
1817
? intl.formatMessage({ id: "disabled" })
19-
: intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })}
18+
: intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })}
2019
</div>
2120
) : null}
2221
</div>

frontend/src/components/Table/Formatter/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./CertificateFormatter";
22
export * from "./DomainsFormatter";
33
export * from "./EmailFormatter";
4+
export * from "./EventFormatter";
45
export * from "./GravatarFormatter";
56
export * from "./RolesFormatter";
67
export * from "./StatusFormatter";

0 commit comments

Comments
 (0)