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:
- Completion checklist for the Users & Access menu so every sub-panel has full CRUD/action (not just "coming soon").
- 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)
| Action | HTTP | Endpoint pattern | UI entry-point | Confirmation | Audit |
|---|---|---|---|---|---|
| List | GET | /{domain}/{resource}?limit&offset&search&status | main panel page, paginated | – | – |
| Detail | GET | /{domain}/{resource}/{id} | row click (or view button) | – | log read if data is sensitive (e.g. KTP photo URL) |
| Add | POST | /{domain}/{resource} | "+ Add Data" button in the panel header | form dialog with backend-side validation | 1 audit row: create |
| Edit | PATCH | /{domain}/{resource}/{id} | edit icon on the row | form dialog prefilled + diff-aware save | 1 audit row: update with before/after diff |
| Delete | DELETE | /{domain}/{resource}/{id} | trash icon on the row | confirmation dialog "type the name to delete" if hard-delete | 1 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 / reject —
POST .../{id}/approve+/{id}/reject, with areasonfield. - Bulk action —
POST .../bulk-{action}with an array of ids, atomic (all or nothing). - Assign / revoke relationship —
POST .../{parent_id}/{child_resource}+ the sameDELETE. ExamplePOST /users/{id}/roles/{role_code}.
1.3 Standard dialog shell
Every Users & Access dialog — and every future Kesles dashboard — must use a consistent shell:
| Need | Shell | Location |
|---|---|---|
| CRUD form (Add/Edit) + destructive confirm | StandardDialog | dashboard_shell_shared_widgets.dart |
| Read-only detail (more content) | DashboardDialogShell | ditto |
| Long form (purchase order, vendor, etc.) | DashboardFormDialogShell | ditto |
Rules:
- Avoid raw Material
AlertDialog— visuals don't match (border radius, action alignment, icon-in-circle). StandardDialoghas 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 itareNavigator.pop— no UX trap. actionBusy+actionEnabledare used for the loading state;actionBusy=truedisables 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_vertoverflow menu. - Every destructive action (Delete, Revoke, Force-logout, Reset password) MUST go through an
AlertDialogconfirmation — 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.messageafterreplaceFirst('Exception: ', '')). Don't expose the stacktrace. - Success: green SnackBar + auto-reload of the list.
1.5 Backend contract
- Consistent response shape for every endpoint:
or single:{ "items": [...], "meta": {"limit": 10, "offset": 0, "total": 83, "can_write": true} }
{ "item": {...} }. meta.can_writeis driven by the caller's role/permission — the FE uses this to hide/show buttons (don't re-check role in the FE).- Consistent error shape:
{ "error": "validation_failed", "message": "swift_code must be 8 or 11 characters", "field": "swift_code" }
- 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.
- Audit write for every mutating endpoint — insert into
auth.auth_audit_logs(db_kesles_merchant_auth) withactor_user_id,resource_type,resource_id,action,before,after,request_id. - RBAC check in the handler uses the standard helper (
profile.hasAnyRole(...)orprofile.hasPermission(...)) — don't hardcode role codes per file.
1.6 Testing acceptance
Before a panel is considered done:
-
go testpassing for the store helper + handler -
flutter analyzeclean - 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)
| Panel | List | Add | Edit | Delete/Revoke | Overall status |
|---|---|---|---|---|---|
| Users | ✅ | ✅ | ✅ | ✅ (status toggle) | Complete |
| Roles | ✅ | ✅ | ✅ | ✅ | Complete |
| Permissions | ✅ | n/a (migration-only) | ✅ ship-13.3 (metadata) | n/a (migration-only) | Complete |
| Role Matrix | ✅ | ✅ redirect-dialog | ✅ redirect-dialog | ✅ redirect-dialog | By 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-12 | Complete |
| Devices | ✅ | n/a (auto-register) | n/a | ✅ ship-12 (revoke+session-kill) | Complete |
| Sessions | ✅ | n/a | n/a | ✅ ship-13.1 (per-session + logout-all) | Complete |
| Audit Logs | ✅ | n/a | n/a | n/a | Complete — 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)→ UPDATEauth.user_sessionsSETrevoked_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, supportvia thesessionRevokeRolesconstant. - Audit row via
insertAuditLog(Ship-13.7 helper).
Frontend:
- API:
revokeSession,logoutAllUserSessionsindashboard_home_api.dart. -
SessionsPanel— Action column + per-row Terminate button (redpower_settings_newicon), confirmation + reload. Shown only for active sessions. - (Polish) Logout Everywhere button in the
UsersPaneldetail 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 inidentity.user_roles+ 1 audit rowuser_role_updatedwith metadataold_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
updateUserRoleindashboard_home_api.dart. - Edit button on the User Roles panel row →
_ChangeRoleDialogwith 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}— onlyname+description.code+moduleimmutable. - RBAC: super_admin only (tighter than other writes).
- Audit row via
insertAuditLogTx(permission_metadata_updatedevent with old/new snapshot).
Frontend:
- API
updatePermissionMetadataindashboard_home_api.dart. -
_PermissionEditDialogread-only for code/module, editable for name + description. - Delete button →
_showDeleteNotAllowedSnackBar: "Permissions cannot be deleted from the UI — structural changes need a migration."
Actual effort: ~0.4 day.
3.4 Role Matrix → Deep-link to Role Access Map (LOW — UX polish) ✅ DONE (pragmatic)
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-logswith query paramsactor_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. -
AuditLogFilterstruct +queryDashboardAuditLogsshared helper between list and export.
Frontend:
- API methods
getAuditLogsFiltered+buildAuditLogsExportUrl+downloadAuditLogsCsvindashboard_home_api.dart. - Export CSV button in the
AuditLogsPanelheader →downloadAuditLogsCsv(Bearer auth) →triggerCsvDownload(Blob anchor-click on web, no-op on mobile/desktop). - Cross-platform helper:
services/file_download.dart+ platform-specificfile_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— callrevokeAllDashboardUserSessions(reuse Ship-13.1 helper) + audit rowuser_force_rekey. - RBAC: superadmin + admin only (tighter than the regular logout-all which also allows support).
- Response:
{status:"rekeyed", revoked_count:N}.
Frontend:
- API
forceRekeyUserindashboard_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_versionfor pre-expiry JWT invalidation. - Endpoint
POST /users/{id}/reset-password→ generate token → insertauth.dashboard_password_reset_tokens→ POST to core-api/api/internal/emailwith the reset link. - Landing page
/reset-password?token=...in core-api. - Complete reset endpoint that consumes the token + updates
password_hashinidentity.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 indb.goexplaining 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_logsschema (db_kesles_merchant_auth): addrequest_id uuid,ip_address varchar(64),user_agent textcolumns + 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
| Item | Estimated | Actual | Status |
|---|---|---|---|
| 3.1 Sessions revoke | 1.0 d | 0.75 d | ✅ |
| 3.2 User Roles edit | 0.5 d | 0.4 d | ✅ |
| 3.3 Permissions metadata edit | 0.5 d | 0.4 d | ✅ |
| 3.4 Role Matrix deep-link | 0.5 d | 0.2 d | ✅ (pragmatic AlertDialog) |
| 3.5 Audit filter + export | 1.5 d | 1.0 d | ✅ |
| 3.6.a Force rekey | 0.5 d | 0.3 d | ✅ |
| 3.6.b Reset password | 1.0 d | – | ⏸ Ship-14 |
| 3.7 Audit writer standardization | 1.0 d | 0.2 d | ✅ (scoped — schema bump deferred) |
| Total Ship-13 shipped | 5.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()orrequirePermission()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
fieldfor FE highlighting. - Optimistic locking for edit (check
updated_atif 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
AlertDialogconfirmation. - Loading state (spinner / dim) while in-flight.
- Error SnackBar + auto-reload on success.
-
flutter analyzeclean, 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
- Operations SOP: merchant_docs/services/dashboard/role-access-sop.md — policy level.
- Partner access scope implementation: merchant_docs/architecture/partner-access-scope-plan.md — example of a full Ship pattern.
- Purchasing & Sales refactor: merchant_docs/architecture/purchasing-sales-refactor-plan.md — example of a Ship with migrations + handlers.
- Vendor expansion + PDF: merchant_docs/architecture/vendor-expansion-pdf-plan.md — example of an extension migration pattern.
7. Review & sign-off
| Reviewer | Role | Date | Notes |
|---|---|---|---|
| (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.