Lewati ke konten utama

Users & Access — CRUD Standard + Completion Checklist

Audience: backend + frontend engineers on the Kesles Dashboard. Scope: the 8 sub-panels in the Users & Access dashboard menu (Users, Roles, Permissions, Role Matrix, Role Access Map, User Roles, Platform Access, Devices, Sessions, Audit Logs). Status: living document — update after each PR.

This document has two roles:

  1. Completion checklist for the Users & Access menu so every sub-panel has full CRUD/action (not just "coming soon").
  2. Platform standard that is the MANDATORY reference for other Kesles dashboards being built (mobile-partner-dashboard, pos-admin, etc.). Every new panel that has master data must follow this pattern, no exceptions.

1. CRUD Pattern Standard (MANDATORY for every Kesles dashboard)

Every master-data panel in any dashboard MUST implement this contract before being released to production. There is no "Phase 2 later" for primary actions.

1.1 Action set — primary (mandatory)

ActionHTTPEndpoint patternUI entry-pointConfirmationAudit
ListGET/{domain}/{resource}?limit&offset&search&statusmain panel page, paginated
DetailGET/{domain}/{resource}/{id}row click (or view button)log read if data is sensitive (e.g. KTP photo URL)
AddPOST/{domain}/{resource}"+ Add Data" button in the panel headerform dialog with backend-side validation1 audit row: create
EditPATCH/{domain}/{resource}/{id}edit icon on the rowform dialog prefilled + diff-aware save1 audit row: update with before/after diff
DeleteDELETE/{domain}/{resource}/{id}trash icon on the rowconfirmation dialog "type the name to delete" if hard-delete1 audit row: delete

1.2 Action set — secondary (conditional)

Depending on the domain, you may need:

  • Toggle status (activate/deactivate) — PATCH .../{id} with payload {is_enabled: bool} or {status: 'active'|'inactive'}. Not DELETE — delete = permanent removal, toggle = soft-disable.
  • Revoke / terminate (session, token, device) — POST .../{id}/revoke. Different from delete because the row stays for the audit trail.
  • Approve / rejectPOST .../{id}/approve + /{id}/reject, with a reason field.
  • Bulk actionPOST .../bulk-{action} with an array of ids, atomic (all or nothing).
  • Assign / revoke relationshipPOST .../{parent_id}/{child_resource} + the same DELETE. Example POST /users/{id}/roles/{role_code}.

1.3 Standard dialog shell

Every Users & Access dialog — and every future Kesles dashboard — must use a consistent shell:

NeedShellLocation
CRUD form (Add/Edit) + destructive confirmStandardDialogdashboard_shell_shared_widgets.dart
Read-only detail (more content)DashboardDialogShellditto
Long form (purchase order, vendor, etc.)DashboardFormDialogShellditto

Rules:

  • Avoid raw Material AlertDialog — visuals don't match (border radius, action alignment, icon-in-circle).
  • StandardDialog has icon-in-circle + close button top-right + a consistent action row.
  • Destructive (Delete/Revoke/Terminate): actionColor: const Color(0xFFE45447) (Kesles red).
  • Primary (Save/Submit): default Kesles blue actionColor — no override needed.
  • Info-only redirect dialog: both buttons Close + Got it are Navigator.pop — no UX trap.
  • actionBusy + actionEnabled are used for the loading state; actionBusy=true disables both buttons + shows a spinner on the action button.

Reference examples already consistent (Ship-13 audit):

  • Sessions Terminate confirm, Devices Revoke confirm, Platform Access delete/assign/edit, Role Matrix redirect — all use StandardDialog.

1.4 Standard per-panel UI

Required layout:

┌─────────────────────────────────────────────────────────────────┐
│ <Title> [Refresh] [+ Add Data]│
│ <Subtitle> │
├─────────────────────────────────────────────────────────────────┤
│ [Filter chip 1] [Filter 2] … [Search box] [Dropdown ▾] │
├─────────────────────────────────────────────────────────────────┤
│ ☐ Col1 Col2 Col3 Status Action │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ row1 … [edit] [delete] [•••] │ │
│ │ row2 … [edit] [delete] [•••] │ │
│ └─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Showing 1–10 of 83 · < 1 2 3 … > │
└─────────────────────────────────────────────────────────────────┘
  • Add button on the right of the header; if the role has no write-permission → the button is hidden (NOT disabled with a tooltip).
  • Row action icons: at most 3 visible (Edit / Delete / More); the rest go in the Icons.more_vert overflow menu.
  • Every destructive action (Delete, Revoke, Force-logout, Reset password) MUST go through an AlertDialog confirmation — must not execute directly from the row.
  • Loading state: spinner on the action button while in-flight; the row is dimmed to opacity 0.5 while being edited/deleted.
  • Error: red SnackBar with the backend message (exception.message after replaceFirst('Exception: ', '')). Don't expose the stacktrace.
  • Success: green SnackBar + auto-reload of the list.

