7.3 KiB
7.3 KiB
Implementation Plan: Admin Area General Settings and Categories 1.16
1. Architectural Reconnaissance
- Codebase style & conventions: TypeScript monorepo with a NestJS 10 REST backend and Angular 17 standalone components. Backend controllers currently delegate category persistence to TypeORM-backed services and validate JSON DTOs with Zod pipes; uploads use Nest
FileInterceptor, in-memory Multer buffers, and Sharp. Frontend admin pages use standalone OnPush components, signals, typed reactive forms, and anAdminServicewrapper around typedHttpClientcalls converted to promises withfirstValueFrom. - Data Layer: SQLite through TypeORM. Category abbreviations are stored uniquely, are normalized to uppercase by
AdminCategoriesService, and list queries order byLOWER(abbreviation). No schema migration is needed becauseUpdateCategorySchema, the entity, and service update path already support abbreviation changes for non-system categories. - Test Framework & Structure: Jest 29 with
ts-jest, split into root-level dedicatedtests/backendandtests/frontendprojects and runnable together with rootnpm test. Backend endpoint tests use Nest's testing module, Supertest, an in-memory SQLite database, and an isolated upload directory; frontend tests favor exported pure component helpers rather than browser or visual checks. - Required Tools & Dependencies: No new system tools, global CLIs, or package dependencies are required. Sharp is already installed in both the backend and root test dependencies and can validate/decode and normalize uploaded images. Existing
setup.shalready creates persistent/data/hipctf/uploads, installs dependencies, rebuilds the native SQLite binding, and builds both workspaces, so no setup change is required.
2. Impacted Files
- To Modify:
frontend/src/app/features/admin/categories/categories.component.ts— include the submitted abbreviation in user-category update requests, preserve the modal on failure, and route save/upload errors to form-modal state rather than delete-modal state.frontend/src/app/features/admin/categories/category-form-modal.component.ts— accept and render a clear save/upload error and accurately expose submission state while the parent performs the async operation.backend/src/modules/uploads/uploads.controller.ts— reject undecodable/non-image category-icon buffers before any filesystem write instead of falling back to raw bytes.tests/frontend/admin-categories-form-modal.spec.ts— add minimal logic assertions for user-category abbreviation editability/submission and form error-state contract as appropriate to the extracted helpers.tests/backend/uploads.spec.ts— replace the legacy text-file success expectation with a real valid image success case and add focused corrupt/non-image rejection checks that assert no overwrite or new file occurs.
- To Create: None.
3. Proposed Changes
- Database / Schema Migration: No database or migration changes. Retain the existing
UpdateCategorySchema.abbreviationfield andAdminCategoriesService.update()behavior, which uppercases non-system abbreviations, rejects duplicates, and protects system abbreviations. After a successful client save, the existing reload and defensive sort will move the renamed row into alphabetical order. - Backend Logic & APIs:
- In the existing
POST /api/v1/uploads/category-iconflow, treat Sharp decode/resize failure as aBadRequestExceptionwith a clear image-validation message; do not use multipart MIME type, filename extension, or browseracceptmetadata as proof of validity. - Decode and normalize the in-memory buffer completely to a 128x128 PNG before creating directories or calling
writeFileSync. Only after Sharp succeeds should the deterministic{categoryId}.pngpath be overwritten. This preserves any previously valid icon when a replacement is corrupt and prevents partial/new files for invalid uploads. - Return the existing successful response contract (
publicUrl,width: 128,height: 128,mimeType: image/png, plus current metadata fields) soAdminService.uploadCategoryIcon()remains compatible. Keep the existing size and missing-file checks unchanged. - Keep category persistence separate from upload persistence: on edit, the frontend must not dispatch the category update after upload rejection, so
updatedAtand other fields remain unchanged. On create, retain the current create-then-upload architecture; if upload fails, no image or icon-path update is created, although the category row already created by the existing API remains. Do not introduce a database migration or new multipart category-creation endpoint outside this job's reported edit/upload defects.
- In the existing
- Frontend UI Integration:
- Extend the edit branch of
AdminCategoriesComponent.onFormSubmit()to passabbreviation: payload.abbreviationalongside name, description, and icon path. The modal already uppercases the emitted value, while the server remains the canonical normalizer and protects system rows. - Preserve the current
syncCategoryForm()contract: user rows remain editable (readonly=false) and system rows remain locked (readonly=true). Do not mark user-category abbreviation readonly, because the required behavior is to persist it rather than silently discard it. - Add dedicated category-form error state in the container, clear it when opening/closing or beginning a submission, and bind it into the form modal. On upload or update failure, keep the edit modal open, show the server's clear error, and leave the existing list/icon unchanged; do not reuse
deleteError, which is only rendered by the delete modal. - Coordinate a parent-managed saving flag with the modal so repeated submissions are disabled during upload/update and reset in
finally. The modal should render the bound error using a stable test id and continue to emit a typedCategoryFormSubmit; no HTTP calls move into the presentational component.
- Extend the edit branch of
4. Test Strategy
- Target Unit Test File:
tests/backend/uploads.spec.ts: use Sharp to generate one tiny valid image and verify category-icon normalization succeeds; upload a plain-text payload and a corrupt PNG using a deterministiccategoryId, expect HTTP 400 with the validation message, and assert the target icon is absent or that pre-existing valid bytes are unchanged.tests/frontend/admin-categories-form-modal.spec.ts: retain pure, CLI-only tests and add only the core contract checks needed to prove a non-system edit stays writable and produces/forwards an uppercase abbreviation, plus that a supplied save error is exposed without closing/resetting the form. If submission mapping is not currently isolatable, extract a small typed pure helper from the modal/container rather than building a large TestBed environment.
- Mocking Strategy: Backend endpoint tests use the real Nest route, Sharp, and filesystem boundary in the suite's isolated temporary upload directory, with generated buffers and explicit cleanup; they do not mock image decoding or require
/data. Frontend tests mock no browser UI and exercise exported pure helpers/typed payload mapping directly. Run all tests from/repowithnpm test; then run the existing workspace builds (npm --workspace frontend run buildandnpm --workspace backend run build) as type/build verification. No visual confirmation or external services are required.