fix(frontend/ux): hide sections + buttons users can't access instead of toasting "admin access required" · Issue #365 · LeanerCloud/cloud-commitments-cli · GitHub
Skip to content

fix(frontend/ux): hide sections + buttons users can't access instead of toasting "admin access required" #365

Closed

Description

@cristim

Summary

When a non-admin user (or a user lacking a specific permission) lands on a page, the current UX is:

  1. Frontend unconditionally fires every API call the page would normally make.
  2. Backend returns 403 for the calls the user isn't allowed to make.
  3. Frontend catches each 403 and shows a red toast: Failed to load X: admin access required.

The result is a page full of empty sections + a stack of red error toasts, which is both ugly and misleading (the user didn't fail at anything — they're just looking at a page that's not for them). Better UX: hide what the user can't access, don't surface "denied" toasts at all. A readonly user shouldn't see "Create Plan" buttons whose only outcome is a 403.

PR #364 fixes the symptom for read endpoints by widening their Auth level to AuthUser. This issue tracks the broader cleanup: pre-emptively suppress UI that triggers permission-denied responses, rather than firing the call and catching the denial.

Scope

Two layers — same canAccess(action, resource) helper drives both:

1. Page-level — hide entire sections / nav entries the user can't reach

Section Hide when
Admin → Users (/admin/users) role !== admin
Admin → Registrations role !== admin (manages new-account approvals)
Admin → Cloud Accounts write actions (Add / Edit / Delete) role !== admin
Plans → Create / Edit / Delete buttons not in create:plans / update:plans / delete:plans
Purchases → Execute / Approve / Cancel buttons not in execute:purchases / approve:purchases
RI Exchange → Execute / Reject / Approve buttons role !== admin
Settings (/admin/general) write actions role !== admin
Top-nav "Admin" dropdown role !== admin

2. Element-level — hide mutating CTAs on otherwise-readable pages

Once the page itself is reachable for readonly, the per-row Edit / Delete / Run buttons in tables (plans, planned-purchases, users on admin pages) should be hidden for users who can't actually invoke the underlying endpoint. Currently they're rendered unconditionally and the 403 only surfaces on click.

Proposed design

A single canAccess(action, resource) helper in frontend/src/auth.ts (or a new permissions.ts) that:

  1. Reads state.currentUser.role (already populated post-login via /api/auth/me).
  2. Maps role → default permission set, matching the backend's DefaultAdminPermissions / DefaultUserPermissions / DefaultReadOnlyPermissions. This logic already exists in frontend/src/users/userList.ts:155-163 (effectivePermissions) — extract it into a shared module and reuse there.
  3. Optionally folds in group-grant permissions when the user has explicit groups[] entries (the backend's group-permissions stay authoritative; the frontend's job is only to decide whether to render an element).
// permissions.ts
export type Action = 'view' | 'create' | 'update' | 'delete' | 'execute' | 'approve' | 'admin';
export type Resource = 'recommendations' | 'plans' | 'purchases' | 'history' | 'accounts' | 'users' | 'groups' | '*';

export function canAccess(action: Action, resource: Resource): boolean { /* … */ }

Renderers then guard sections:

if (!canAccess('admin', '*')) return ''; // skip the whole admin section

And buttons:

${canAccess('create', 'plans') ? `<button class="btn-primary">Create plan</button>` : ''}

API call sites can also short-circuit when the user lacks the permission — no network round-trip just to discover the answer is no:

if (!canAccess('view', 'plans')) return; // skip the list-plans fetch entirely

Acceptance criteria

  • canAccess helper extracted into frontend/src/permissions.ts with unit tests covering the three role defaults + the empty-state.
  • userList.ts:effectivePermissions rewritten to delegate to the helper (one source of truth for role → permissions).
  • Top-nav: Admin dropdown hidden for non-admins.
  • Plans page: Create/Edit/Delete buttons hidden when the user lacks the permission.
  • Planned Purchases page: pause / resume / run / delete row buttons hidden for users lacking execute:purchases.
  • RI Exchange page: action buttons hidden for non-admins.
  • Admin → Users page: entire page hidden for non-admins; route also bounces (/admin/users → /).
  • No Failed to … : admin access required toast fires for a logged-in readonly user navigating the entire app (golden-path manual check).

Out of scope

  • Backend RBAC widening — separate work. The frontend just decides what to render based on the role currently in state; the backend stays authoritative.
  • Per-account permission scoping (the user has access to some accounts but not others). Backend already filters response rows by allowed_accounts; the frontend just renders what it gets back.

Related

Activity

  1. added a commit that references this issue on May 14, 2026
    d448d72
  2. cristim commented on May 20, 2026

    @cristim
    MemberAuthor

    Completed by #452 (merged 2026-05-14, commit d448d72). This issue stayed open only because #452 merged into the feat/multicloud-web-frontend feature branch rather than the default branch, so GitHub's auto-close didn't fire. The role-based hide-instead-of-toast gating is verified present in base (canAccess(...) in recommendations.ts/settings.ts, admin-only handling in auth.ts/base.css/index.html). Duplicate PR #534 closed as superseded.

  3. added a commit that references this issue on Sep 27, 2026
    a380130
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions