AI Implementation feature(899): Data and run #39
@@ -6,6 +6,7 @@ dist/
|
||||
!.env.example
|
||||
.DS_Store
|
||||
coverage/
|
||||
data/
|
||||
backend/data/
|
||||
backend/themes/.cache/
|
||||
frontend/.angular/
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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<string>('UPLOAD_DIR', '/data/hipctf/uploads'));
|
||||
const dest = opts.destination ?? path.resolve(config.get<string>('UPLOAD_DIR', './data/uploads'));
|
||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
||||
return multer({
|
||||
storage: multer.diskStorage({
|
||||
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -113,6 +113,6 @@ export class DatabaseInitService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
getDbPath(): string {
|
||||
return this.config.get<string>('DATABASE_PATH', '/data/hipctf/db.sqlite');
|
||||
return this.config.get<string>('DATABASE_PATH', './data/db.sqlite');
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ const MIGRATIONS = [
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => {
|
||||
const dbPath = config.get<string>('DATABASE_PATH', '/data/hipctf/db.sqlite');
|
||||
const dbPath = config.get<string>('DATABASE_PATH', './data/db.sqlite');
|
||||
const isMemory = dbPath === ':memory:' || dbPath.startsWith('file:');
|
||||
if (!isMemory) {
|
||||
const dir = path.dirname(dbPath);
|
||||
|
||||
@@ -9,7 +9,7 @@ export class UploadsStaticMiddleware implements NestMiddleware {
|
||||
private rootDir = '/tmp';
|
||||
|
||||
constructor(config: ConfigService) {
|
||||
const dir = path.resolve(config.get<string>('UPLOAD_DIR', '/data/hipctf/uploads'));
|
||||
const dir = path.resolve(config.get<string>('UPLOAD_DIR', './data/uploads'));
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
this.rootDir = dir;
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ async function bootstrap(): Promise<void> {
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const bodyLimit = config.get<string>('BODY_SIZE_LIMIT', '1mb');
|
||||
const uploadDir = path.resolve(config.get<string>('UPLOAD_DIR', '/data/hipctf/uploads'));
|
||||
const uploadDir = path.resolve(config.get<string>('UPLOAD_DIR', './data/uploads'));
|
||||
const tlsEnabled = config.get<boolean>('TLS_ENABLED', false);
|
||||
const frontendDist = path.resolve(config.get<string>('FRONTEND_DIST', '../frontend/dist'));
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export class UploadsController {
|
||||
private readonly globalLimit: number;
|
||||
|
||||
constructor(private readonly config: ConfigService) {
|
||||
this.uploadDir = path.resolve(this.config.get<string>('UPLOAD_DIR', '/data/hipctf/uploads'));
|
||||
this.uploadDir = path.resolve(this.config.get<string>('UPLOAD_DIR', './data/uploads'));
|
||||
this.globalLimit = parseUploadSizeLimit(this.config.get<string>('UPLOAD_SIZE_LIMIT', '50mb'));
|
||||
}
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
+4
-1
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Generated
+55
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
echo "[setup] Done. Start with: npm start (dev mode: npm run dev)"
|
||||
@@ -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);
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user