From 1a40e5708a60b7270148f935a31f196713193cea Mon Sep 17 00:00:00 2001 From: m0rph3us1987 Date: Wed, 22 Jul 2026 19:56:33 +0000 Subject: [PATCH] AI Implementation feature(899): Data and run (#39) --- .gitignore | 1 + .kilo/plans/898.md | 35 ------ .kilo/plans/899.md | 43 +++++++ backend/src/common/utils/upload.ts | 2 +- backend/src/config/env.schema.ts | 4 +- backend/src/database/database-init.service.ts | 2 +- backend/src/database/database.module.ts | 2 +- .../src/frontend/uploads-static.middleware.ts | 2 +- backend/src/main.ts | 2 +- .../src/modules/uploads/uploads.controller.ts | 2 +- docs/architecture/key-files.md | 5 + docs/architecture/overview.md | 6 +- docs/database/category-repair-migration.md | 3 +- docs/database/schema.md | 5 +- docs/guides/dev-workflow.md | 119 ++++++++++++++++++ docs/index.md | 5 +- frontend/angular.json | 3 + frontend/proxy.conf.json | 14 +++ package-lock.json | 55 ++++++++ package.json | 4 + setup.sh | 6 +- tests/backend/env-schema.spec.ts | 12 ++ tests/backend/root-dev-script.spec.ts | 26 ++++ 23 files changed, 305 insertions(+), 53 deletions(-) delete mode 100644 .kilo/plans/898.md create mode 100644 .kilo/plans/899.md create mode 100644 docs/guides/dev-workflow.md create mode 100644 frontend/proxy.conf.json create mode 100644 tests/backend/root-dev-script.spec.ts diff --git a/.gitignore b/.gitignore index ab3e92e..cd34769 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist/ !.env.example .DS_Store coverage/ +data/ backend/data/ backend/themes/.cache/ frontend/.angular/ diff --git a/.kilo/plans/898.md b/.kilo/plans/898.md deleted file mode 100644 index 7731dd3..0000000 --- a/.kilo/plans/898.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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 an `AdminService` wrapper around typed `HttpClient` calls converted to promises with `firstValueFrom`. -- **Data Layer:** SQLite through TypeORM. Category abbreviations are stored uniquely, are normalized to uppercase by `AdminCategoriesService`, and list queries order by `LOWER(abbreviation)`. No schema migration is needed because `UpdateCategorySchema`, 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 dedicated `tests/backend` and `tests/frontend` projects and runnable together with root `npm 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.sh` already 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 -1. **Database / Schema Migration:** No database or migration changes. Retain the existing `UpdateCategorySchema.abbreviation` field and `AdminCategoriesService.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. -2. **Backend Logic & APIs:** - - In the existing `POST /api/v1/uploads/category-icon` flow, treat Sharp decode/resize failure as a `BadRequestException` with a clear image-validation message; do not use multipart MIME type, filename extension, or browser `accept` metadata 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}.png` path 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) so `AdminService.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 `updatedAt` and 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. -3. **Frontend UI Integration:** - - Extend the edit branch of `AdminCategoriesComponent.onFormSubmit()` to pass `abbreviation: payload.abbreviation` alongside 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 typed `CategoryFormSubmit`; no HTTP calls move into the presentational component. - -## 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 deterministic `categoryId`, 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 `/repo` with `npm test`; then run the existing workspace builds (`npm --workspace frontend run build` and `npm --workspace backend run build`) as type/build verification. No visual confirmation or external services are required. diff --git a/.kilo/plans/899.md b/.kilo/plans/899.md new file mode 100644 index 0000000..91bc799 --- /dev/null +++ b/.kilo/plans/899.md @@ -0,0 +1,43 @@ +# Implementation Plan: Data and run + +## 1. Architectural Reconnaissance +- **Codebase style & conventions:** npm-workspace TypeScript monorepo with a NestJS 10 backend and Angular 17 standalone frontend. Runtime configuration is validated centrally with Zod in `backend/src/config/env.schema.ts` and consumed through Nest `ConfigService`; filesystem paths are normalized with Node `path.resolve`, and root scripts delegate to workspace scripts. The feature is not already implemented: persistent defaults still point at `/data/hipctf`, and the root package has no `dev` script. +- **Data Layer:** TypeORM with `better-sqlite3`. `DatabaseModule` creates the database parent directory before connecting, `DatabaseInitService` runs migrations and seeds, and uploads are written and served from the configured upload directory. No schema migration is required; this job changes only default filesystem locations and development process orchestration. +- **Test Framework & Structure:** Jest 29 with `ts-jest`, configured in `tests/jest.config.js` as backend and frontend projects. Tests live under dedicated `tests/backend` and `tests/frontend` folders and all run from the repository root with `npm test`. Add only focused configuration/script contract tests; no browser, UI, database file, port, or long-running process is required. +- **Required Tools & Dependencies:** Existing Node.js/npm workspace tooling, Angular CLI, `ts-node`, TypeScript, Jest, and `better-sqlite3` remain required. Add the root development dependency `concurrently` (and update `package-lock.json`) so one root command can supervise both long-running workspaces and propagate shutdown/failure. `setup.sh` already installs root/workspace dependencies via `npm ci`; replace its obsolete absolute `/data/hipctf/uploads` preparation with project-local `./data/uploads` preparation. No global CLI or new system package is required. + +## 2. Impacted Files +- **To Modify:** + - `backend/src/config/env.schema.ts` — change validated `DATABASE_PATH` and `UPLOAD_DIR` defaults to `./data/db.sqlite` and `./data/uploads`. + - `backend/src/database/database.module.ts` — align the `ConfigService.get` database fallback with the canonical project-local database default. + - `backend/src/database/database-init.service.ts` — align `getDbPath()` fallback with the canonical project-local database default. + - `backend/src/main.ts` — align startup upload-directory fallback with the canonical project-local upload default. + - `backend/src/modules/uploads/uploads.controller.ts` — align upload controller fallback with the project-local upload default. + - `backend/src/common/utils/upload.ts` — align Multer destination fallback with the project-local upload default. + - `backend/src/frontend/uploads-static.middleware.ts` — align static-upload middleware fallback with the project-local upload default. + - `package.json` — add root `dev` orchestration for backend and frontend and retain `start:dev` as a compatible alias if desired. + - `package-lock.json` — lock the new process-runner dependency and root package metadata. + - `setup.sh` — create `./data/uploads` relative to the repository root instead of `/data/hipctf/uploads`, while retaining dependency installation and builds. + - `.gitignore` — ignore the root runtime `data/` directory rather than the unrelated `backend/data/` path, preventing local database/uploads from entering source control. + - `tests/backend/env-schema.spec.ts` — assert the exact project-local data defaults returned by the Zod schema. +- **To Create:** + - `tests/backend/root-dev-script.spec.ts` — focused static contract test for root development orchestration and both workspace targets. + +## 3. Proposed Changes +1. **Database / Schema Migration:** + - Do not add a TypeORM migration or alter tables. Update the canonical default database location from `/data/hipctf/db.sqlite` to `./data/db.sqlite`. + - Replace every hard-coded database fallback outside the schema with the same value so direct/unit construction cannot silently revert to `/data` when configuration is absent. + - Preserve environment overrides such as `DATABASE_PATH=:memory:` in tests and custom deployment paths; only defaults change. +2. **Backend Logic & APIs:** + - Change the canonical default upload location from `/data/hipctf/uploads` to `./data/uploads` in the environment schema and every defensive `ConfigService.get` fallback used by startup, upload handling, Multer storage, and static serving. + - Continue resolving relative paths through `path.resolve`; when launched through the root scripts, `./data` resolves from the project root. Preserve recursive directory creation in `DatabaseModule`, `main.ts`, upload helpers, and middleware. + - Update `setup.sh` to derive/use the repository-local data directory and create `data/uploads`, avoiding writes to the container’s shared `/data` volume for normal application defaults. Keep setup idempotent. + - Update `.gitignore` for `/data/` so generated SQLite and uploaded files remain local runtime artifacts. + - Add `concurrently` as a root development dependency and define `npm run dev` to launch `npm --workspace backend run start:dev` and `npm --workspace frontend run start` in parallel, with clear process names and kill-on-peer-failure/shutdown behavior. Point the existing root `start:dev` script at `npm run dev` to avoid two conflicting development entry points. +3. **Frontend UI Integration:** + - No Angular source or API URL changes are needed. The Angular dev server remains on port 4200 while the Nest API remains on port 3000; both become available from one root command. + - Because the frontend uses relative `/api` and `/uploads` URLs and no Angular dev-server proxy is currently configured, add a development proxy configuration (and reference it from Angular’s `serve` target) routing `/api` and `/uploads` to `http://localhost:3000`. This makes the concurrently started frontend functional rather than merely starting two disconnected processes, including credentialed API calls and SSE streams. + +## 4. Test Strategy +- **Target Unit Test File:** Extend `tests/backend/env-schema.spec.ts` with one success-path assertion that empty input resolves to `DATABASE_PATH === './data/db.sqlite'` and `UPLOAD_DIR === './data/uploads'`. Add `tests/backend/root-dev-script.spec.ts` to read root `package.json` and `frontend/angular.json`, asserting that `dev` invokes both workspace development commands and the Angular development serve target references the backend proxy. Both tests execute through the existing single root `npm test` command. +- **Mocking Strategy:** No external boundaries need heavy mocks. Parse checked-in JSON directly with Node filesystem APIs; do not spawn `npm run dev`, bind ports, launch Angular/Nest, touch `/data`, or create persistent files. The environment-schema test calls the pure Zod parser. During implementation, follow Red-Green-Refactor, then verify with `npm test`, backend/frontend builds, and the repository’s available lint/typecheck commands (no lint script currently exists; the builds provide TypeScript compilation checks). diff --git a/backend/src/common/utils/upload.ts b/backend/src/common/utils/upload.ts index c620126..189361c 100644 --- a/backend/src/common/utils/upload.ts +++ b/backend/src/common/utils/upload.ts @@ -63,7 +63,7 @@ export function buildMulter(config: ConfigService, opts: MulterOptions = {}): mu const limits = opts.fileSize !== undefined ? { fileSize: opts.fileSize } : buildUploadLimits(config); - const dest = opts.destination ?? path.resolve(config.get('UPLOAD_DIR', '/data/hipctf/uploads')); + const dest = opts.destination ?? path.resolve(config.get('UPLOAD_DIR', './data/uploads')); if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); return multer({ storage: multer.diskStorage({ diff --git a/backend/src/config/env.schema.ts b/backend/src/config/env.schema.ts index e86e1e6..c901fd0 100644 --- a/backend/src/config/env.schema.ts +++ b/backend/src/config/env.schema.ts @@ -4,8 +4,8 @@ export const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.coerce.number().int().positive().default(3000), - DATABASE_PATH: z.string().min(1).default('/data/hipctf/db.sqlite'), - UPLOAD_DIR: z.string().min(1).default('/data/hipctf/uploads'), + DATABASE_PATH: z.string().min(1).default('./data/db.sqlite'), + UPLOAD_DIR: z.string().min(1).default('./data/uploads'), THEMES_DIR: z.string().min(1).default('./themes'), FRONTEND_DIST: z.string().min(1).default('../frontend/dist'), diff --git a/backend/src/database/database-init.service.ts b/backend/src/database/database-init.service.ts index d36a88e..14d0f92 100644 --- a/backend/src/database/database-init.service.ts +++ b/backend/src/database/database-init.service.ts @@ -113,6 +113,6 @@ export class DatabaseInitService implements OnApplicationBootstrap { } getDbPath(): string { - return this.config.get('DATABASE_PATH', '/data/hipctf/db.sqlite'); + return this.config.get('DATABASE_PATH', './data/db.sqlite'); } } \ No newline at end of file diff --git a/backend/src/database/database.module.ts b/backend/src/database/database.module.ts index fe24417..450e29d 100644 --- a/backend/src/database/database.module.ts +++ b/backend/src/database/database.module.ts @@ -44,7 +44,7 @@ const MIGRATIONS = [ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => { - const dbPath = config.get('DATABASE_PATH', '/data/hipctf/db.sqlite'); + const dbPath = config.get('DATABASE_PATH', './data/db.sqlite'); const isMemory = dbPath === ':memory:' || dbPath.startsWith('file:'); if (!isMemory) { const dir = path.dirname(dbPath); diff --git a/backend/src/frontend/uploads-static.middleware.ts b/backend/src/frontend/uploads-static.middleware.ts index 68ab3c6..3e3eb9b 100644 --- a/backend/src/frontend/uploads-static.middleware.ts +++ b/backend/src/frontend/uploads-static.middleware.ts @@ -9,7 +9,7 @@ export class UploadsStaticMiddleware implements NestMiddleware { private rootDir = '/tmp'; constructor(config: ConfigService) { - const dir = path.resolve(config.get('UPLOAD_DIR', '/data/hipctf/uploads')); + const dir = path.resolve(config.get('UPLOAD_DIR', './data/uploads')); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); this.rootDir = dir; } diff --git a/backend/src/main.ts b/backend/src/main.ts index ab60c3c..e65970a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -28,7 +28,7 @@ async function bootstrap(): Promise { .map((s) => s.trim()) .filter(Boolean); const bodyLimit = config.get('BODY_SIZE_LIMIT', '1mb'); - const uploadDir = path.resolve(config.get('UPLOAD_DIR', '/data/hipctf/uploads')); + const uploadDir = path.resolve(config.get('UPLOAD_DIR', './data/uploads')); const tlsEnabled = config.get('TLS_ENABLED', false); const frontendDist = path.resolve(config.get('FRONTEND_DIST', '../frontend/dist')); diff --git a/backend/src/modules/uploads/uploads.controller.ts b/backend/src/modules/uploads/uploads.controller.ts index 2f2532f..d170000 100644 --- a/backend/src/modules/uploads/uploads.controller.ts +++ b/backend/src/modules/uploads/uploads.controller.ts @@ -19,7 +19,7 @@ export class UploadsController { private readonly globalLimit: number; constructor(private readonly config: ConfigService) { - this.uploadDir = path.resolve(this.config.get('UPLOAD_DIR', '/data/hipctf/uploads')); + this.uploadDir = path.resolve(this.config.get('UPLOAD_DIR', './data/uploads')); this.globalLimit = parseUploadSizeLimit(this.config.get('UPLOAD_SIZE_LIMIT', '50mb')); } diff --git a/docs/architecture/key-files.md b/docs/architecture/key-files.md index d7e3a95..e7e8839 100644 --- a/docs/architecture/key-files.md +++ b/docs/architecture/key-files.md @@ -12,6 +12,7 @@ timestamp: 2026-07-22T19:18:00Z |---|---| | `backend/src/main.ts` | Bootstraps Nest, middleware, OpenAPI, static assets, and SPA fallback. | | `backend/src/app.module.ts` | Wires feature modules and global providers. | +| `backend/src/config/env.schema.ts` | Zod-validated runtime config; canonical defaults for `DATABASE_PATH` (`./data/db.sqlite`) and `UPLOAD_DIR` (`./data/uploads`). | | `backend/src/database/database.module.ts` | Configures TypeORM with SQLite. | | `backend/src/database/database-init.service.ts` | Initializes the database and runs migrations; `verifySeed()` also asserts the canonical six system-category keys and reports missing/duplicate rows. | | `backend/src/database/migrations/1700000000400-RepairCategorySchemaAndSystemCategories.ts` | Forward-only repair migration: adds `created_at`/`updated_at` to legacy six-column `category` tables, deletes obsolete `FOR`/`OSI` system rows, rewrites canonical row metadata, inserts any missing canonical key, and re-asserts unique indexes. Exports `CANONICAL_SYSTEM_CATEGORIES`. | @@ -35,6 +36,10 @@ timestamp: 2026-07-22T19:18:00Z | File | Responsibility | |---|---| | `frontend/src/main.ts` | Bootstraps Angular and registers HTTP interceptors. | +| `frontend/proxy.conf.json` | Angular dev-server proxy: forwards `/api` and `/uploads` to `http://localhost:3000`. | +| `frontend/angular.json` | Angular workspace config; the `serve` target references `proxy.conf.json`. | +| `package.json` (root) | Root npm scripts; `dev` runs both workspaces via `concurrently`, `dev:backend` / `dev:frontend` run one at a time. | +| `setup.sh` | Idempotent setup: installs root + workspace deps, builds both packages, creates `./data/uploads`. | | `frontend/src/app/app.routes.ts` | Defines public, shell, child, and admin routes. | | `frontend/src/app/core/services/bootstrap.service.ts` | Caches and exposes bootstrap state, refreshes it after admin updates, and applies the resolved theme. | | `frontend/src/app/core/services/bootstrap.types.ts` | Defines bootstrap/theme payload types and maps theme tokens to CSS custom properties. | diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 9f5e6bc..84e2ad4 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -27,12 +27,12 @@ Browser ──HTTPS──▶ NestJS process (PORT, default 3000) ├── /api/v1/* ── controllers → services → TypeORM │ │ │ ▼ - │ better-sqlite3 - │ (DATABASE_PATH) + │ better-sqlite3 + │ (DATABASE_PATH, default ./data/db.sqlite) │ ├── /api/docs, /api/docs-json ── Swagger UI (OpenAPI 3.1) │ - ├── /uploads/* ── static file server (UPLOAD_DIR) + ├── /uploads/* ── static file server (UPLOAD_DIR, default ./data/uploads) │ └── /* ── SPA fallback (FRONTEND_DIST/index.html) ``` diff --git a/docs/database/category-repair-migration.md b/docs/database/category-repair-migration.md index 809a910..b59a825 100644 --- a/docs/database/category-repair-migration.md +++ b/docs/database/category-repair-migration.md @@ -8,7 +8,8 @@ timestamp: 2026-07-22T16:44:54Z # Purpose -Some persistent `/data/hipctf/db.sqlite` databases were created before +Some persistent HIPCTF databases (default path +[`./data/db.sqlite`](/database/schema.md)) were created before the canonical six system categories existed. They contain a legacy six-column `category` table (no `created_at` / `updated_at`) and system rows whose `system_key` / `abbreviation` no longer match the diff --git a/docs/database/schema.md b/docs/database/schema.md index d39fa50..f4667c7 100644 --- a/docs/database/schema.md +++ b/docs/database/schema.md @@ -10,8 +10,9 @@ timestamp: 2026-07-22T16:44:54Z HIPCTF uses **SQLite via better-sqlite3** with TypeORM migrations. The database is a single file (`DATABASE_PATH`, default -`/data/hipctf/db.sqlite`) running in **WAL journal mode** for better -concurrent read/write performance. +`./data/db.sqlite`, resolved relative to the process working directory) +running in **WAL journal mode** for better concurrent read/write +performance. The schema is created by a single migration (`backend/src/database/migrations/1700000000000-InitSchema.ts`) and seeded diff --git a/docs/guides/dev-workflow.md b/docs/guides/dev-workflow.md new file mode 100644 index 0000000..6162af5 --- /dev/null +++ b/docs/guides/dev-workflow.md @@ -0,0 +1,119 @@ +--- +type: guide +title: Local Development Workflow +description: How to run the backend and frontend together in development, including the root dev script, Angular dev-server proxy, and project-local data directory. +tags: [guide, dev, dev-server, proxy, concurrently, setup, tester] +timestamp: 2026-07-22T19:54:00Z +--- + +# Overview + +The HIPCTF monorepo ships a single root command — `npm run dev` — that +boots the NestJS backend and the Angular dev server in parallel, with +the Angular dev server proxying `/api` and `/uploads` calls to the +backend on port 3000. All persistent runtime files (SQLite database, +uploaded icons / attachments / logos) live under the project-local +`./data/` directory. + +# How to start the full stack (tester steps) + +1. From the repository root, install workspace dependencies once: + ``` + npm ci + ``` +2. Start both servers in parallel: + ``` + npm run dev + ``` + This uses `concurrently` to run: + * `npm --workspace backend run start:dev` (NestJS watch mode on `http://localhost:3000`) + * `npm --workspace frontend run start` (Angular dev server on `http://localhost:4200`) + Process names `backend` and `frontend` are prefixed per output line. + If either process exits with a non-zero status, the other is killed + (`--kill-others-on-fail`). +3. Open the browser: + * **Frontend (preferred for app work):** `http://localhost:4200/` + * **Backend (single-port mode, serves built SPA later):** `http://localhost:3000/` +4. To run them individually during debugging: + ``` + npm run dev:backend + npm run dev:frontend + ``` + +# Project-local data directory + +| Path | Purpose | Backed by env var | +|-------------------|-------------------------------------------------------|---------------------| +| `./data/` | Root runtime data directory (gitignored). | — | +| `./data/db.sqlite`| SQLite database file (created on first boot). | `DATABASE_PATH` | +| `./data/uploads/` | Uploaded category icons, challenge files, site logos. | `UPLOAD_DIR` | + +Both defaults are declared in `backend/src/config/env.schema.ts` and +re-declared as fallbacks in every backend code path that calls +`ConfigService.get` so direct/unit construction cannot silently revert +to the legacy absolute paths. Relative paths are resolved with +`path.resolve` against the process working directory — when launched +via the root scripts, that is the repository root. + +The legacy absolute defaults (`/data/hipctf/db.sqlite`, +`/data/hipctf/uploads`) have been removed; set `DATABASE_PATH` and +`UPLOAD_DIR` explicitly if you need to point at an external location +(e.g. a mounted volume in production). + +The data directory is created automatically: +* `DatabaseModule` ensures the parent directory of `DATABASE_PATH` + exists before connecting. +* `main.ts`, `UploadsController`, `buildMulter`, and + `UploadsStaticMiddleware` all `mkdir -p` the upload directory on + startup. +* `setup.sh` creates `./data/uploads` before any build so first-boot + Multer writes never fail. + +# Angular dev-server proxy + +`frontend/proxy.conf.json` is wired into the `serve` target in +`frontend/angular.json` so relative URLs from the SPA hit the backend +without CORS prompts: + +| Origin path | Target | Notes | +|-------------|-------------------------|--------------------------------------------------------| +| `/api` | `http://localhost:3000` | All REST controllers + SSE endpoints. `changeOrigin: true`. | +| `/uploads` | `http://localhost:3000` | Static files served by `UploadsStaticMiddleware`. | + +The proxy is **development-only.** In production (`npm start` / +`backend/dist/main.js`) the backend serves the built SPA directly and +no proxy is involved. + +# Root scripts + +| Script | What it does | +|---------------------|--------------------------------------------------------------------| +| `npm run dev` | Start backend + frontend together via `concurrently`. | +| `npm run dev:backend` | Start only the NestJS backend in watch mode. | +| `npm run dev:frontend` | Start only the Angular dev server. | +| `npm start` | Run the **production** backend (`node backend/dist/main.js`). | +| `npm run build` | Build both workspaces (frontend first, then backend). | +| `npm run start:dev` | Alias for `npm run dev` (kept for backwards compatibility). | +| `npm test` | Run the full Jest project (backend + frontend). | + +# Expected behavior (tester) + +* `npm run dev` displays interleaved backend and frontend logs, each + prefixed with the process name and a color. +* The Angular dev server prints `Application bundle generation complete` + within a few seconds and the page is reachable at + `http://localhost:4200/`. +* Visiting `http://localhost:4200/` triggers + `GET /api/v1/bootstrap` (proxied to port 3000) and shows the + appropriate public, login, or bootstrap page. +* Uploads (e.g. category icons via the admin UI) land under + `./data/uploads/` and are immediately readable from the dev frontend + via `/uploads/...` (proxied). +* Pressing `Ctrl+C` once terminates both processes cleanly. + +# See also + +- [System Overview](/architecture/overview.md) +- [Database Schema Overview](/database/schema.md) +- [Uploads Endpoints](/api/uploads.md) +- [Key Files Index](/architecture/key-files.md) diff --git a/docs/index.md b/docs/index.md index b68315a..130a4ae 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin controls. The docs below are organized by purpose so agents can pull just the slice -they need. Last regenerated 2026-07-22T19:18:00Z. +they need. Last regenerated 2026-07-22T19:54:00Z. # Architecture @@ -80,6 +80,9 @@ they need. Last regenerated 2026-07-22T19:18:00Z. * [Cross-Tab Authenticated Shell Invalidation](/guides/cross-tab-invalidation.md) - How a logout in one tab, or an unauthorized SSE response, instantly clears and redirects every other open tab of the SPA. +* [Local Development Workflow](/guides/dev-workflow.md) - How to run the + backend and frontend together (`npm run dev`), the Angular dev-server + proxy, and the project-local `./data/` directory. * [Session Restoration on Reload](/guides/session-restoration.md) - How the SPA keeps a user signed in across hard refreshes and how the guards wait for bootstrap + auth hydration before deciding where to diff --git a/frontend/angular.json b/frontend/angular.json index 7429b46..43964fa 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -39,6 +39,9 @@ }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, "configurations": { "production": { "buildTarget": "hipctf:build:production" }, "development": { "buildTarget": "hipctf:build:development" } diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 0000000..f26bfac --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1,14 @@ +{ + "/api": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true, + "ws": false + }, + "/uploads": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true, + "ws": false + } +} diff --git a/package-lock.json b/package-lock.json index 8aba315..e486338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@types/jest": "^29.5.12", "@types/jsdom": "^28.0.3", "@types/node": "^20.11.0", + "concurrently": "^9.2.4", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jsdom": "^29.1.1", @@ -7899,6 +7900,60 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", diff --git a/package.json b/package.json index b583ff8..0df63ea 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "build": "npm --workspace frontend run build && npm --workspace backend run build", "start": "node backend/dist/main.js", "start:dev": "npm --workspace backend run start:dev", + "dev": "concurrently --kill-others-on-fail --names \"backend,frontend\" --prefix-colors \"auto\" \"npm --workspace backend run start:dev\" \"npm --workspace frontend run start\"", + "dev:backend": "npm --workspace backend run start:dev", + "dev:frontend": "npm --workspace frontend run start", "test": "jest --config tests/jest.config.js", "test:backend": "jest --config tests/jest.config.js --selectProjects backend", "test:frontend": "jest --config tests/jest.config.js --selectProjects frontend", @@ -21,6 +24,7 @@ "@types/jest": "^29.5.12", "@types/jsdom": "^28.0.3", "@types/node": "^20.11.0", + "concurrently": "^9.1.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jsdom": "^29.1.1", diff --git a/setup.sh b/setup.sh index e3508cb..9a49d4a 100755 --- a/setup.sh +++ b/setup.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -echo "[setup] Ensuring /data/hipctf exists..." -mkdir -p /data/hipctf/uploads +echo "[setup] Ensuring ./data/uploads exists..." +mkdir -p ./data/uploads echo "[setup] Installing root dependencies..." npm ci --no-audit --no-fund || npm install --no-audit --no-fund @@ -16,4 +16,4 @@ npm --workspace frontend run build echo "[setup] Building NestJS backend..." npm --workspace backend run build -echo "[setup] Done. Start with: npm start" \ No newline at end of file +echo "[setup] Done. Start with: npm start (dev mode: npm run dev)" \ No newline at end of file diff --git a/tests/backend/env-schema.spec.ts b/tests/backend/env-schema.spec.ts index 53ad99d..4eca696 100644 --- a/tests/backend/env-schema.spec.ts +++ b/tests/backend/env-schema.spec.ts @@ -6,6 +6,18 @@ describe('envSchema', () => { expect(r.success).toBe(true); }); + it('defaults DATABASE_PATH to ./data/db.sqlite', () => { + const r = envSchema.safeParse({}); + expect(r.success).toBe(true); + expect((r.data as any).DATABASE_PATH).toBe('./data/db.sqlite'); + }); + + it('defaults UPLOAD_DIR to ./data/uploads', () => { + const r = envSchema.safeParse({}); + expect(r.success).toBe(true); + expect((r.data as any).UPLOAD_DIR).toBe('./data/uploads'); + }); + it('rejects invalid PORT', () => { const r = envSchema.safeParse({ PORT: 'not-a-number' }); expect(r.success).toBe(false); diff --git a/tests/backend/root-dev-script.spec.ts b/tests/backend/root-dev-script.spec.ts new file mode 100644 index 0000000..3af77f4 --- /dev/null +++ b/tests/backend/root-dev-script.spec.ts @@ -0,0 +1,26 @@ +import { readFileSync } from 'node:fs'; +import * as path from 'node:path'; + +describe('Root dev script orchestration', () => { + it('exposes a `dev` script that runs both backend and frontend workspaces', () => { + const pkg = JSON.parse( + readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'), + ); + expect(typeof pkg.scripts?.dev).toBe('string'); + expect(pkg.scripts.dev).toMatch(/workspace\s+backend/); + expect(pkg.scripts.dev).toMatch(/workspace\s+frontend/); + }); + + it('configures the Angular dev server to proxy /api and /uploads to the Nest backend', () => { + const angularJson = JSON.parse( + readFileSync( + path.join(__dirname, '..', '..', 'frontend', 'angular.json'), + 'utf8', + ), + ); + const serve = angularJson.projects?.hipctf?.architect?.serve; + expect(serve).toBeDefined(); + expect(typeof serve.options?.proxyConfig).toBe('string'); + expect(serve.options.proxyConfig).toMatch(/proxy/); + }); +});