1.5 Backend contract

  1. Consistent response shape for every endpoint:
    { "items": [...], "meta": {"limit": 10, "offset": 0, "total": 83, "can_write": true} }
    or single: { "item": {...} }.
  2. meta.can_write is driven by the caller's role/permission — the FE uses this to hide/show buttons (don't re-check role in the FE).
  3. Consistent error shape:
    { "error": "validation_failed", "message": "swift_code must be 8 or 11 characters", "field": "swift_code" }
  4. HTTP status: 200 read, 201 create, 200 update/delete, 400 bad request (malformed JSON), 401 unauth, 403 forbidden (insufficient role), 404 not found, 409 conflict (duplicate, FK violation translated), 422 validation error, 500 server error.
  5. Audit write for every mutating endpoint — insert into auth.auth_audit_logs (db_kesles_merchant_auth) with actor_user_id, resource_type, resource_id, action, before, after, request_id.
  6. RBAC check in the handler uses the standard helper (profile.hasAnyRole(...) or profile.hasPermission(...)) — don't hardcode role codes per file.

1.6 Testing acceptance

Before a panel is considered done:

  • go test passing for the store helper + handler
  • flutter analyze clean
  • Manual smoke: List → Add → Edit → Delete → verify the audit row appears
  • RBAC probe: log in as 5 roles → verify Add/Edit/Delete buttons only appear per matrix
  • 409 path: trigger a duplicate (e.g. assign a role that already exists) → UI shows a readable error
  • Concurrent edit: 2 tabs editing the same row → the second save 409/stale-check (optimistic locking via updated_at)

2. Current State — Users & Access (as of 2026-04-23, post Ship-13)

PanelListAddEditDelete/RevokeOverall status
Users✅ (status toggle)Complete
RolesComplete
Permissionsn/a (migration-only)✅ ship-13.3 (metadata)n/a (migration-only)Complete
Role Matrix✅ redirect-dialog✅ redirect-dialog✅ redirect-dialogBy design — directs to Role Access Map (Ship-13.4)
Role Access Map✅ (grant)n/a✅ (revoke)Complete
User Roles✅ ship-13.2 (change role)✅ (revoke)Complete
Platform Access✅ ship-12✅ ship-12✅ ship-12Complete
Devicesn/a (auto-register)n/a✅ ship-12 (revoke+session-kill)Complete
Sessionsn/an/a✅ ship-13.1 (per-session + logout-all)Complete
Audit Logsn/an/an/aComplete — ship-13.5 filter + CSV export

3. Ship-13 Checklist — Finish User & Access

Ship-13 status (as of 2026-04-23):COMPLETE except Reset-Password (deferred, needs SMTP pipeline) and the Force Rekey FE row button (polish item 13.6.b).

Execution order by priority (high → low).

3.1 Sessions → Revoke (HIGH — security) ✅ DONE

Why: an admin must be able to force-log-out a suspect user session (lost phone, security incident, employee rotation) without revoking the device entirely.

Backend:

  • Helper store revokeDashboardSession(ctx, db, sessionID) → UPDATE auth.user_sessions SET revoked_at=now() WHERE id=$1 AND revoked_at IS NULL. Lock + return row.
  • Helper revokeAllDashboardUserSessions(ctx, db, userID) for "Logout Everywhere" — UPDATE WHERE user_id=$1.
  • Handler POST /api/dashboard/users-access/sessions/{id}/revoke + POST /.../users/{id}/logout-all.
  • RBAC: superadmin, admin, support via the sessionRevokeRoles constant.
  • Audit row via insertAuditLog (Ship-13.7 helper).

Frontend:

  • API: revokeSession, logoutAllUserSessions in dashboard_home_api.dart.
  • SessionsPanel — Action column + per-row Terminate button (red power_settings_new icon), confirmation + reload. Shown only for active sessions.
  • (Polish) Logout Everywhere button in the UsersPanel detail dialog — API ready, just needs the button. Ship-13.1.b.

Actual effort: ~0.75 day.

3.2 User Roles → Edit assignment (MEDIUM — UX) ✅ DONE

Why: the "Revoke + Add" workaround breaks the audit chain (looks like the user lost a role and then got a new one). A direct edit preserves history.

