diff --git a/.changeset/olive-bats-jam.md b/.changeset/olive-bats-jam.md new file mode 100644 index 0000000..9bc3970 --- /dev/null +++ b/.changeset/olive-bats-jam.md @@ -0,0 +1,15 @@ +--- +"seamless-auth-admin-dashboard": minor +--- + +Show who performed an administrative action in the events table. + +The API now records the acting administrator separately from the subject of an +action. An administrative event names two people, and showing only the subject +reads as though they did it to themselves, so the User column now shows the +target with the administrator beneath it, each linking to their own detail page. + +Administrative events are also labelled as such rather than as ordinary +user-linked events. + +Also upgrades `@seamless-auth/types` to 0.10.0. diff --git a/package-lock.json b/package-lock.json index 053a9b3..3253c7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.4.0", "dependencies": { "@seamless-auth/react": "^0.7.0", - "@seamless-auth/types": "^0.9.0", + "@seamless-auth/types": "^0.10.0", "@tailwindcss/vite": "^4.3.3", "@tanstack/react-query": "^5.101.4", "clsx": "^2.1.1", @@ -1703,9 +1703,9 @@ } }, "node_modules/@seamless-auth/types": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.9.0.tgz", - "integrity": "sha512-oTVDH975RQ+W7azEdNYFD7Qe763iMtXliV8s4EXwhFjjXq+SvJdofiF5JtygHpqubSVYIbXKyNaT6wpyTaax3A==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@seamless-auth/types/-/types-0.10.0.tgz", + "integrity": "sha512-H2YKoBhj5/l8HM+EgQkh8X5MHFxuuE1uhQ8881E55JBMJf110EynGS6GhMEuDuvruwsJEv/GaW/ScjsuVthP4Q==", "license": "AGPL-3.0-only", "dependencies": { "zod": "^4.3.6" diff --git a/package.json b/package.json index 7e64afd..f5609f2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@seamless-auth/react": "^0.7.0", - "@seamless-auth/types": "^0.9.0", + "@seamless-auth/types": "^0.10.0", "@tailwindcss/vite": "^4.3.3", "@tanstack/react-query": "^5.101.4", "clsx": "^2.1.1", diff --git a/src/pages/Events.test.tsx b/src/pages/Events.test.tsx index e47ca68..9b4d24e 100644 --- a/src/pages/Events.test.tsx +++ b/src/pages/Events.test.tsx @@ -158,3 +158,57 @@ describe("Events time range", () => { expect(screen.queryByText("Suspicious Signals")).not.toBeInTheDocument(); }); }); + +describe("Events actor attribution", () => { + function withEvents(events: unknown[]) { + mocks.useEvents.mockReset(); + mocks.useEvents.mockReturnValue({ + data: { events, total: events.length }, + isLoading: false, + isError: false, + error: null, + refetch: vi.fn(), + }); + } + + const base = { + id: "evt-1", + type: "admin_device_replacement_recovery", + ip_address: "127.0.0.1", + user_agent: "agent", + metadata: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + it("names the administrator alongside the target", () => { + withEvents([ + { + ...base, + user_id: "11111111-aaaa-bbbb-cccc-222222222222", + actor_user_id: "99999999-dddd-eeee-ffff-333333333333", + }, + ]); + + renderPage(); + + expect(screen.getByText("11111111...")).toBeInTheDocument(); + expect(screen.getByText("99999999...")).toBeInTheDocument(); + expect(screen.getByText("Administrative action")).toBeInTheDocument(); + }); + + it("leaves an ordinary user event unattributed", () => { + withEvents([ + { + ...base, + type: "login_success", + user_id: "11111111-aaaa-bbbb-cccc-222222222222", + }, + ]); + + renderPage(); + + expect(screen.getByText("User-linked event")).toBeInTheDocument(); + expect(screen.queryByText("by")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/Events.tsx b/src/pages/Events.tsx index 41d0cfa..4689a86 100644 --- a/src/pages/Events.tsx +++ b/src/pages/Events.tsx @@ -345,7 +345,11 @@ export default function Events() { {value as string} - {row.user_id ? "User-linked event" : "System-level event"} + {row.actor_user_id + ? "Administrative action" + : row.user_id + ? "User-linked event" + : "System-level event"} ), @@ -354,20 +358,38 @@ export default function Events() { key: "user_id", label: "User", width: "small", - render: (value) => - value ? ( - - ) : ( - System - ), + // An administrative event names two people. Showing only the + // subject reads as though they did it to themselves. + render: (value, row) => ( +
+ {value ? ( + + ) : ( + System + )} + + {row.actor_user_id && ( + + )} +
+ ), }, { key: "ip_address",