6.3 KiB
6.3 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| guide | Admin Shell & User Management | How an authenticated admin navigates the post-login shell and reaches the admin user-management area. |
|
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)
- Ensure the instance is initialized (
/api/v1/bootstrapreturnsinitialized: true). If not, follow First-Run Bootstrap. - Sign in as a user with
role === 'admin'via/login. - The browser lands on
/and renders the Home shell with a header containing the page title and a sign-in status line. - Click the Admin nav link (
data-testid="nav-admin") in the shell header, or navigate directly to/admin. - 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<li>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 stillfalsethe guard redirects to/bootstrapinstead 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
/adminwithout 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 <router-outlet> 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 <li> 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 <router-outlet>. |
| 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
HomeComponentas a thin layout owner — child routes (currently just/admin) render into its<router-outlet>. 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. Seetests/frontend/admin-shell.spec.tsandtests/frontend/admin-navigation.spec.ts.