--- type: guide title: Admin Shell & User Management description: How an authenticated admin navigates the post-login shell and reaches the admin user-management area. tags: [guide, admin, shell, navigation, tester] timestamp: 2026-07-21T15:05:00Z --- # When this view is available The admin shell is the post-login UI for users with `role === 'admin'`. It is gated by: | Layer | File | Check | |------------------|--------------------------------------------------------------------------------------------|------------------------------------| | Client route | `frontend/src/app/app.routes.ts` | `/admin` uses `adminGuard`. | | Client nav link | `frontend/src/app/features/home/home.component.ts` | Renders only when `showAdminNav()` is true. | | Client predicate | `frontend/src/app/features/home/home.shell.ts` (`shouldShowAdminNav`) | `isAuthenticated && role === 'admin'`. | | Server route | `backend/src/modules/admin/admin.controller.ts` (mounted under `@UseGuards(AdminGuard)` + `@Roles('admin')`) | Requires admin JWT. | A non-admin (or unauthenticated) request to `/admin` is intercepted by the client guard *before* any HTTP call is made, so the page never renders. # How to access (tester steps) 1. Ensure the instance is initialized (`/api/v1/bootstrap` returns `initialized: true`). If not, follow [First-Run Bootstrap](/guides/bootstrap.md). 2. Sign in as a user with `role === 'admin'` via `/login`. 3. The browser lands on `/` and renders the **Home shell** with a header containing the page title and a sign-in status line. 4. Click the **Admin** nav link (`data-testid="nav-admin"`) in the shell header, or navigate directly to `/admin`. 5. The admin area renders inside the shell: - **Heading:** "Admin area" - **Loading state:** `Loading users...` (`data-testid="admin-loading"`) while the GET is in flight. - **User list:** an unordered list (`data-testid="admin-user-list"`) showing one `
  • ` per user with the username in bold and the role in parentheses, e.g. `root (admin)`. - **Error state:** a red message (`data-testid="admin-error"`) when the request fails (e.g. backend down or auth expired). # Expected behavior | User role | Visiting `/admin` | |----------------------|---------------------------------------------------------| | Unauthenticated | Redirected to `/login` by `adminGuard`. | | Authenticated player | Redirected to `/` by `adminGuard`. | | Authenticated admin | Page renders, fetches `GET /api/v1/admin/users`, lists users. | * When `BootstrapService.initialized()` is still `false` the guard redirects to `/bootstrap` instead of `/login`, so the first admin flow is honored. * The Admin nav link is hidden for non-admin users — there is no visible UI affordance to reach `/admin` without the role. * The list endpoint uses the existing `authInterceptor` + `csrfInterceptor`, so no extra plumbing is required. # Visual elements | Element | Selector | Purpose | |----------------------|-----------------------------------|----------------------------------------------| | Shell header | `.shell-header` | Page title + signed-in identity. | | Admin nav link | `[data-testid="nav-admin"]` | RouterLink to `/admin`; shown to admins only. | | Shell body | `.shell-body` | Holds `` for child routes. | | Admin heading | `h2` inside `.admin-area` | "Admin area". | | Loading message | `[data-testid="admin-loading"]` | "Loading users...". | | Error message | `[data-testid="admin-error"]` | Red text; renders `error().error.message` or fallback. | | User list | `[data-testid="admin-user-list"]` | Renders one `
  • ` per `AdminUser`. | # Architecture map | Step | Where | What happens | |------|----------------------------------------------------|-----------------------------------------------------------------------| | 1 | `frontend/src/app/app.routes.ts` | `/` is a parent route with `authGuard`; `/admin` child uses `adminGuard` + lazy-loads `AdminUsersComponent`. | | 2 | `frontend/src/app/core/guards/admin.guard.ts` | `adminGuard` calls `decideAdminGuard(...)` with bootstrap + auth state. | | 3 | `frontend/src/app/core/guards/admin.guard.decision.ts` | Pure function returning `{kind: 'allow'}` or `{kind: 'redirect', path}`. | | 4 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and ``. | | 5 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). | | 6 | `frontend/src/app/features/admin/admin-users.component.ts` | On init, calls `AdminService.listUsers()` and populates signals (`loading`, `error`, `users`). | | 7 | `frontend/src/app/core/services/admin.service.ts` | `GET /api/v1/admin/users` (credentials included); typed as `AdminUser[]`. | | 8 | `backend/src/modules/admin/admin.controller.ts` | `AdminController.list` returns `AdminService.listUsers({limit, cursor, role})`. | # Notes * The shell deliberately keeps `HomeComponent` as a thin layout owner — child routes (currently just `/admin`) render into its ``. New admin sub-pages can be added as additional children without changing the shell. * The guard decision function (`decideAdminGuard`) is a pure module so it is straightforward to unit-test without Angular DI. See `tests/frontend/admin-shell.spec.ts` and `tests/frontend/admin-navigation.spec.ts`. # See also - [First-Run Bootstrap](/guides/bootstrap.md) - [Frontend Structure](/architecture/frontend-structure.md) - [REST API Overview](/api/rest-overview.md) - [Admin Endpoints](/api/admin.md)