AI Implementation feature(882): Admin Area General Settings and Categories 1.00 (#22)

This commit was merged in pull request #22.
This commit is contained in:
2026-07-22 12:05:16 +00:00
parent c9e8dfc611
commit fac3179427
9 changed files with 578 additions and 189 deletions
+70 -44
View File
@@ -1,9 +1,9 @@
---
type: guide
title: Admin Shell & User Management
description: How an authenticated admin navigates the post-login shell and reaches the admin user-management area.
title: Admin Shell & Side Navigation
description: How an authenticated admin navigates the post-login admin area, the side-nav layout, and how the General and Categories pages are reached.
tags: [guide, admin, shell, navigation, tester]
timestamp: 2026-07-21T22:19:08Z
timestamp: 2026-07-22T12:00:00Z
---
# When this view is available
@@ -13,10 +13,10 @@ It is gated by:
| Layer | File | Check |
|------------------|--------------------------------------------------------------------------------------------|------------------------------------|
| Client route | `frontend/src/app/app.routes.ts` | `/admin` uses `adminGuard`. |
| Client route | `frontend/src/app/app.routes.ts` | `/admin` parent uses `adminGuard`; children inherit it. |
| 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. |
| Server route | `backend/src/modules/admin/admin.controller.ts` + `admin-general.controller.ts` + `admin-categories.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
@@ -30,16 +30,34 @@ renders.
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`.
shell header, or open the username menu and choose **Admin area**,
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 `<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).
- **Aside** (`data-testid="admin-aside"`): heading "Admin area" and
a vertical side-nav (`data-testid="admin-nav"`).
- **Body** (`data-testid="admin-body"`): holds the
`<router-outlet />` that renders the active child page.
# Side-nav entries
The side-nav is defined as a static `ENTRIES` array in
`frontend/src/app/features/admin/admin-shell.component.ts`:
| Label | Path | Enabled | Renders |
|-------------|-----------------------|---------|----------------------------------------------------------------------------------------------|
| General | `/admin/general` | yes | [Admin — General Settings](/guides/admin-general-settings.md) (default — `/admin` redirects here). |
| Challenges | `/admin/challenges` | no | Greyed-out placeholder (`.disabled` class). No route registered. |
| Players | `/admin/players` | no | Greyed-out placeholder. No route registered. |
| Blog | `/admin/blog` | no | Greyed-out placeholder. No route registered. |
| System | `/admin/system` | no | Greyed-out placeholder. No route registered. |
Each `<li>` carries `data-testid="admin-nav-{id}"`. Disabled entries
have `cursor: not-allowed` and `opacity: 0.45`. Clicking a disabled
entry is a no-op (`go()` returns early when `!e.enabled`).
The active entry is highlighted by `[class.active]` whenever the
current URL starts with the entry's path. General is the default child
(`/admin``/admin/general`).
# Expected behavior
@@ -47,50 +65,56 @@ renders.
|----------------------|---------------------------------------------------------|
| Unauthenticated | Redirected to `/login` by `adminGuard`. |
| Authenticated player | Redirected to `/` by `adminGuard`. |
| Authenticated admin | Page renders, fetches `GET /api/v1/admin/users`, lists users. |
| Authenticated admin | Page renders, defaults to `/admin/general`. |
* 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.
* The Admin nav link in the shell header is hidden for non-admin users
— there is no visible UI affordance to reach `/admin` without the
role.
* All admin HTTP requests use 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`. |
| Element | Selector | Purpose |
|------------------------|-------------------------------------------|--------------------------------------------------------------------------|
| Shell aside | `[data-testid="admin-aside"]` | Side-nav container. Heading "Admin area". |
| Side-nav list | `[data-testid="admin-nav"]` | `<ul>` of all five entries. |
| Side-nav entry | `[data-testid="admin-nav-{id}"]` | One `<li>` per entry (General / Challenges / Players / Blog / System). |
| Shell body | `[data-testid="admin-body"]` | Wraps `<router-outlet />` for the active child page. |
# 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})`. |
| Step | Where | What happens |
|------|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| 1 | `frontend/src/app/app.routes.ts` | `/` is a parent route with `authGuard`; `/admin` child uses `adminGuard` + lazy-loads `AdminShellComponent` with its own child routes. |
| 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/admin/admin-shell.component.ts` | Renders the aside + body; `ENTRIES` is the static side-nav list. |
| 5 | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` (default `/admin/general`); embeds `AdminCategoriesComponent`. |
| 6 | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent` (`/admin/categories`); also rendered inside the General page. |
| 7 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
| 8 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
| 9 | `frontend/src/app/core/services/admin.service.ts` | Typed wrappers for `/api/v1/admin/users`, `/admin/general/*`, `/admin/categories/*`, `/uploads/{logo,category-icon}`. |
| 10 | `backend/src/modules/admin/admin.module.ts` | Registers `AdminController`, `AdminGeneralController`, `AdminCategoriesController` + their services. Imports `AuthModule`, `UsersModule`, `SettingsModule`, `CommonModule`, and `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity])`. |
# Notes
* The shell deliberately keeps `HomeComponent` as 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 shell deliberately keeps `AdminShellComponent` as a thin layout
owner — child routes (`general`, `categories`) render into its
`<router-outlet>`. New admin sub-pages can be added by enabling
entries in `ENTRIES` and registering a child route in `app.routes.ts`.
* 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`.
`tests/frontend/admin-shell.spec.ts` and
`tests/frontend/admin-navigation.spec.ts`.
* The Categories management page is intentionally embedded inside the
General settings page (see
[Admin — General Settings](/guides/admin-general-settings.md)) so an
admin can adjust platform settings and challenge taxonomy in one
place.
# See also
@@ -98,5 +122,7 @@ renders.
- [Frontend Structure](/architecture/frontend-structure.md)
- [Authenticated Shell](/guides/authenticated-shell.md) — header / LED / quick tabs / change-password modal / user menu (the surrounding shell this page renders into).
- [Change Password](/guides/change-password.md)
- [Admin — General Settings](/guides/admin-general-settings.md)
- [Admin — Categories](/guides/admin-categories.md)
- [REST API Overview](/api/rest-overview.md)
- [Admin Endpoints](/api/admin.md)
- [Admin Endpoints](/api/admin.md)