Files
HIPCTF2/docs/guides/admin-categories.md
T

9.8 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
guide Admin — Categories How an admin lists, creates, edits, and deletes challenge categories from the /admin/categories page, including system-row protection and challenge-attached protection.
guide
admin
categories
tester
2026-07-22T12:00:00Z

When this view is available

The Categories admin page renders at /admin/categories for users with role === 'admin'. It is reached from the Admin Shell side-nav (categories are also embedded inside the General settings page).

Layer File Check
Client route frontend/src/app/app.routes.ts /admin/categories child of adminGuard.
Client component frontend/src/app/features/admin/categories/categories.component.ts AdminCategoriesComponent.ngOnInit fetches the list.
Client modals frontend/src/app/features/admin/categories/category-form-modal.component.ts Create / edit modal.
frontend/src/app/features/admin/categories/category-delete-modal.component.ts Delete confirmation modal.
Server route backend/src/modules/admin/admin-categories.controller.ts GET/POST /api/v1/admin/categories, PUT/DELETE /:id.

How to access (tester steps)

  1. Sign in as an admin user.
  2. Open Admin areaCategories in the side-nav, or visit /admin/categories directly. (The page also renders below the General settings form at /admin/general.)
  3. The page shows Loading categories... (data-testid="cat-loading") while the list request is in flight, then renders one row per category sorted alphabetically by (lowercased) abbreviation.

Visual elements

Element Selector Purpose
Page section [data-testid="admin-categories"] Root container.
Add button (+) [data-testid="cat-add"] Opens the create modal.
Loading [data-testid="cat-loading"] "Loading categories..." placeholder.
Load error [data-testid="cat-error"] Red error text.
List [data-testid="cat-list"] <ul> of category rows.
Row [data-testid="cat-row-{ABBR}"] One <li> per category.
Edit button [data-testid="cat-edit-{ABBR}"] Opens the edit modal. Disabled for system rows (the abbreviation input becomes readonly).
Delete button [data-testid="cat-delete-{ABBR}"] Opens the delete confirmation modal.
Form modal [data-testid="cat-form-modal"] Create/edit dialog (cat-form-backdrop is the click-outside dismiss layer).
Delete modal [data-testid="cat-delete-modal"] Confirmation dialog (cat-delete-backdrop is the dismiss layer).
Delete error [data-testid="cat-delete-error"] Inline error message after a failed delete.

Form modal fields

Label data-testid Notes
Name cf-name Required, max 120 chars.
Abbreviation (uppercase) cf-abbr Required, 26 chars; server upper-cases on save. readonly when editing a system row.
Description cf-desc Optional, max 2000 chars.
Icon (file picker) cf-icon Optional image; uploaded to POST /api/v1/uploads/category-icon and resized/normalized server-side.
Cancel / OK cf-cancel, cf-ok OK disabled while form invalid or already submitting.

Expected behavior

List

  • The list is sorted by LOWER(abbreviation) ascending. Server returns rows already sorted; the client re-sorts defensively in AdminCategoriesComponent.load().
  • System rows (isSystem === true) render with the edit/delete actions still visible, but the abbreviation field is locked when editing, and the delete confirmation modal hides the "OK" button (see "Delete behavior" below).

Create

  1. Click cat-addcat-form-modal opens in create mode.
  2. Fill name, abbreviation, description. Optionally pick an icon file (preview appears immediately).
  3. Click cf-ok. The component calls AdminService.createCategory({...}), then — if an icon file was selected — uploadCategoryIcon(id, file) and finally updateCategory(id, { iconPath: publicUrl }) to persist the icon URL.
  4. On success the modal closes and the list refreshes.

Edit

  1. Click cat-edit-{ABBR} → modal opens in edit mode, pre-filled with the row's values.
  2. For system rows, the abbreviation field is readonly.
  3. On save the component issues updateCategory(id, {...}). If an icon file is selected, the new icon is uploaded first and the returned publicUrl replaces iconPath in the same update.

Delete behavior

  • System rows: the delete modal renders "This is a system category and cannot be deleted." and the OK button is hidden. canConfirm() returns false. If somehow the request is dispatched, the backend returns 403 SYSTEM_PROTECTED and the UI shows System categories cannot be deleted..
  • Rows with attached challenges: the backend returns 409 CATEGORY_HAS_CHALLENGES with { count } in details. The UI maps this to Cannot delete: category has N challenge(s) attached. and renders it in cat-delete-error.
  • Normal row: the modal confirms the name + abbreviation, OK dispatches DELETE /api/v1/admin/categories/:id, and the list refreshes.

Validation and error codes (server)

Code HTTP Triggered by
VALIDATION_FAILED 400 CreateCategorySchema / UpdateCategorySchema fails (length, required).
NOT_FOUND 404 PUT/DELETE on a non-existent id.
CONFLICT 409 Duplicate abbreviation on create or on update (user row).
SYSTEM_PROTECTED 409 Update attempts to change a system row's abbreviation.
SYSTEM_PROTECTED 403 Delete on a system row.
CATEGORY_HAS_CHALLENGES 409 Delete on a category with at least one attached challenge (carries { count } in details).

Architecture map

Step Where What happens
1 frontend/src/app/app.routes.ts /admin/categories lazy-loads AdminCategoriesComponent.
2 frontend/src/app/features/admin/categories/categories.component.ts ngOnInit calls AdminService.listCategories().
3 frontend/src/app/core/services/admin.service.ts listCategories, createCategory, updateCategory, deleteCategory, uploadCategoryIcon.
4 frontend/src/app/features/admin/categories/category-form-modal.component.ts Holds the form state, emits CategoryFormSubmit.
5 frontend/src/app/features/admin/categories/category-delete-modal.component.ts Maps error codes to user-friendly messages.
6 backend/src/modules/admin/admin-categories.controller.ts AdminGuard + @Roles('admin') on every handler.
7 backend/src/modules/admin/categories.service.ts list (sorted by LOWER(abbreviation)), create (uppercase + dup-check), update (system-abbr-immutable), remove (system-protected, challenge-count check).
8 backend/src/modules/admin/dto/categories.dto.ts zod schemas with length constraints; param validator for :id.
9 backend/src/modules/uploads/uploads.controller.ts POST /api/v1/uploads/category-icon (multipart) — also admin-only.

Notes

  • Abbreviations are uppercased server-side before persistence and uniqueness check (DB enforces uniqueness via the uq_category_abbreviation index — see Challenge Tables).
  • System rows are seeded by the UpdateSystemCategoryKeys1700000000300 migration and identified by a non-null system_key column.
  • The icon upload pipeline normalizes the image to a fixed size; the returned publicUrl is stored in category.icon_path.
  • The Categories component is embedded inside the General settings page, so changes to a category are visible on either route without an extra refresh.

See also