strapi-cms-audit-log 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +12 -12
- package/dist/admin/{App-BLt4lqEM.js → App-CmbG2k9E.js} +110 -100
- package/dist/admin/{App-ou5hy99f.mjs → App-jWvaMXIw.mjs} +113 -103
- package/dist/admin/{en-BMlJxq3g.js → en-Bs4tlklI.js} +11 -9
- package/dist/admin/{en-B0rPiE2W.mjs → en-C3Og1VyK.mjs} +11 -9
- package/dist/admin/{index-DdmY-p3Q.mjs → index-BIIMJgKU.mjs} +3 -5
- package/dist/admin/{index-BYi8OPTw.js → index-BsVOGSDf.js} +3 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/AuditLogTable.d.ts +1 -2
- package/dist/admin/src/components/IntegrityStatus.d.ts +13 -0
- package/dist/admin/src/hooks/useAuditLogs.d.ts +14 -3
- package/dist/admin/src/permissions.d.ts +0 -8
- package/dist/admin/src/types.d.ts +26 -2
- package/dist/server/index.js +321 -94
- package/dist/server/index.mjs +321 -94
- package/dist/server/src/config/index.d.ts +1 -0
- package/dist/server/src/constants.d.ts +24 -2
- package/dist/server/src/content-types/audit-log/index.d.ts +8 -0
- package/dist/server/src/content-types/audit-log/schema.d.ts +21 -0
- package/dist/server/src/content-types/index.d.ts +8 -0
- package/dist/server/src/controllers/audit-log.d.ts +5 -3
- package/dist/server/src/controllers/index.d.ts +4 -4
- package/dist/server/src/index.d.ts +36 -7
- package/dist/server/src/register.d.ts +7 -3
- package/dist/server/src/routes/admin.d.ts +8 -0
- package/dist/server/src/services/audit.d.ts +0 -1
- package/dist/server/src/services/correlation.d.ts +35 -0
- package/dist/server/src/services/immutability.d.ts +5 -5
- package/dist/server/src/services/index.d.ts +21 -1
- package/dist/server/src/services/integrity.d.ts +69 -0
- package/dist/server/src/types/index.d.ts +54 -4
- package/package.json +10 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,84 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
6
|
the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] — 2026-11-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Building the list page's filter options no longer takes extra pooled
|
|
12
|
+
database connections. `readStoredOptions` ran its six `DISTINCT` queries on
|
|
13
|
+
raw `strapi.db.connection`, which from inside an open transaction takes a
|
|
14
|
+
second connection per query and can exhaust a small pool. The queries now run
|
|
15
|
+
in turn on a single transaction connection.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Removed the upper bound on the supported Node.js version; `engines.node` is
|
|
19
|
+
now `>=18.0.0`.
|
|
20
|
+
|
|
21
|
+
## [1.2.0] — 2026-09-11
|
|
22
|
+
|
|
23
|
+
Remediates every finding of the 10 September OWASP ASVS Level 2 (V7) assessment.
|
|
24
|
+
The audit log can no longer be pruned by anyone, can prove it has not been, and
|
|
25
|
+
carries a correlation id on every request-borne record.
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
**F-2 — audit records were deletable (HIGH, V7.3.1 / 7.3.3)**
|
|
30
|
+
- The `DELETE /audit-log/logs/:id` route, its controller handler, the
|
|
31
|
+
`plugin::audit-log.delete` RBAC action and the per-row delete button are
|
|
32
|
+
**removed**. There is no permission an administrator could grant to bring
|
|
33
|
+
deletion back. Strapi drops the stale grants from the database at the first
|
|
34
|
+
boot after upgrading.
|
|
35
|
+
- `strapi.documents(uid).delete()` on the audit type now throws, alongside
|
|
36
|
+
update, publish, unpublish, discardDraft and clone.
|
|
37
|
+
- The retention purge — the one deletion that remains — **records itself** as
|
|
38
|
+
`retention.purge`, with the count, the cutoff and the configured window. It is
|
|
39
|
+
not gated by `securityEvents`; there is no configuration in which a purge
|
|
40
|
+
should be silent.
|
|
41
|
+
- **Hash chain.** Every record stores `hash` (SHA-256 over its content fields,
|
|
42
|
+
`createdAt` and `prevHash`) and `prevHash`. Editing, removing or inserting a
|
|
43
|
+
record is detectable, and the verifier says at which row. Writes are
|
|
44
|
+
serialised — an in-process queue plus a PostgreSQL transaction-scoped
|
|
45
|
+
advisory lock for multi-replica deployments — so concurrent records cannot
|
|
46
|
+
fork the chain. `createdAt` is now set by the plugin rather than the database
|
|
47
|
+
so it can be part of the hash.
|
|
48
|
+
- New `integrity` service with `verify()`, new `GET /audit-log/integrity` route
|
|
49
|
+
(behind `read`), and a verdict line at the top of the list page on every visit.
|
|
50
|
+
- Rows written before 1.2.0 have no hash. They are counted as `legacy` and never
|
|
51
|
+
backfilled — a hash computed today over a row written last week vouches for
|
|
52
|
+
nothing about last week.
|
|
53
|
+
|
|
54
|
+
**F-3 — no off-box copy (MEDIUM, V7.3.3)**
|
|
55
|
+
- The forwarded log line now includes `id`, `hash` and `prevHash`, so the
|
|
56
|
+
collector's copy carries the chain and a row that has vanished from the
|
|
57
|
+
database is still provably in the external log. The consuming project should
|
|
58
|
+
set `forwardToLogger: true`; the mechanism was already there, switched off.
|
|
59
|
+
|
|
60
|
+
**F-4 — correlation ids absent on ~90% of records (LOW, V7.1.x)**
|
|
61
|
+
- New `correlation` middleware, registered ahead of the access middleware from
|
|
62
|
+
the plugin's `register` lifecycle: honours an inbound `x-request-id` /
|
|
63
|
+
`x-correlation-id` / `x-amzn-trace-id` when it is shaped like an id, mints a
|
|
64
|
+
UUID otherwise, and echoes it back as `X-Request-Id`. Every record written
|
|
65
|
+
inside a request now has one. New `correlationId` option, default `true`.
|
|
66
|
+
|
|
67
|
+
**F-1 — failed logins not observed (HIGH, V7.2.1)**
|
|
68
|
+
- No defect found: the listener is correct and the assessment created no
|
|
69
|
+
records to observe. A test now drives the exact shape Strapi's login
|
|
70
|
+
controller produces — an un-awaited `emit` followed by a synchronous throw —
|
|
71
|
+
and asserts the row lands. The README gains a one-line production check.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- `AuditMaintenanceAction` (`'retention.purge'`), `IntegrityReport`, and the
|
|
76
|
+
`admin::audit-log` subject.
|
|
77
|
+
- `hash` and `prevHash` on `AuditLog`; both shown on the detail page.
|
|
78
|
+
- ASVS V7 mapping table in the README.
|
|
79
|
+
|
|
80
|
+
### Removed
|
|
81
|
+
|
|
82
|
+
- `PERMISSIONS.delete`, `useDeleteAuditLog`, the `delete.*` translations, and
|
|
83
|
+
`audit.deleteOne()`. Consumers that referenced any of them will fail to compile,
|
|
84
|
+
which is the intended way to find out.
|
|
85
|
+
|
|
8
86
|
## [1.1.0] — 2026-09-03
|
|
9
87
|
|
|
10
88
|
Adds the half of an audit trail that is not about content — authentication,
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# strapi-
|
|
1
|
+
# strapi-cms-audit-log
|
|
2
2
|
|
|
3
3
|
Automatic audit logging for **Strapi v5**, in two halves.
|
|
4
4
|
|
|
@@ -103,11 +103,11 @@ Mapped against the usual checklist:
|
|
|
103
103
|
## Installation
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
npm install strapi-
|
|
106
|
+
npm install strapi-cms-audit-log
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
yarn add strapi-
|
|
110
|
+
yarn add strapi-cms-audit-log
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Then enable it in `config/plugins.ts`:
|
|
@@ -126,7 +126,7 @@ sidebar entry are all created on boot.
|
|
|
126
126
|
| | |
|
|
127
127
|
|---|---|
|
|
128
128
|
| Strapi | `^5.0.0` (developed and tested against 5.52) |
|
|
129
|
-
| Node | `>=18
|
|
129
|
+
| Node | `>=18` |
|
|
130
130
|
| Database | Any Strapi v5 supports — PostgreSQL, MySQL/MariaDB, SQLite |
|
|
131
131
|
|
|
132
132
|
There is one manual step after installing: **grant the permission**. See
|
|
@@ -503,7 +503,7 @@ Worth alerting on: repeated `login.failed` from one `ipAddress`, any
|
|
|
503
503
|
Developing the plugin alongside a Strapi app usually means a path dependency:
|
|
504
504
|
|
|
505
505
|
```json
|
|
506
|
-
{ "dependencies": { "strapi-
|
|
506
|
+
{ "dependencies": { "strapi-cms-audit-log": "file:../strapi-cms-audit-log" } }
|
|
507
507
|
```
|
|
508
508
|
|
|
509
509
|
That works, with **one thing you must clean up afterwards**.
|
|
@@ -511,7 +511,7 @@ That works, with **one thing you must clean up afterwards**.
|
|
|
511
511
|
Yarn 1's `file:` protocol copies the directory wholesale. It does not honour the
|
|
512
512
|
`files` field and it does not skip `node_modules`, so the plugin's *development*
|
|
513
513
|
dependencies are installed into your app at
|
|
514
|
-
`node_modules/strapi-
|
|
514
|
+
`node_modules/strapi-cms-audit-log/node_modules/` — `@strapi/strapi`,
|
|
515
515
|
`@strapi/admin`, `react`, `react-dom`, `react-router-dom`, `react-intl` and
|
|
516
516
|
`styled-components` among them.
|
|
517
517
|
|
|
@@ -539,13 +539,13 @@ nothing wrong to find in any of them.
|
|
|
539
539
|
npm would have given you anyway:
|
|
540
540
|
|
|
541
541
|
```bash
|
|
542
|
-
rm -rf node_modules/strapi-
|
|
542
|
+
rm -rf node_modules/strapi-cms-audit-log/node_modules
|
|
543
543
|
```
|
|
544
544
|
|
|
545
545
|
Make it survive the next install with a `postinstall` script:
|
|
546
546
|
|
|
547
547
|
```json
|
|
548
|
-
{ "scripts": { "postinstall": "rm -rf node_modules/strapi-
|
|
548
|
+
{ "scripts": { "postinstall": "rm -rf node_modules/strapi-cms-audit-log/node_modules" } }
|
|
549
549
|
```
|
|
550
550
|
|
|
551
551
|
Then clear Vite's dependency cache (`rm -rf node_modules/.strapi/vite`), restart
|
|
@@ -555,7 +555,7 @@ Two more things about a path install, unrelated to the above:
|
|
|
555
555
|
|
|
556
556
|
- Yarn **copies** rather than links, so changes to the plugin need
|
|
557
557
|
`npm run build` in the plugin followed by re-copying `dist/` into
|
|
558
|
-
`node_modules/strapi-
|
|
558
|
+
`node_modules/strapi-cms-audit-log/dist`. A bare `yarn install` will not
|
|
559
559
|
refresh it — the resolution is unchanged, so yarn skips the copy.
|
|
560
560
|
- Strapi loads plugins at boot, so a restart is always required.
|
|
561
561
|
|
|
@@ -748,7 +748,7 @@ trusting a spoofable header.
|
|
|
748
748
|
### Why admin and server types are separate
|
|
749
749
|
|
|
750
750
|
The canonical types live in `server/src/types` and are re-exported from
|
|
751
|
-
`strapi-
|
|
751
|
+
`strapi-cms-audit-log/strapi-server`. The admin panel keeps its own copies of
|
|
752
752
|
the few shared unions in `admin/src/types.ts`.
|
|
753
753
|
|
|
754
754
|
That is a build constraint, not an oversight. `@strapi/sdk-plugin` compiles the
|
|
@@ -941,7 +941,7 @@ import type {
|
|
|
941
941
|
AuditUserConfig,
|
|
942
942
|
AuditWriteMode,
|
|
943
943
|
ContentTypeSelector,
|
|
944
|
-
} from 'strapi-
|
|
944
|
+
} from 'strapi-cms-audit-log/strapi-server';
|
|
945
945
|
```
|
|
946
946
|
|
|
947
947
|
`PLUGIN_ID`, `AUDIT_LOG_UID`, `PERMISSIONS` and `DEFAULT_IGNORED_FIELDS` are
|
|
@@ -979,7 +979,7 @@ two-field edit on a 173-widget page does not populate 173 widgets.
|
|
|
979
979
|
npm run watch:link
|
|
980
980
|
|
|
981
981
|
# in your Strapi project
|
|
982
|
-
npx yalc add --link strapi-
|
|
982
|
+
npx yalc add --link strapi-cms-audit-log && npm install
|
|
983
983
|
npm run develop
|
|
984
984
|
```
|
|
985
985
|
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
4
|
const admin = require("@strapi/strapi/admin");
|
|
5
5
|
const reactRouterDom = require("react-router-dom");
|
|
6
|
-
const index = require("./index-
|
|
6
|
+
const index = require("./index-BsVOGSDf.js");
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const reactIntl = require("react-intl");
|
|
9
9
|
const React = require("react");
|
|
@@ -53,7 +53,9 @@ const ACTION_COLORS = {
|
|
|
53
53
|
"media.delete": { background: "danger100", text: "danger600" },
|
|
54
54
|
"media-folder.create": { background: "success100", text: "success600" },
|
|
55
55
|
"media-folder.update": { background: "primary100", text: "primary600" },
|
|
56
|
-
"media-folder.delete": { background: "danger100", text: "danger600" }
|
|
56
|
+
"media-folder.delete": { background: "danger100", text: "danger600" },
|
|
57
|
+
// The log's own housekeeping. Neutral: expected, scheduled, and self-reported.
|
|
58
|
+
"retention.purge": { background: "neutral150", text: "neutral700" }
|
|
57
59
|
};
|
|
58
60
|
const actionColor = (action) => ACTION_COLORS[action] ?? { background: "neutral150", text: "neutral700" };
|
|
59
61
|
const shortContentTypeName = (uid) => {
|
|
@@ -588,6 +590,30 @@ const useAuditFilterOptions = () => {
|
|
|
588
590
|
}, [get]);
|
|
589
591
|
return { options, isLoading };
|
|
590
592
|
};
|
|
593
|
+
const useAuditIntegrity = () => {
|
|
594
|
+
const { get } = admin.useFetchClient();
|
|
595
|
+
const [data, setData] = React__namespace.useState(null);
|
|
596
|
+
const [isLoading, setIsLoading] = React__namespace.useState(true);
|
|
597
|
+
const [error, setError] = React__namespace.useState(null);
|
|
598
|
+
React__namespace.useEffect(() => {
|
|
599
|
+
let cancelled = false;
|
|
600
|
+
const load = async () => {
|
|
601
|
+
try {
|
|
602
|
+
const { data: response } = await get(`/${index.PLUGIN_ID}/integrity`);
|
|
603
|
+
if (!cancelled) setData(response.data);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
if (!cancelled) setError(message(err));
|
|
606
|
+
} finally {
|
|
607
|
+
if (!cancelled) setIsLoading(false);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
void load();
|
|
611
|
+
return () => {
|
|
612
|
+
cancelled = true;
|
|
613
|
+
};
|
|
614
|
+
}, [get]);
|
|
615
|
+
return { data, isLoading, error };
|
|
616
|
+
};
|
|
591
617
|
const useAuditLog = (id) => {
|
|
592
618
|
const { get } = admin.useFetchClient();
|
|
593
619
|
const [data, setData] = React__namespace.useState(null);
|
|
@@ -622,15 +648,6 @@ const useAuditLog = (id) => {
|
|
|
622
648
|
}, [get, id]);
|
|
623
649
|
return { data, isLoading, error };
|
|
624
650
|
};
|
|
625
|
-
const useDeleteAuditLog = () => {
|
|
626
|
-
const { del } = admin.useFetchClient();
|
|
627
|
-
return React__namespace.useCallback(
|
|
628
|
-
async (id) => {
|
|
629
|
-
await del(`/${index.PLUGIN_ID}/logs/${id}`);
|
|
630
|
-
},
|
|
631
|
-
[del]
|
|
632
|
-
);
|
|
633
|
-
};
|
|
634
651
|
const MetaField = ({ label, value, mono = false }) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Item, { col: 4, s: 6, xs: 12, direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
635
652
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: label }),
|
|
636
653
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -806,6 +823,25 @@ const AuditLogDetails = () => {
|
|
|
806
823
|
value: log.userAgent,
|
|
807
824
|
mono: true
|
|
808
825
|
}
|
|
826
|
+
),
|
|
827
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
828
|
+
MetaField,
|
|
829
|
+
{
|
|
830
|
+
label: formatMessage({ id: index.getTranslation("detail.hash"), defaultMessage: "Hash" }),
|
|
831
|
+
value: log.hash,
|
|
832
|
+
mono: true
|
|
833
|
+
}
|
|
834
|
+
),
|
|
835
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
836
|
+
MetaField,
|
|
837
|
+
{
|
|
838
|
+
label: formatMessage({
|
|
839
|
+
id: index.getTranslation("detail.prevHash"),
|
|
840
|
+
defaultMessage: "Previous hash"
|
|
841
|
+
}),
|
|
842
|
+
value: log.prevHash,
|
|
843
|
+
mono: true
|
|
844
|
+
}
|
|
809
845
|
)
|
|
810
846
|
] }) })
|
|
811
847
|
] }),
|
|
@@ -1029,7 +1065,6 @@ const AuditLogTable = ({
|
|
|
1029
1065
|
sort,
|
|
1030
1066
|
onSortChange,
|
|
1031
1067
|
onView,
|
|
1032
|
-
onDelete,
|
|
1033
1068
|
isLoading
|
|
1034
1069
|
}) => {
|
|
1035
1070
|
const { formatMessage, locale } = reactIntl.useIntl();
|
|
@@ -1084,36 +1119,69 @@ const AuditLogTable = ({
|
|
|
1084
1119
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral800", children: log.locale ?? "-" }) }),
|
|
1085
1120
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: log.source }) }),
|
|
1086
1121
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: log.outcome === "failure" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "danger100", textColor: "danger600", children: log.outcome }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: log.outcome ?? "success" }) }),
|
|
1087
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
{
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
),
|
|
1100
|
-
onDelete ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1101
|
-
designSystem.IconButton,
|
|
1102
|
-
{
|
|
1103
|
-
label: formatMessage({
|
|
1104
|
-
id: index.getTranslation("list.delete"),
|
|
1105
|
-
defaultMessage: "Delete this record"
|
|
1106
|
-
}),
|
|
1107
|
-
variant: "ghost",
|
|
1108
|
-
onClick: () => onDelete(log),
|
|
1109
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
|
|
1110
|
-
}
|
|
1111
|
-
) : null
|
|
1112
|
-
] }) })
|
|
1122
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 1, justifyContent: "flex-end", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1123
|
+
designSystem.IconButton,
|
|
1124
|
+
{
|
|
1125
|
+
label: formatMessage({
|
|
1126
|
+
id: index.getTranslation("list.view"),
|
|
1127
|
+
defaultMessage: "View details"
|
|
1128
|
+
}),
|
|
1129
|
+
variant: "ghost",
|
|
1130
|
+
onClick: () => onView(log),
|
|
1131
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Eye, {})
|
|
1132
|
+
}
|
|
1133
|
+
) }) })
|
|
1113
1134
|
] }, log.id);
|
|
1114
1135
|
}) })
|
|
1115
1136
|
] });
|
|
1116
1137
|
};
|
|
1138
|
+
const IntegrityStatus = () => {
|
|
1139
|
+
const { formatMessage, locale } = reactIntl.useIntl();
|
|
1140
|
+
const { data, isLoading, error } = useAuditIntegrity();
|
|
1141
|
+
if (isLoading) {
|
|
1142
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
|
|
1143
|
+
id: index.getTranslation("integrity.checking"),
|
|
1144
|
+
defaultMessage: "Verifying the audit chain…"
|
|
1145
|
+
}) });
|
|
1146
|
+
}
|
|
1147
|
+
if (error || !data) {
|
|
1148
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "warning600", children: formatMessage(
|
|
1149
|
+
{
|
|
1150
|
+
id: index.getTranslation("integrity.unavailable"),
|
|
1151
|
+
defaultMessage: "Integrity check unavailable: {error}"
|
|
1152
|
+
},
|
|
1153
|
+
{ error: error ?? "no report" }
|
|
1154
|
+
) });
|
|
1155
|
+
}
|
|
1156
|
+
if (!data.ok) {
|
|
1157
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "danger100", hasRadius: true, padding: 3, borderColor: "danger200", borderWidth: "1px", borderStyle: "solid", children: [
|
|
1158
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
|
|
1159
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "danger600", textColor: "neutral0", children: formatMessage({ id: index.getTranslation("integrity.broken"), defaultMessage: "CHAIN BROKEN" }) }),
|
|
1160
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger700", fontWeight: "bold", children: formatMessage(
|
|
1161
|
+
{
|
|
1162
|
+
id: index.getTranslation("integrity.brokenAt"),
|
|
1163
|
+
defaultMessage: "Record #{id}: {reason}"
|
|
1164
|
+
},
|
|
1165
|
+
{ id: data.brokenAt?.id ?? "?", reason: data.brokenAt?.reason ?? "" }
|
|
1166
|
+
) })
|
|
1167
|
+
] }),
|
|
1168
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger700", children: formatMessage({
|
|
1169
|
+
id: index.getTranslation("integrity.brokenHint"),
|
|
1170
|
+
defaultMessage: "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
|
|
1171
|
+
}) }) })
|
|
1172
|
+
] });
|
|
1173
|
+
}
|
|
1174
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
|
|
1175
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: formatMessage({ id: index.getTranslation("integrity.ok"), defaultMessage: "Chain verified" }) }),
|
|
1176
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(
|
|
1177
|
+
{
|
|
1178
|
+
id: index.getTranslation("integrity.summary"),
|
|
1179
|
+
defaultMessage: "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}"
|
|
1180
|
+
},
|
|
1181
|
+
{ hashed: data.hashed, legacy: data.legacy, when: formatDate(data.verifiedAt, locale) }
|
|
1182
|
+
) })
|
|
1183
|
+
] });
|
|
1184
|
+
};
|
|
1117
1185
|
const toSearchParams = (query) => {
|
|
1118
1186
|
const params = new URLSearchParams();
|
|
1119
1187
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -1125,17 +1193,10 @@ const toSearchParams = (query) => {
|
|
|
1125
1193
|
const AuditLogs = () => {
|
|
1126
1194
|
const { formatMessage } = reactIntl.useIntl();
|
|
1127
1195
|
const navigate = reactRouterDom.useNavigate();
|
|
1128
|
-
const { toggleNotification } = admin.useNotification();
|
|
1129
1196
|
const [{ query }, setQuery] = admin.useQueryParams({ page: 1, pageSize: 20 });
|
|
1130
|
-
const {
|
|
1131
|
-
allowedActions: { canDelete },
|
|
1132
|
-
isLoading: isLoadingPermissions
|
|
1133
|
-
} = admin.useRBAC(index.PERMISSIONS);
|
|
1134
1197
|
const search = React__namespace.useMemo(() => toSearchParams(query), [query]);
|
|
1135
|
-
const { data, pagination, isLoading, error
|
|
1198
|
+
const { data, pagination, isLoading, error } = useAuditLogs(search);
|
|
1136
1199
|
const { options } = useAuditFilterOptions();
|
|
1137
|
-
const deleteLog = useDeleteAuditLog();
|
|
1138
|
-
const [pendingDelete, setPendingDelete] = React__namespace.useState(null);
|
|
1139
1200
|
const updateQuery = React__namespace.useCallback(
|
|
1140
1201
|
(patch) => {
|
|
1141
1202
|
setQuery({ ...patch, page: 1 });
|
|
@@ -1159,33 +1220,6 @@ const AuditLogs = () => {
|
|
|
1159
1220
|
"remove"
|
|
1160
1221
|
);
|
|
1161
1222
|
}, [setQuery]);
|
|
1162
|
-
const confirmDelete = React__namespace.useCallback(async () => {
|
|
1163
|
-
if (!pendingDelete) return;
|
|
1164
|
-
try {
|
|
1165
|
-
await deleteLog(pendingDelete.id);
|
|
1166
|
-
toggleNotification({
|
|
1167
|
-
type: "success",
|
|
1168
|
-
message: formatMessage({
|
|
1169
|
-
id: index.getTranslation("delete.success"),
|
|
1170
|
-
defaultMessage: "Audit record deleted."
|
|
1171
|
-
})
|
|
1172
|
-
});
|
|
1173
|
-
refresh();
|
|
1174
|
-
} catch {
|
|
1175
|
-
toggleNotification({
|
|
1176
|
-
type: "danger",
|
|
1177
|
-
message: formatMessage({
|
|
1178
|
-
id: index.getTranslation("delete.error"),
|
|
1179
|
-
defaultMessage: "Could not delete this audit record."
|
|
1180
|
-
})
|
|
1181
|
-
});
|
|
1182
|
-
} finally {
|
|
1183
|
-
setPendingDelete(null);
|
|
1184
|
-
}
|
|
1185
|
-
}, [deleteLog, formatMessage, pendingDelete, refresh, toggleNotification]);
|
|
1186
|
-
if (isLoadingPermissions) {
|
|
1187
|
-
return /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Loading, {});
|
|
1188
|
-
}
|
|
1189
1223
|
return /* @__PURE__ */ jsxRuntime.jsxs(admin.Page.Main, { children: [
|
|
1190
1224
|
/* @__PURE__ */ jsxRuntime.jsx(admin.Page.Title, { children: formatMessage({ id: index.getTranslation("plugin.name"), defaultMessage: "Audit Logs" }) }),
|
|
1191
1225
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1216,6 +1250,7 @@ const AuditLogs = () => {
|
|
|
1216
1250
|
}
|
|
1217
1251
|
),
|
|
1218
1252
|
/* @__PURE__ */ jsxRuntime.jsxs(admin.Layouts.Content, { children: [
|
|
1253
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsx(IntegrityStatus, {}) }),
|
|
1219
1254
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1220
1255
|
AuditLogFilters,
|
|
1221
1256
|
{
|
|
@@ -1233,8 +1268,7 @@ const AuditLogs = () => {
|
|
|
1233
1268
|
isLoading,
|
|
1234
1269
|
sort: String(query.sort ?? "createdAt:desc"),
|
|
1235
1270
|
onSortChange: (sort) => setQuery({ sort, page: 1 }),
|
|
1236
|
-
onView: (log) => navigate(`${log.id}`)
|
|
1237
|
-
onDelete: canDelete ? setPendingDelete : null
|
|
1271
|
+
onView: (log) => navigate(`${log.id}`)
|
|
1238
1272
|
}
|
|
1239
1273
|
),
|
|
1240
1274
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1249,31 +1283,7 @@ const AuditLogs = () => {
|
|
|
1249
1283
|
] })
|
|
1250
1284
|
}
|
|
1251
1285
|
) })
|
|
1252
|
-
] })
|
|
1253
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1254
|
-
designSystem.Dialog.Root,
|
|
1255
|
-
{
|
|
1256
|
-
open: pendingDelete !== null,
|
|
1257
|
-
onOpenChange: (open) => {
|
|
1258
|
-
if (!open) setPendingDelete(null);
|
|
1259
|
-
},
|
|
1260
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1261
|
-
admin.ConfirmDialog,
|
|
1262
|
-
{
|
|
1263
|
-
title: formatMessage({
|
|
1264
|
-
id: index.getTranslation("delete.title"),
|
|
1265
|
-
defaultMessage: "Delete audit record"
|
|
1266
|
-
}),
|
|
1267
|
-
onConfirm: confirmDelete,
|
|
1268
|
-
onCancel: () => setPendingDelete(null),
|
|
1269
|
-
children: formatMessage({
|
|
1270
|
-
id: index.getTranslation("delete.confirm"),
|
|
1271
|
-
defaultMessage: "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?"
|
|
1272
|
-
})
|
|
1273
|
-
}
|
|
1274
|
-
)
|
|
1275
|
-
}
|
|
1276
|
-
)
|
|
1286
|
+
] })
|
|
1277
1287
|
] });
|
|
1278
1288
|
};
|
|
1279
1289
|
const App = () => /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Protect, { permissions: index.PERMISSIONS.read, children: /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Routes, { children: [
|