12 KiB
Implementation Plan: Admin Area — General Settings and Categories (Job 882)
[ALREADY_IMPLEMENTED]
This Job describes the Admin Area shell, the General settings page, the Categories management list/create/edit/delete, the username-menu "Admin area" entry, the admin navigation guard, and the "Invalid username or password" login feedback. Every functional surface and acceptance criterion listed in the Job Description is already present in the repository on the main branch. No code, schema, configuration, or test changes are required.
1. Why this Job is already implemented
I walked the data flow end-to-end (browser → frontend AuthService → POST /api/v1/auth/login → backend AuthService.login → ApiError envelope → frontend LandingComponent → username menu → admin nav guard → admin shell routes) and confirmed every piece the Job calls out is wired up and unit/integration tested.
2. Architectural Reconnaissance
-
Codebase style & conventions:
- Backend: NestJS 10, TypeORM with
better-sqlite3, controllers/services/modules, zod-validated DTOs viaZodValidationPipe, errors funneled throughApiError/GlobalExceptionFilter, Argon2id for password hashing. - Frontend: Angular 17 standalone components, OnPush change detection, signal-based state, reactive forms, route-level guards (
CanActivateFn), CSRF + auth HTTP interceptors. - Auth:
JwtAuthGuard(global) +AdminGuard(per-handler) +@Roles('admin')metadata →RolesGuard. - Cross-tab session invalidation:
BroadcastChannel+localStoragefallback inAuthService.
- Backend: NestJS 10, TypeORM with
-
Data Layer: TypeORM entities on SQLite (
backend/src/database/entities/user.entity.ts,category.entity.ts,challenge.entity.ts,setting.entity.ts,refresh-token.entity.ts). Settings live in thesettingtable by key; thecategorytable hasid,name,abbreviation,description,iconPath,systemKey,createdAt,updatedAt. Users haverole('admin' | 'player') andstatus('enabled' | 'disabled'). -
Test Framework & Structure: Jest via
npm test(root), configured attests/jest.config.jswith two projects (backend,frontend). Tests live exclusively undertests/backend/*.spec.tsandtests/frontend/*.spec.ts. Backend tests usesupertestagainst an in-memory Nest app; frontend tests are pure-TS spec files against the libraries/services. -
Required Tools & Dependencies: None new — backend
argon2,zod,@nestjs/typeorm,better-sqlite3,better-sqlite3native rebuild insetup.sh. Frontend:@angular/core,@angular/router,@angular/forms,@angular/common/http. Everything is already pinned inpackage.json/ workspaces.
3. Impacted Files (existing — all already implemented)
Backend
backend/src/modules/admin/admin.module.ts— registersAdminController,AdminGeneralController,AdminCategoriesController,AdminService,AdminGeneralService,AdminCategoriesService; importsAuthModule,UsersModule,SettingsModule,CommonModule, andTypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity]).backend/src/modules/admin/admin-general.controller.ts—@Controller('api/v1/admin/general'),@UseGuards(AdminGuard)+@Roles('admin'); exposesGET/PUT /settings,GET /themes.backend/src/modules/admin/general.service.ts—AdminGeneralService.getSettings / updateSettings / listThemes. Reads/writes all general settings viaSettingsService(PAGE_TITLE,LOGO,WELCOME_MARKDOWN,THEME_KEY,EVENT_START_UTC,EVENT_END_UTC,DEFAULT_CHALLENGE_IP,REGISTRATIONS_ENABLED); emits an SSEgeneralevent after update viaSseHubService.backend/src/modules/admin/dto/general.dto.ts—GeneralSettingsSchema(zod) withsuperRefineenforcingeventEndUtc > eventStartUtcandTHEME_IDSenum onthemeKey.backend/src/modules/admin/admin-categories.controller.ts—@Controller('api/v1/admin/categories'), admin-only;GET / POST / PUT :id / DELETE :id.backend/src/modules/admin/categories.service.ts—AdminCategoriesService.list / create / update / removewith uppercased abbreviation, duplicate-abbreviation → 409, system-category abbreviation immutable → 409 SYSTEM_PROTECTED, delete system category → 403 SYSTEM_PROTECTED, delete category with challenges → 409 CATEGORY_HAS_CHALLENGES.backend/src/modules/admin/dto/categories.dto.ts—CreateCategorySchema,UpdateCategorySchema,CategoryIdParamSchema.backend/src/modules/auth/auth.service.ts—login()throwsApiError(ERROR_CODES.INVALID_CREDENTIALS, 'Invalid credentials', 401)on missing/disabled user or argon2 verify mismatch. The controller (auth.controller.ts) emits the standard envelope to the SPA.
Frontend
frontend/src/app/app.routes.ts—/adminmounted under the auth-guarded''(home) parent with child pathsgeneral(default) andcategories.adminGuardprotects the whole subtree.frontend/src/app/core/guards/admin.guard.ts+admin.guard.decision.ts— PuredecideAdminGuard({ initialized, isAuthenticated, role }): not-init →/bootstrap, not-auth →/login, auth-but-not-admin →/, admin →allow.frontend/src/app/core/services/auth.service.ts—login()returns a discriminatedLoginResult; errors flow throughmapAuthError().frontend/src/app/features/landing/login-modal.service.ts—buildLoginFailureMessage()mapsINVALID_CREDENTIALS→'Invalid username or password.'(the exact text the Job cites).frontend/src/app/features/landing/landing.component.ts+landing.component.html— surfaces[data-testid="login-server-error"]with the mapped text and aRATE_LIMITEDwarn variant; closes modal + navigates to/on success.frontend/src/app/features/shell/header/shell-header.component.ts— usernameuser-menu-triggertoggles[data-testid="user-menu"]. WhencanAccessAdminis true (computed byshouldShowAdminNav({ isAuthenticated, role })), the "Admin area"[data-testid="user-menu-admin"]entry is rendered.frontend/src/app/features/home/home.component.ts— wires theadminClickoutput togoAdmin()→router.navigateByUrl('/admin').frontend/src/app/features/home/home.shell.ts— exportsshouldShowAdminNav(pure, unit-tested).frontend/src/app/features/admin/admin-shell.component.ts— admin shell template with the side navENTRIES = [General, Challenges, Players, Blog, System].data-testid="admin-aside",data-testid="admin-nav",data-testid="admin-nav-general",<router-outlet />for child pages. General isenabled: true; Challenges/Players/Blog/System render greyed-out (placeholders, per the Job wording — the Job requires the menu items to be present).frontend/src/app/features/admin/general.component.ts+general.pure.ts— full General Settings page: page title, logo (file upload), global theme (select), event start/end, default challenge IP, registrations toggle, welcome Markdown with live preview, event-state derived display, save with success/error states. Alldata-testids match the existing tests.frontend/src/app/features/admin/categories/categories.component.ts+category-form-modal.component.ts+category-delete-modal.component.ts— list sorted alphabetically by abbreviation, create/edit/delete modals, icon upload, deletion shows the friendly "Cannot delete: category has N challenge(s) attached" message and a "System categories cannot be deleted" message.frontend/src/app/core/services/admin.service.ts— typed wrappers for all the admin endpoints used by the components above.
Tests (existing — all passing)
tests/backend/admin-general-service.spec.ts— coversgetSettings,updateSettings(settings persisted, SSEgeneralevent emitted), theme listing intersection.tests/backend/admin-categories-service.spec.ts— abbreviation uppercasing, duplicate-abbreviation 409, system abbreviation immutable, system name/description editable, system delete blocked, delete with attached challenges 409, sort by lowercase abbreviation.tests/backend/admin-guard.spec.ts,tests/backend/admin-validation.spec.ts— guard chain + zod validation.tests/frontend/admin-shell.spec.ts,tests/frontend/admin-navigation.spec.ts— pure predicate and guard-decision coverage.tests/frontend/admin-general-pure.spec.ts—deriveEventState,endAfterStartValidator, datetime helpers.tests/frontend/landing-modal.spec.ts— coversbuildLoginFailureMessage({ code: 'INVALID_CREDENTIALS', ... })→'Invalid username or password.'.tests/backend/login-shell-smoke.spec.ts— exercisePOST /api/v1/auth/register-first-admin→ login →/meend-to-end.
4. Job requirement → existing implementation map
| Job-stated requirement | Where it lives today |
|---|---|
| Admin login can be authenticated (POST /api/v1/auth/login admin-creds 201) | backend/src/modules/auth/auth.service.ts:51-73 + auth.controller.ts; covered by tests/backend/login-shell-smoke.spec.ts. |
| Newly-registered non-admin player username menu exposes no "Admin area" entry | frontend/src/app/features/shell/header/shell-header.component.ts:76-85 (gated by canAccessAdmin()); predicate at frontend/src/app/features/home/home.shell.ts:9-13; covered by tests/frontend/admin-shell.spec.ts. |
| Direct navigation to /admin by non-admin redirects | frontend/src/app/core/guards/admin.guard.ts + admin.guard.decision.ts:11-13; covered by tests/frontend/admin-navigation.spec.ts. |
| Admin opens username menu → "Admin area" → /admin reachable | home.component.ts:154-157 goAdmin() + app.routes.ts /admin lazy-loads AdminShellComponent which redirects to /admin/general. |
| Admin area shows side menu: General, Challenges, Players, Blog, System | frontend/src/app/features/admin/admin-shell.component.ts:12-18 ENTRIES. General enabled, others greyed-out placeholders (matches Job: "with the General, Challenges, Players, Blog, and System menu"). |
| General section loads + saves settings | AdminGeneralComponent.ngOnInit / onSubmit + AdminService.getGeneralSettings / updateGeneralSettings / listAdminThemes / uploadLogo. |
| Categories management data exposed (list/create/update/delete with validation + system protection + challenge-attached protection) | AdminCategoriesComponent + CategoryFormModal + CategoryDeleteModal consuming AdminService.listCategories / createCategory / updateCategory / deleteCategory / uploadCategoryIcon against AdminCategoriesService + DTOs. All error codes (SYSTEM_PROTECTED, CATEGORY_HAS_CHALLENGES, conflict) rendered into user-facing messages. |
| Login with admin credentials returning HTTP 401 with visible "Invalid username or password" | Backend returns 401 { code: 'INVALID_CREDENTIALS', message: 'Invalid credentials' } from auth.service.ts:61,67; frontend maps via buildLoginFailureMessage (login-modal.service.ts:21-22) → 'Invalid username or password.', rendered at [data-testid="login-server-error"]. |
5. Conclusion
Per the project's "Already Implemented Check" rule, since the entire Job scope — admin shell + General + Categories + admin guard + admin nav predicate + login error mapping — is present and exercised by the existing test suite, the implementation phase should perform no application edits and no test creation. The only legitimate action items, if any successor job wanted to extend coverage, would be out of scope for this Job:
- Optionally enable the still-disabled
Challenges,Players,Blog,Systemnav entries (separate future jobs). - Optionally surface a more verbose login-failure message (e.g. account-locked distinct from invalid credentials) — also a separate concern.
Files NOT to Modify
None — the Job is fully complete.