Backend:

  • Handler PATCH /api/dashboard/users-access/user-roles/{userId}/{oldRoleCode} body {new_role_code} → transaction: DELETE old + INSERT new in identity.user_roles + 1 audit row user_role_updated with metadata old_role_code + new_role_code.
  • Validation: the old assignment must exist (sql.ErrNoRows otherwise), the new role scope must be web|internal, must differ from the old one.

Frontend:

  • API updateUserRole in dashboard_home_api.dart.
  • Edit button on the User Roles panel row → _ChangeRoleDialog with a role dropdown that excludes the current role → PATCH → reload.

Actual effort: ~0.4 day.

3.3 Permissions → Metadata Edit (LOW — convenience) ✅ DONE

Why: seed migration 014 is already complete. But occasionally name or description need to change without a new migration (e.g. typo, clarification).

Backend:

  • Handler PATCH /api/dashboard/users-access/permissions/{code} — only name + description. code + module immutable.
  • RBAC: super_admin only (tighter than other writes).
  • Audit row via insertAuditLogTx (permission_metadata_updated event with old/new snapshot).

Frontend:

  • API updatePermissionMetadata in dashboard_home_api.dart.
  • _PermissionEditDialog read-only for code/module, editable for name + description.
  • Delete button → _showDeleteNotAllowed SnackBar: "Permissions cannot be deleted from the UI — structural changes need a migration."

Actual effort: ~0.4 day.

Why: the current Role Matrix shows a SnackBar "use Role Access Map". A clearer UI is needed.

Implementation (pragmatic — no state plumbing):

  • Replace the SnackBar with an informative AlertDialog: info icon + "Add/Edit/Delete in Role Access Map" title + clear body + "Got it" button.
  • (Deferred) True deep-link navigation needs sidebar-state plumbing across widgets (UsersAccessSection ↔ home_dashboard_content ↔ UI state). Can be added later if it becomes an operator blocker — for now the AlertDialog already explains the flow.

Actual effort: ~0.2 day.

3.5 Audit Panel Filter + Export (LOW — compliance) ✅ DONE

Why: the audit logs are currently just a list. For compliance/forensic needs we need filter by actor, event, status + CSV export.

Backend:

  • Extend GET /audit-logs with query params actor_user_id, event_type, status, search, from, to, limit.
  • GET /audit-logs/export — stream CSV, Admin+ only, cap 10k rows, Content-Disposition header for browser-native download.
  • AuditLogFilter struct + queryDashboardAuditLogs shared helper between list and export.

Frontend:

  • API methods getAuditLogsFiltered + buildAuditLogsExportUrl + downloadAuditLogsCsv in dashboard_home_api.dart.
  • Export CSV button in the AuditLogsPanel header → downloadAuditLogsCsv (Bearer auth) → triggerCsvDownload (Blob anchor-click on web, no-op on mobile/desktop).
  • Cross-platform helper: services/file_download.dart + platform-specific file_download_io.dart / file_download_web.dart.
  • (Polish) Date-range picker UI — backend already supports it, UI later. Ship-13.5.b.

Actual effort: ~1 day.

3.6 User Profile — Reset Password + Force Rekey (MEDIUM — security)

Why: an admin needs to be able to reset a forgetful user's password (or force rotation after an incident) + invalidate all refresh tokens (rekey).

3.6.a Force Rekey ✅ DONE

Backend:

  • Handler POST /api/dashboard/users-access/users/{id}/force-rekey — call revokeAllDashboardUserSessions (reuse Ship-13.1 helper) + audit row user_force_rekey.
  • RBAC: superadmin + admin only (tighter than the regular logout-all which also allows support).
  • Response: {status:"rekeyed", revoked_count:N}.

Frontend:

  • API forceRekeyUser in dashboard_home_api.dart.
  • (Polish — Ship-13.6.b) Force Rekey button (red, warning "All sessions + tokens will be invalidated") in the UsersPanel row menu. API ready, just needs the button + confirmation dialog.

3.6.b Reset Password — ⏸ DEFERRED to Ship-14

Reason for deferring: needs a delivery pipeline (SMTP via core-api internal call) + a reset landing page + a token-consume endpoint. Not a security blocker (force-rekey already covers the account-compromise scenario). Split into its own Ship-14:

  • Migration: optional identity.users.rekey_version for pre-expiry JWT invalidation.
  • Endpoint POST /users/{id}/reset-password → generate token → insert auth.dashboard_password_reset_tokens → POST to core-api /api/internal/email with the reset link.
  • Landing page /reset-password?token=... in core-api.
  • Complete reset endpoint that consumes the token + updates password_hash in identity.users.

