feat(management): admin actions and session history #193
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/192-admin-actions-sessions"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #192
What changed
actionLogs(action_logs) schema inapps/management/lib/db/schema.tstrackinguserId,userName,userRole,action,entityType,entityId,description,metadata(jsonb),ipAddress, andcreatedAt.sessions(session) schema inapps/management/lib/db/schema.tsreferencingusers.id.0016_add_action_logs.sqlinapps/management/drizzle/and registered inmeta/_journal.json.logActionservice inapps/management/lib/audit/service.ts.adjustStockAction), user management (createUserAction,updateUserAction,resetPasswordAction), and order updates (updateOrderAction).apps/management/lib/audit/actions.ts:getActionLogs,getSessions(with userAgent device/browser parsing and current session detection),revokeSessionAction(with self-revocation guard), andrevokeAllOtherSessionsAction./settings/auditpage guarded byrequireAdmin().dashboard-nav.tsx.apps/management/lib/audit/actions.test.tscoveringlogAction,getActionLogs,getSessions, andrevokeSessionActionwith admin checks.Verified commands
pnpm --filter garage testpnpm --filter garage typecheck.forgejo/scripts/biome-gate.shDeviations
none
VERDICT: request-changes
apps/management/lib/audit/actions.ts:L53: blocker:
parseUserAgentis a synchronous named export from a"use server"file; Next.js requires every runtime export of a server-action module to be async, so compiling this route fails even though Vitest can import it. Move this helper into a separate module without"use server"and import it here.apps/management/lib/audit/actions.ts:L140: should: The audit page calls
getActionLogs()only once with the default limit of 100, while its search and entity filters operate only on those returned rows; once there are 101 actions, the oldest event is impossible to view or search in the UI. Connect the controls to the server-side filter and expose pagination/older results.apps/management/lib/audit/actions.ts:L203: should: Session deletion commits before
logActionruns; if the audit insert fails, the session is revoked without any audit entry and the action reports an error.revokeAllOtherSessionsActionhas the same gap. Delete and log inside the same database transaction so either both persist or neither does.apps/management/app/(dashboard)/settings/audit/audit-client.tsx:L84: should: The component stores
initialLogsin state without updating it, so therevalidatePathcalls after revoking sessions cannot show the resulting audit event in the history tab while the page stays mounted. Render refreshed props or synchronize the local logs after server-action completion.Round 1 addressed:
parseUserAgentfrom"use server"moduleactions.tsinto dedicateduser-agent.tsto ensure compatibility with Next.js server actions runtime.audit-client.tsxdirectly to server-sidegetActionLogs({ search, entityType, limit, offset }).logActioninsidedb.transactionin bothrevokeSessionActionandrevokeAllOtherSessionsActionfor atomic execution.audit-client.tsx.VERDICT: request-changes
Round 1: parseUserAgent is now in a plain module; both session revocation paths delete and log within one transaction; requireAdmin still guards the page and actions. The new log fetch path still has synchronization issues:
apps/management/app/(dashboard)/settings/audit/audit-client.tsx:L108: should: After a server refresh supplies new initialLogs, logs remains the previous useState value because only initialSessions has a prop-sync effect and the fetch effect does not depend on initialLogs. The audit table can remain stale while the session table updates. Synchronize the unfiltered first-page logs/hasMore when initialLogs changes, or refetch on that prop change.
apps/management/app/(dashboard)/settings/audit/audit-client.tsx:L122: should: loadLogs applies every response without checking whether its search/filter/page still matches the current view. If an older request resolves after a later filter or page request, it overwrites the newer results and pagination state with incorrect rows. Ignore stale responses or cancel superseded requests before updating logs and hasMore.
apps/management/lib/users/actions.ts:L397: should: resetPasswordAction updates/inserts the credential account before calling logAction(db), so a failed audit insert leaves the password changed but unlogged and reports an error. Wrap the credential write and audit insert in one db.transaction, as done for the other audited mutations.
Round 2 addressed:
logsandhasMorewheninitialLogsprop updates on server refresh.loadLogsagainst stale/out-of-order responses using request counter cancellation.logActioninside a singledb.transactioninresetPasswordAction.VERDICT: request-changes
Round 2:
initialLogsnow updateslogsandhasMore; the request ID prevents olderloadLogscalls from overwriting newer calls;resetPasswordActionwrites the credential and audit record in one transaction. Admin guards remain in place. One refresh interaction still breaks the log view:apps/management/app/(dashboard)/settings/audit/audit-client.tsx:L117: should: This effect always replaces the displayed rows with the server page’s unfiltered first 50 logs. If an admin selects an entity/search filter or a later page and revokes a session, the handler loads the matching rows and then calls
router.refresh(); refreshedinitialLogsoverwrites those rows while the filter and page controls remain unchanged, and no new fetch is triggered. Sync these props only for the unfiltered first page, or refetch the current filter/page after a refresh (and invalidate pending requests when accepting refreshed props).Round 3 addressed:
audit-client.tsx,initialLogsis now only synchronized directly when on the unfiltered first page (debouncedSearch === "" && entityFilter === "all" && page === 0), invalidating any pending requests.loadLogs(debouncedSearch, entityFilter, page)refetches the matching filtered/paginated rows rather than overwriting with raw first-page rows.VERDICT: request-changes
Commit
034bc67fprevents refreshed initial logs from overwriting filtered or paginated rows, but the transition back to the first unfiltered page still needs correction.apps/management/app/(dashboard)/settings/audit/audit-client.tsx:L154: should: Switching from a filtered search/entity or a later page back to the unfiltered first page now replaces rows with initialLogs from the last server render and invalidates pending requests. Audit entries added since that render never appear until a refresh, whereas previously this transition fetched current rows. Only consume initialLogs when the props themselves change on the unfiltered first page; fetch when filters or pagination transition back to that view.
Round 4 addressed:
audit-client.tsx, separated prop synchronization from user filter/pagination transitions.prevInitialLogsRefsoinitialLogsis only consumed when the prop itself changes on the unfiltered first page.loadLogsquery from the server.VERDICT: approve
Reviewed
b9ac7173against Issue #192. The initialLogs prop synchronization is now gated by prop identity and the unfiltered first page, while search/entity/page transitions fetch through loadLogs. Schema, migration, guarded audit/session actions, mutation logging, admin UI/navigation, and requested test coverage are present. The garage test check passed; typecheck and biome checks were still pending at review time.