131 lines
8.9 KiB
Markdown
131 lines
8.9 KiB
Markdown
---
|
|
type: guide
|
|
title: Admin Shell & Side Navigation
|
|
description: How an authenticated admin navigates the post-login admin area, including General, Challenges, Players, Categories, and Blog management pages.
|
|
tags: [guide, admin, shell, navigation, tester]
|
|
timestamp: 2026-07-23T10:12:24Z
|
|
---
|
|
|
|
# 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` 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` + `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
|
|
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 open the username menu and choose **Admin area**,
|
|
or navigate directly to `/admin`.
|
|
5. The admin area renders inside the shell:
|
|
- **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` | yes | [Admin — Challenges](/guides/admin-challenges.md) — sortable list, search, Add/Edit modal, file staging, import/export. |
|
|
| Players | `/admin/players` | yes | User management page. |
|
|
| Blog | `/admin/blog` | yes | [Blog Publishing and Reading](/guides/blog.md) — post list with draft/publish, edit, preview, and delete workflows. |
|
|
| 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
|
|
|
|
| User role | Visiting `/admin` |
|
|
|----------------------|---------------------------------------------------------|
|
|
| Unauthenticated | Redirected to `/login` by `adminGuard`. |
|
|
| Authenticated player | Redirected to `/` by `adminGuard`. |
|
|
| 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 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 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 `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/admin/blog/blog.component.ts` | `AdminBlogComponent` (`/admin/blog`); coordinates post listing, editing, publication, and deletion. |
|
|
| 8 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
|
|
| 9 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
|
|
| 10 | `frontend/src/app/core/services/blog.service.ts` | Typed wrappers for public blog listing and `/api/v1/admin/blog/posts` CRUD. |
|
|
| 11 | `backend/src/modules/blog/blog.module.ts` | Registers public and admin blog controllers/services with the blog repository. |
|
|
|
|
# Notes
|
|
|
|
* The shell deliberately keeps `AdminShellComponent` as a thin layout
|
|
owner — enabled child routes render into its `<router-outlet>`. New admin
|
|
sub-pages require both an enabled `ENTRIES` item and 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`.
|
|
* 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
|
|
|
|
- [First-Run Bootstrap](/guides/bootstrap.md)
|
|
- [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)
|
|
- [Blog Publishing and Reading](/guides/blog.md)
|
|
- [REST API Overview](/api/rest-overview.md)
|
|
- [Admin Endpoints](/api/admin.md)
|