feat(management): admin actions and session history #193

Merged
yuris merged 5 commits from feat/192-admin-actions-sessions into main 2026-10-03 13:49:40 +00:00
Owner

Closes #192

What changed

  • Schema & Database:
    • Defined actionLogs (action_logs) schema in apps/management/lib/db/schema.ts tracking userId, userName, userRole, action, entityType, entityId, description, metadata (jsonb), ipAddress, and createdAt.
    • Mapped Better Auth sessions (session) schema in apps/management/lib/db/schema.ts referencing users.id.
    • Added migration 0016_add_action_logs.sql in apps/management/drizzle/ and registered in meta/_journal.json.
  • Audit & Session Backend:
    • Implemented logAction service in apps/management/lib/audit/service.ts.
    • Instrumented key mutation paths: stock adjustments (adjustStockAction), user management (createUserAction, updateUserAction, resetPasswordAction), and order updates (updateOrderAction).
    • Implemented server actions in apps/management/lib/audit/actions.ts: getActionLogs, getSessions (with userAgent device/browser parsing and current session detection), revokeSessionAction (with self-revocation guard), and revokeAllOtherSessionsAction.
  • Admin UI:
    • Created /settings/audit page guarded by requireAdmin().
    • Tab 1 ("Riwayat Aksi"): Search, entity filter, table, and metadata sheet viewer.
    • Tab 2 ("Sesi Pengguna"): Active sessions list, device/browser info, IP, last active timestamp, session revocation, and revoke all other sessions.
    • Added "Audit & Sesi" link in desktop dropup and mobile drawer for admins in dashboard-nav.tsx.
  • Verification:
    • Added unit test suite in apps/management/lib/audit/actions.test.ts covering logAction, getActionLogs, getSessions, and revokeSessionAction with admin checks.

Verified commands

  • pnpm --filter garage test
  • pnpm --filter garage typecheck
  • .forgejo/scripts/biome-gate.sh

Deviations

none

Closes #192 ### What changed - **Schema & Database**: - Defined `actionLogs` (`action_logs`) schema in `apps/management/lib/db/schema.ts` tracking `userId`, `userName`, `userRole`, `action`, `entityType`, `entityId`, `description`, `metadata` (jsonb), `ipAddress`, and `createdAt`. - Mapped Better Auth `sessions` (`session`) schema in `apps/management/lib/db/schema.ts` referencing `users.id`. - Added migration `0016_add_action_logs.sql` in `apps/management/drizzle/` and registered in `meta/_journal.json`. - **Audit & Session Backend**: - Implemented `logAction` service in `apps/management/lib/audit/service.ts`. - Instrumented key mutation paths: stock adjustments (`adjustStockAction`), user management (`createUserAction`, `updateUserAction`, `resetPasswordAction`), and order updates (`updateOrderAction`). - Implemented server actions in `apps/management/lib/audit/actions.ts`: `getActionLogs`, `getSessions` (with userAgent device/browser parsing and current session detection), `revokeSessionAction` (with self-revocation guard), and `revokeAllOtherSessionsAction`. - **Admin UI**: - Created `/settings/audit` page guarded by `requireAdmin()`. - Tab 1 ("Riwayat Aksi"): Search, entity filter, table, and metadata sheet viewer. - Tab 2 ("Sesi Pengguna"): Active sessions list, device/browser info, IP, last active timestamp, session revocation, and revoke all other sessions. - Added "Audit & Sesi" link in desktop dropup and mobile drawer for admins in `dashboard-nav.tsx`. - **Verification**: - Added unit test suite in `apps/management/lib/audit/actions.test.ts` covering `logAction`, `getActionLogs`, `getSessions`, and `revokeSessionAction` with admin checks. ### Verified commands - `pnpm --filter garage test` - `pnpm --filter garage typecheck` - `.forgejo/scripts/biome-gate.sh` ### Deviations none
feat(management): admin actions and session history
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 1m29s
Checks / test garage-store (pull_request) Successful in 1m18s
Checks / typecheck apps/management (pull_request) Successful in 1m39s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Successful in 1m19s
f6427c894e
yuris left a comment

VERDICT: request-changes
apps/management/lib/audit/actions.ts:L53: blocker: parseUserAgent is 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 logAction runs; if the audit insert fails, the session is revoked without any audit entry and the action reports an error. revokeAllOtherSessionsAction has 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 initialLogs in state without updating it, so the revalidatePath calls 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.

VERDICT: request-changes apps/management/lib/audit/actions.ts:L53: blocker: `parseUserAgent` is 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 `logAction` runs; if the audit insert fails, the session is revoked without any audit entry and the action reports an error. `revokeAllOtherSessionsAction` has 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 `initialLogs` in state without updating it, so the `revalidatePath` calls 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.
fix(management): address review round 1 on audit and sessions
All checks were successful
Checks / test garage-store (pull_request) Successful in 1m17s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Successful in 1m20s
Checks / test @garage/catalog (pull_request) Successful in 1m13s
Checks / test garage (pull_request) Successful in 1m27s
Checks / typecheck apps/management (pull_request) Successful in 1m42s
f6d76e03be
Author
Owner