Actual Ship-13.6.a effort: ~0.3 day. Ship-14 (reset password): estimated 1.5 days.

3.7 Audit Log Writer Standardization ✅ DONE (scoped)

Why: today the audit handlers are sometimes inline, sometimes in a helper. Standardize so there is one consistent entry point.

Backend:

  • Helper insertAuditLog(ctx, db, ...) non-tx + insertAuditLogTx(ctx, tx, ...) in-tx — both now have a canonical doc header in db.go explaining usage rules, naming convention, status values, metadata rules.
  • Ship-13.1 (Sessions), 13.2 (User Roles), 13.3 (Permissions), 13.6 (Force Rekey) all use this helper — new handlers automatically follow the pattern.
  • (Deferred — Ship-14.x) Bump the auth.auth_audit_logs schema (db_kesles_merchant_auth): add request_id uuid, ip_address varchar(64), user_agent text columns + a UUID v7 middleware injector in the context. Currently sufficient for basic audit trail.
  • (Deferred) Refactor old handlers (vendors, PO, GR) that still use manual INSERTs → call the helper. Audit correctness via a migration-level SELECT once a quarter.

Actual effort: ~0.2 day (doc + wrapper). Schema bump deferred until there's a real SIEM need.


4. Total Ship-13 Effort

ItemEstimatedActualStatus
3.1 Sessions revoke1.0 d0.75 d
3.2 User Roles edit0.5 d0.4 d
3.3 Permissions metadata edit0.5 d0.4 d
3.4 Role Matrix deep-link0.5 d0.2 d✅ (pragmatic AlertDialog)
3.5 Audit filter + export1.5 d1.0 d
3.6.a Force rekey0.5 d0.3 d
3.6.b Reset password1.0 d⏸ Ship-14
3.7 Audit writer standardization1.0 d0.2 d✅ (scoped — schema bump deferred)
Total Ship-13 shipped5.5 d~3.25 d

5. Standard Platform Checklist (for other Kesles dashboards)

Every new dashboard built (mobile-partner-dashboard, pos-admin-dashboard, kiosk-admin, etc.) must satisfy this checklist 100% before v1.0.0 release:

5.1 Backend

  • Every mutating endpoint goes through a centralized RBAC check (requireRoles() or requirePermission() helper).
  • Every mutating endpoint writes an audit row via the standard helper.
  • Consistent response shape ({items, meta} / {item} / {error, message, field?}).
  • HTTP status codes per the §1.4 table.
  • Validation errors carry field for FE highlighting.
  • Optimistic locking for edit (check updated_at if the payload carries it; 409 if stale).
  • Rate limit on destructive endpoints (delete, revoke, force-logout) — minimum 10/minute per admin.
  • Test coverage ≥ 70% for store helpers.
  • Integration tests for the happy path + 1 error path per mutating endpoint.

5.2 Frontend

  • Panel layout follows the §1.3 template (header, filter, table, footer, pagination).
  • Add button only appears when meta.can_write=true.
  • Per-row action icons: max 3 visible + overflow menu.
  • Destructive action goes through an AlertDialog confirmation.
  • Loading state (spinner / dim) while in-flight.
  • Error SnackBar + auto-reload on success.
  • flutter analyze clean, zero warnings.
  • ValueKey('form-<label>') on TextFormFields when fields can be reordered (prevent state-swap bugs).
  • Widget test for form validation (required, format).

5.3 Operations

  • An operations SOP (mirroring dashboard-role-access-sop.md) is written for team onboarding.
  • Incident-response runbook: who can revoke, when, the approval bound.
  • Audit log retention policy (default: 2 years, finance compliance: 5 years).
  • Backup + restore procedure for the identity.* / auth.* tables (db_kesles_merchant_auth).

5.4 Anti-patterns (MUST AVOID)

  • ❌ "Coming soon" SnackBar as a permanent button — remove the button if the endpoint isn't there.
  • ❌ Hardcoding role codes per handler — use the permission/role helper.
  • ❌ Delete without a confirmation dialog.
  • ❌ Hard-deleting a row that has an active FK relationship without a clear cascade strategy.
  • ❌ Writing sensitive fields (password, token, sensitive PII) to the audit log in plaintext.
  • ❌ Destructive action via a GET request (Meta tab restore / browser pre-fetch → auto-execute).
  • ❌ An action button that shows no loading state → double-click double-action.

6. Cross-references


7. Review & sign-off

ReviewerRoleDateNotes
(TBD)Tech Lead
(TBD)Security
(TBD)Operations

This document must be re-reviewed every 6 months or after any access incident that produces a new action item.