Files
HIPCTF2/docs/guides/admin-general-settings.md
T
2026-07-22 12:45:42 +00:00

163 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: guide
title: Admin — General Settings
description: How an admin edits global platform settings (page title, logo, theme, event window, default challenge IP, registrations, welcome Markdown) from the /admin/general page.
tags: [guide, admin, settings, general, tester]
timestamp: 2026-07-22T12:44:45Z
---
# When this view is available
The General Settings page is rendered at `/admin/general` for users with
`role === 'admin'` once the instance is initialized. It is reached from
the [Admin Shell](/guides/admin-shell.md) side-nav (`General` entry) or
by navigating directly to the URL.
| Layer | File | Check |
|------------------|----------------------------------------------------------------------------|------------------------------------------------|
| Client route | `frontend/src/app/app.routes.ts` | `/admin/general` child of `adminGuard`. |
| Client component | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent.ngOnInit` fetches settings + themes. |
| Client predicate | `frontend/src/app/features/admin/general.pure.ts` (`deriveEventState`, `pageTitleError`, `pageTitleMessage`) | Pure helpers for event-state derivation and Page-title error/message mapping. |
| Server route | `backend/src/modules/admin/admin-general.controller.ts` | `GET/PUT /api/v1/admin/general/settings` + `GET /api/v1/admin/general/themes`. |
# How to access (tester steps)
1. Sign in as an admin user (see [First-Run Bootstrap](/guides/bootstrap.md)
if no admin exists).
2. Open the username menu in the shell header and click **Admin area**,
or use the side-nav's **General** entry, or visit `/admin/general`
directly.
3. The page renders a `Loading settings...` placeholder while the
initial `GET /api/v1/admin/general/settings` + `GET .../themes`
requests are in flight.
# Fields
The page is a single reactive form with these controls (every
`data-testid` listed is asserted in the existing test suite):
| Label | `data-testid` | Backend field | Notes |
|------------------------|---------------------------|-------------------------|-------|
| Page title | `general-pageTitle` | `pageTitle` | Required, non-blank, max 120 chars; trimmed server-side. |
| Logo (file picker) | `general-logo-file` | `logo` (public URL) | Uploads via `POST /api/v1/uploads/logo`; the returned `publicUrl` is bound to a hidden input `general-logo`. |
| Logo upload status | `general-logo-uploading` / `general-logo-error` / `general-logo-current` | — | Inline status text under the file picker. |
| Global theme | `general-themeKey` | `themeKey` | `<select>` populated from `/themes`. Value is one of `THEME_IDS`. |
| Event start (UTC) | `general-eventStart` | `eventStartUtc` | `datetime-local` input converted to ISO UTC on save. |
| Event end (UTC) | `general-eventEnd` | `eventEndUtc` | Must be strictly after Event start. Invalid pair renders `general-endBeforeStart`. |
| Default challenge IP | `general-defaultIp` | `defaultChallengeIp` | Required, max 255 chars. |
| Enable registrations | `general-registrations` | `registrationsEnabled` | Boolean checkbox. When `false`, the public register endpoint returns `REGISTRATIONS_DISABLED`. |
| Welcome description | `general-welcome` | `welcomeMarkdown` | Multi-line textarea; a live preview is rendered into `general-welcome-preview`. |
| Event controls | `general-event-toggle` | (derived) | Disabled button whose text is the derived event state (`UNCONFIGURED` / `COUNTDOWN` / `RUNNING` / `STOPPED`). |
| Save button | `general-save` | — | Disabled while `submitting()` or `form.invalid`. |
| Save error / success | `general-save-error` / `general-save-ok` | — | Inline status. |
# Expected behavior
* **Initial load:** `loading() === true` renders `general-loading`. After
both requests resolve, the form is patched with the values from the
backend (UTC timestamps are converted to `datetime-local` strings via
`toDatetimeLocal` so the native picker shows them).
* **Logo upload:** selecting a file fires `POST /api/v1/uploads/logo`,
then writes the returned `publicUrl` into the hidden `logo` control.
If the upload fails, `general-logo-error` shows the message; the
previous logo is preserved.
* **Welcome Markdown preview:** every keystroke in `general-welcome`
triggers `MarkdownService.render` and updates `general-welcome-preview`
synchronously.
* **Event-state derivation:** the disabled `general-event-toggle` label
is computed from `deriveEventState(start, end)`:
* both empty → `UNCONFIGURED`
* `now < start``COUNTDOWN`
* `start <= now < end``RUNNING`
* `now >= end``STOPPED`
* **End-before-start validation:** if the user picks an end that is not
strictly after the start, the form becomes invalid and
`general-endBeforeStart` appears under the end input. Save remains
disabled.
* **Save:** clicking Save sends `PUT /api/v1/admin/general/settings`
with all fields. The Page title input is validated client-side for
non-blank content; an empty or whitespace-only Page title disables the
Save button and renders `general-pageTitle-error` so the request is
never issued. The server trims surrounding whitespace and re-validates
against the same 1120 character rule, so invalid payloads return
`400 VALIDATION_FAILED` and the stored Page title is unchanged.
On success the form is patched with the response, the Page-title
control is marked untouched/pristine (so a freshly-loaded valid
form does not flash an error), `general-save-ok` renders briefly, and
the backend emits an SSE `general` event via `SseHubService` so
other tabs refresh their theme.
* **Error states:** load failures render `general-error`; save failures
render `general-save-error` with the `error.message` (or
`error.error.message`) from the standard envelope.
# Page-title inline error message
The inline error message under the Page-title field
(`general-pageTitle-error`) is driven by the pure helper
`pageTitleMessage(value, controlErrors, maxLength = 120)` exported from
`frontend/src/app/features/admin/general.pure.ts:10-22`. The component
keeps three local `signal`s — `pageTitleValue`, `pageTitleInvalid`,
`pageTitleTouchedOrDirty` — and subscribes to the reactive form
control's `valueChanges` and `statusChanges` (via
`takeUntilDestroyed(this.destroyRef)`) so the `computed`s for
`showPageTitleError` and `pageTitleMessage` re-evaluate when the user
types or blurs the input. The helper returns:
| Condition | Message |
|--------------------------------------------------------------|-----------------------------------------------------------|
| Empty or whitespace-only value | `Page title is required and cannot contain only whitespace.` |
| Value length > 120 | `Page title must be 120 characters or fewer.` |
| Control has `required`, `maxlength`, or `whitespace` error | Matching human-readable message (whitespace falls back to "required" wording). |
| Valid value | `null` (no message rendered, Save becomes enabled). |
As a result, the `general-pageTitle-error` element renders as soon as
the Page-title control becomes both `touched || dirty` AND `invalid`,
without requiring a full component re-render. After a successful
Save, the control is reset to untouched/pristine and the
`pageTitleTouchedOrDirty` signal is cleared, so reloading valid
settings does not flash a stale error.
# Visual elements
| Element | Selector |
|-------------------------------|-----------------------------------------------------|
| Page section | `[data-testid="admin-general"]` |
| Loading placeholder | `[data-testid="general-loading"]` |
| Load error | `[data-testid="general-error"]` |
| Form | `[data-testid="general-form"]` |
| Welcome preview | `[data-testid="general-welcome-preview"]` |
| End-before-start message | `[data-testid="general-endBeforeStart"]` |
| Page-title inline error | `[data-testid="general-pageTitle-error"]` |
# Architecture map
| Step | Where | What happens |
|------|------------------------------------------------------|---------------------------------------------------------------------------|
| 1 | `frontend/src/app/app.routes.ts` | `/admin/general` lazy-loads `AdminGeneralComponent`. |
| 2 | `frontend/src/app/features/admin/general.component.ts` | `ngOnInit` calls `AdminService.getGeneralSettings()` + `listAdminThemes()` in parallel. |
| 3 | `frontend/src/app/core/services/admin.service.ts` | `getGeneralSettings()``GET /api/v1/admin/general/settings`; `listAdminThemes()``GET /api/v1/admin/general/themes`; `updateGeneralSettings()``PUT .../settings`; `uploadLogo()``POST /api/v1/uploads/logo`. |
| 4 | `backend/src/modules/admin/admin-general.controller.ts` | `AdminGuard` + `@Roles('admin')` on every handler. |
| 5 | `backend/src/modules/admin/general.service.ts` | `getSettings` reads 8 keys via `SettingsService`; `updateSettings` writes all 8, emits `{ topic: 'general', themeKey }` via `SseHubService`. |
| 6 | `backend/src/modules/admin/dto/general.dto.ts` | `GeneralSettingsSchema` enforces string lengths, `themeKey` enum, and `eventEndUtc > eventStartUtc` via `superRefine`. |
# Notes
* The `general` SSE event (`{ topic: 'general', themeKey }`) is a
lightweight signal so authenticated tabs can pick up the new theme
without polling. Other tabs do not auto-refresh settings values.
* The "Event controls" toggle is intentionally a derived display, not
an editable control — adjust the UTC timestamps to change state.
* All timestamps are stored as ISO-8601 UTC strings in the `setting`
table; the UI converts to/from `datetime-local` for display.
* Saving the form requires the page title and default challenge IP to
be non-empty; the form is `invalid` and Save stays disabled until
they are.
# See also
- [Admin Shell](/guides/admin-shell.md) — side-nav layout and guard chain.
- [Admin — Categories](/guides/admin-categories.md) — categories management page that renders below General settings.
- [Admin Endpoints](/api/admin.md) — `GET/PUT /api/v1/admin/general/*` reference.
- [Uploads Endpoints](/api/uploads.md) — `POST /api/v1/uploads/logo` reference.
- [Backend Module Map](/architecture/backend-modules.md)