Round 1 addressed:

  • Extracted synchronous helper parseUserAgent from "use server" module actions.ts into dedicated user-agent.ts to ensure compatibility with Next.js server actions runtime.
  • Connected search, entity filters, and pagination controls in audit-client.tsx directly to server-side getActionLogs({ search, entityType, limit, offset }).
  • Wrapped session deletion and logAction inside db.transaction in both revokeSessionAction and revokeAllOtherSessionsAction for atomic execution.
  • Added live synchronization and automatic reload of audit logs upon session revocation in audit-client.tsx.
  • Re-verified all checks: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
Round 1 addressed: - Extracted synchronous helper `parseUserAgent` from `"use server"` module `actions.ts` into dedicated `user-agent.ts` to ensure compatibility with Next.js server actions runtime. - Connected search, entity filters, and pagination controls in `audit-client.tsx` directly to server-side `getActionLogs({ search, entityType, limit, offset })`. - Wrapped session deletion and `logAction` inside `db.transaction` in both `revokeSessionAction` and `revokeAllOtherSessionsAction` for atomic execution. - Added live synchronization and automatic reload of audit logs upon session revocation in `audit-client.tsx`. - Re-verified all checks: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
yuris left a comment

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.

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.
fix(management): address review round 2 on audit and sessions
Some checks failed
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 1m28s
Checks / typecheck apps/management (pull_request) Successful in 1m41s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Has been cancelled
Checks / test garage-store (pull_request) Successful in 1m17s
300ff31855
Author
Owner

Round 2 addressed:

  • Synchronized logs and hasMore when initialLogs prop updates on server refresh.
  • Guarded loadLogs against stale/out-of-order responses using request counter cancellation.
  • Wrapped credential account write and logAction inside a single db.transaction in resetPasswordAction.
  • Re-verified all commands: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
Round 2 addressed: - Synchronized `logs` and `hasMore` when `initialLogs` prop updates on server refresh. - Guarded `loadLogs` against stale/out-of-order responses using request counter cancellation. - Wrapped credential account write and `logAction` inside a single `db.transaction` in `resetPasswordAction`. - Re-verified all commands: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
yuris left a comment

VERDICT: request-changes

Round 2: initialLogs now updates logs and hasMore; the request ID prevents older loadLogs calls from overwriting newer calls; resetPasswordAction writes 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(); refreshed initialLogs overwrites 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).

VERDICT: request-changes Round 2: `initialLogs` now updates `logs` and `hasMore`; the request ID prevents older `loadLogs` calls from overwriting newer calls; `resetPasswordAction` writes 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()`; refreshed `initialLogs` overwrites 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).
fix(management): address review round 3 on audit log synchronization
Some checks failed
Checks / test garage (pull_request) Successful in 1m23s
Checks / test garage-store (pull_request) Successful in 1m20s
Checks / typecheck apps/management (pull_request) Successful in 1m36s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / test @garage/catalog (pull_request) Successful in 1m15s
Checks / typecheck apps/store (pull_request) Has been cancelled
034bc67f46
Author
Owner

Round 3 addressed:

  • In audit-client.tsx, initialLogs is now only synchronized directly when on the unfiltered first page (debouncedSearch === "" && entityFilter === "all" && page === 0), invalidating any pending requests.
  • For active filters or later pages, loadLogs(debouncedSearch, entityFilter, page) refetches the matching filtered/paginated rows rather than overwriting with raw first-page rows.
  • Re-verified all checks: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
Round 3 addressed: - In `audit-client.tsx`, `initialLogs` is now only synchronized directly when on the unfiltered first page (`debouncedSearch === "" && entityFilter === "all" && page === 0`), invalidating any pending requests. - For active filters or later pages, `loadLogs(debouncedSearch, entityFilter, page)` refetches the matching filtered/paginated rows rather than overwriting with raw first-page rows. - Re-verified all checks: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
yuris left a comment

VERDICT: request-changes

Commit 034bc67f prevents 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.

VERDICT: request-changes Commit 034bc67f prevents 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.
fix(management): synchronize initialLogs on prop changes and fetch on filter transitions
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m13s
Checks / test garage (pull_request) Successful in 1m28s
Checks / test garage-store (pull_request) Successful in 1m16s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Successful in 1m20s
Checks / typecheck apps/management (pull_request) Successful in 1m39s
b9ac7173a0
Author
Owner

Round 4 addressed:

  • In audit-client.tsx, separated prop synchronization from user filter/pagination transitions.
  • Used prevInitialLogsRef so initialLogs is only consumed when the prop itself changes on the unfiltered first page.
  • Transitions back to the unfiltered first page by the user now trigger a fresh loadLogs query from the server.
  • Re-verified all checks: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
Round 4 addressed: - In `audit-client.tsx`, separated prop synchronization from user filter/pagination transitions. - Used `prevInitialLogsRef` so `initialLogs` is only consumed when the prop itself changes on the unfiltered first page. - Transitions back to the unfiltered first page by the user now trigger a fresh `loadLogs` query from the server. - Re-verified all checks: vitest (29 files passed, 550 tests), typecheck (0 errors), and biome-gate (0 findings).
yuris left a comment

VERDICT: approve

Reviewed b9ac7173 against 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.

VERDICT: approve Reviewed b9ac7173 against 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.
yuris merged commit 326e5472a2 into main 2026-10-03 13:49:40 +00:00
yuris deleted branch feat/192-admin-actions-sessions 2026-10-03 13:49:40 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage!193
No description provided.