# Compton Platform Monorepo-lite project that follows the `docs/TZ.md` specification for the Compton platform. ## Stack - **Frontend:** React 19, TypeScript, Vite, React Router, TanStack Query, Zustand, RHF + Zod, Ant Design (admin panel) - **Backend:** FastAPI, SQLAlchemy 2, Alembic, Pydantic v2 - **Data/Infra:** PostgreSQL, Redis, MinIO (S3-compatible), Docker Compose - **Quality:** Vitest, Testing Library, Playwright, pytest, coverage gates in CI ## Repository Layout - `apps/web` — frontend SPA - `apps/api` — backend API (`app/core/crypto.py` — unified crypto; `app/core/install_secrets.py` — bootstrap/lock) - `apps/api/data/secrets/` — per-install secrets (`install.env`, gitignored) - `apps/api/data/compton_settings.json` — runtime panel settings (superuser-editable via API) - `packages/shared-types` — generated API types contract target - `packages/eslint-config` — shared eslint config package - `infra` — docker/nginx/ci helper files - `docker-compose.dev-ports.yml` — optional override to expose DB/Redis/MinIO on host ## Quick Start **Full stack in Docker** (API + DB + frontend — no local `pnpm install` required): ```bash # 1. Bootstrap install secrets — REQUIRED before the first docker compose up python apps/api/scripts/bootstrap_install.py # 2. Build and start everything docker compose --profile docker-web up -d --build # 3. Verify (Windows PowerShell: use curl.exe, not curl — it is an alias for Invoke-WebRequest) curl.exe http://localhost:8000/api/v1/health ``` Open [http://localhost:5173](http://localhost:5173) — static landing (`index.html`). SPA routes (`/login`, `/admin`, `/profile`, …) are served via `app.html` fallback in Vite dev/preview. The `web` container runs Vite with hot-reload; dependencies are installed inside the container automatically. Log in as `admin@compton.example` (password `Admin1234` by default) and open `/admin`. See [Seed data](#seed-data) for all demo accounts. **Optional** — copy env files if you also run API or frontend locally (hybrid mode): ```bash cp apps/api/.env.example apps/api/.env cp apps/web/.env.example apps/web/.env ``` > **Important:** Run bootstrap **before** the first `docker compose up`. If Postgres was started without `install.env`, the API will fail with `password authentication failed for user "compton_app"`. Fix: `docker compose --profile docker-web down -v`, then bootstrap + up again. See [Troubleshooting](#troubleshooting). **Hybrid setup** (Docker API + local frontend): ```bash cp apps/api/.env.example apps/api/.env cp apps/web/.env.example apps/web/.env pnpm install python apps/api/scripts/bootstrap_install.py docker compose up -d pnpm --filter web dev ``` ## Local Setup ### Prerequisites - **Docker + Docker Compose** — enough for the full-stack Docker workflow - **Node.js 22+ and pnpm 9+** — only if you run the frontend locally (`pnpm --filter web dev`) - **Python 3.12** — bootstrap script, local API, or tests outside Docker ### Start services | Mode | Command | What runs | | ---- | ------- | --------- | | **Full Docker (recommended)** | `docker compose --profile docker-web up -d --build` | API, Postgres, Redis, MinIO, Vite on `:5173` | | **API + infra only** | `docker compose up -d` | API, Postgres, Redis, MinIO — frontend locally | | **Local API + local frontend** | see [Run applications](#run-applications) | everything on host | | Service | URL / Port | Notes | | --------- | ---------------------------------------------- | ----- | | API | [http://localhost:8000](http://localhost:8000) | migrations + seed on startup | | Web (dev) | [http://localhost:5173](http://localhost:5173) | Docker `--profile docker-web` or `pnpm --filter web dev` | | Postgres | internal docker network | host access via `docker-compose.dev-ports.yml` | | Redis | internal docker network | host access via `docker-compose.dev-ports.yml` | | MinIO | internal docker network | host access via `docker-compose.dev-ports.yml` | > Do not run Docker `web` and `pnpm --filter web dev` at the same time — both bind port **5173**. The Docker `web` container bind-mounts source for live-reload on Windows/macOS (`CHOKIDAR_USEPOLLING=true`); `node_modules` stay isolated inside the container. ### Install secrets Each project copy gets unique runtime secrets generated once and locked forever (prevents accidental rotation and DB credential mismatch). | File | Purpose | | ---- | ------- | | `apps/api/data/secrets/install.env` | PostgreSQL, JWT, S3/MinIO credentials (gitignored) | | `apps/api/data/secrets/install.meta.json` | Install ID and lock timestamp (gitignored) | **Bootstrap (required before first `docker compose up`):** ```bash python apps/api/scripts/bootstrap_install.py ``` Generated keys: `POSTGRES_USER`, `POSTGRES_PASSWORD`, `DATABASE_URL`, `JWT_ACCESS_SECRET`, `JWT_REFRESH_PEPPER`, `S3_SECRET_KEY`, `MINIO_ROOT_PASSWORD`. Docker Compose passes `install.env` directly into the `api`, `postgres`, and `minio` services — no root `.env` or `--env-file` flag needed. - Re-run is safe: existing locked secrets are never overwritten. - API entrypoint also calls `ensure_install_secrets()` on startup (adopts env vars when migrating from an older setup). - **Do not rotate** `POSTGRES_PASSWORD` / JWT secrets after first bootstrap without a coordinated DB migration — see [docs/secrets-recovery.md](docs/secrets-recovery.md). **Host access to DB/Redis/MinIO** (DBeaver, pgAdmin, MinIO console): ```bash docker compose -f docker-compose.yml -f docker-compose.dev-ports.yml up -d ``` | Exposed port | Service | | ------------ | ------- | | 5432 | PostgreSQL | | 6379 | Redis | | 9000 / 9001 | MinIO API / console | **Reveal credentials:** Admin → Security → Install Secrets (superuser only, audited). Supports `database_password`, `jwt_access_secret`, `jwt_refresh_pepper`, `s3_secret_key`. ### Configure environment For hybrid or fully local dev, copy example env files: ```bash cp apps/api/.env.example apps/api/.env cp apps/web/.env.example apps/web/.env ``` Key variables: | Variable | App | Purpose | | ----------------------------------------- | --- | ---------------------------------------------------- | | `DATABASE_URL` | api | PostgreSQL connection string | | `APP_ENV` | api | `development` / `production` startup guards | | `JWT_ACCESS_SECRET`, `JWT_REFRESH_PEPPER` | api | Token signing (32+ bytes) | | `VITE_API_URL` | web | Local dev: `http://localhost:8000`. Docker `web`: `http://api:8000` | | `VITE_USE_API_PROXY` | web | `true` — proxy `/api` through Vite (default in dev) | | `EMAIL_DELIVERY_MODE` | api | `memory` (Docker dev) or `smtp` (real mail) | | `STORAGE_MODE` | api | `s3` (MinIO) or `memory` (tests) | | `S3_*` | api | MinIO/S3 credentials and bucket | | `CORS_ORIGINS` | api | Must include `http://localhost:5173` | | `ENABLE_TEST_ROUTES` | api | `true` only for E2E (email token helper) | | `TRUSTED_PROXY_IPS` | api | Which proxy IPs may set `X-Forwarded-For` | | `ADMIN_INITIAL_PASSWORD` | api | Seed password for superuser admin | | `DEMO_USER_PASSWORD` | api | Seed password for regular demo user | | `DEMO_OPS_PASSWORD` | api | Seed password for ops admin (no superuser) | | `ENABLE_RATE_LIMIT` | api | Rate limiting (required `true` in production) | | `COOKIE_SECURE` | api | HttpOnly refresh cookie `Secure` flag | | `COMPTON_SETTINGS_PATH` | api | Path to runtime settings JSON (default `data/compton_settings.json`) | In dev the frontend proxies API requests through Vite (`/api` → backend). Locally the target is `localhost:8000`; in Docker Compose it is the `api` service. ### Runtime settings (`compton_settings.json`) Non-secret runtime toggles live in `apps/api/data/compton_settings.json` and are applied on API startup. Superusers can read/update them via `GET/PATCH /admin/settings`. Env vars with the same keys (e.g. `ENABLE_RATE_LIMIT`, `CORS_ORIGINS`) act as **locks** — when set, the corresponding JSON field cannot be changed from the admin panel. Typical fields: rate limit, API docs, secure cookie, JWT TTL, CORS origins, SMTP host/port, avatar limits, audit retention. ### Install dependencies (hybrid / local dev only) Skip if you use `docker compose --profile docker-web` — dependencies are installed inside the `web` container. ```bash pnpm install pip install -r apps/api/requirements-dev.txt ``` ### Run applications **Option A — Full Docker stack:** ```bash python apps/api/scripts/bootstrap_install.py # first run only docker compose --profile docker-web up -d --build ``` **Option B — Docker API + local frontend:** ```bash python apps/api/scripts/bootstrap_install.py # first run only docker compose up -d pnpm --filter web dev ``` **Option C — Local API + local frontend:** ```bash cd apps/api alembic upgrade head uvicorn app.main:app --reload --app-dir . ``` In another terminal: ```bash pnpm --filter web dev ``` Open [http://localhost:5173](http://localhost:5173). ### Seed data After migrations the database is seeded on every API startup with demo users and CMS pages. Passwords come from env vars (defaults in `.env.example`): | Email | Env variable | Default (dev) | Role | Superuser | Access | | ----- | ------------ | --------------- | ---- | --------- | ------ | | `admin@compton.example` | `ADMIN_INITIAL_PASSWORD` | `Admin1234` | `admin` | yes | Full admin + Security/Diagnostics/Install Secrets | | `ops@compton.example` | `DEMO_OPS_PASSWORD` | `OpsAdmin1234` | `admin` | no | Users, Content, Activity (no Security/Diagnostics) | | `user@compton.example` | `DEMO_USER_PASSWORD` | `User1234` | `user` | no | Profile only | **Content pages:** `about`, `privacy`, `terms` (published). > Change demo passwords via env before first seed in production. Admin-created users must pass the shared password policy (length, complexity, denylist). ## Frontend Routes The project uses a **dual-entry** frontend: | Entry | Served at | Purpose | | ----- | --------- | ------- | | `index.html` | `/` | Public marketing landing (static HTML/CSS/JS in `main/`) | | `app.html` | `/login`, `/register`, `/profile`, `/admin`, `/pages/:slug`, … | React SPA (auth, profile, admin, CMS pages) | Vite middleware rewrites SPA paths to `app.html` on dev/preview (`vite.main-static.ts`). | Path | Page | Access | | ------------------ | --------------------- | ----------------------- | | `/` | Landing (`index.html`)| public | | `/login` | Login | guest | | `/register` | Registration | guest | | `/verify` | Email verification | public | | `/forgot-password` | Password reset request| guest | | `/reset-password` | Set new password | public (with token) | | `/pages/:slug` | CMS page | public (published only) | | `/profile` | Profile CRUD + avatar | authenticated | | `/admin` | Admin panel | role `admin` | **Auth model:** one login flow for everyone. The `is_superuser` flag on admin accounts controls access to critical panel sections (Security, Diagnostics, Install Secrets reveal, runtime settings). Regular admins see Users, Content, and Activity only. - **Access JWT** — in memory only (Zustand), not in `localStorage` / `sessionStorage`. - **Refresh token** — HttpOnly cookie (`Path=/api/v1/auth`, `SameSite=Lax`), rotated on each refresh. - **Session restore on reload** — `AuthBootstrap` calls a deduplicated `bootstrapSessionRefresh()` when a session hint exists in `sessionStorage` **or** the current path is protected (`/admin`, `/profile`). Guards wait for `bootstrapped` before redirecting. - **After login** — admins go to `/admin`, regular users to `/profile`. Admin users can return to the public landing via the **«На сайт»** topbar link (full navigation to `/`, not client-side React routing). ## API Overview Base URL: `http://localhost:8000/api/v1` | Area | Endpoints | | ----------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Health** | `GET /health` | | **Auth** | `POST /auth/register`, `/login`, `/logout`, `/refresh`, `/verify-email`, `/forgot-password`, `/reset-password` | | **Profile** | `GET/PATCH /users/me`, `POST /users/me/password`, `POST /users/me/avatar` | | **Content** | `GET /content/pages`, `GET /content/pages/{slug}`, admin: `POST/PATCH/DELETE /content/pages`, `GET /content/pages/manage/all` | | **Admin** | `GET/PATCH /admin/users`, `POST /admin/users`, `PATCH /admin/users/{id}/password`, `GET /admin/stats`, `GET/PATCH /admin/settings`, `GET /admin/diagnostics/report`, `GET /admin/activity-feed`, `POST /admin/ui-activity`, `GET /admin/server-log`, `GET /admin/secrets`, `POST /admin/secrets/reveal` | | **Media** | `GET /media/files/{path}` | OpenAPI docs (when `ENABLE_DOCS=true`): [http://localhost:8000/api/v1/docs](http://localhost:8000/api/v1/docs) ## Database Migrations ```bash cd apps/api alembic upgrade head # apply schema alembic downgrade -1 # rollback one revision ``` Tables: `users`, `user_profiles`, `refresh_tokens`, `password_reset_tokens`, `email_verification_tokens`, `content_pages`. **Security constraints** (PostgreSQL, migration `20260714_0004`): - `CHECK` on `users.role`, `users.status`, `content_pages.status` - Superuser rule: `is_superuser=true` only when `role=admin` - Indexes on `expires_at` for token tables Expired tokens are cleaned up on API startup (`cleanup_expired_tokens`, 30-day retention). ## Auth Email (SMTP) - Verify/reset use one-time opaque tokens (SHA-256 hash + pepper in DB, TTL 1 hour); raw token omitted from production email bodies - Env: `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM`, `FRONTEND_URL`, `EMAIL_DELIVERY_MODE` - Docker dev: `EMAIL_DELIVERY_MODE=memory` (no real SMTP required) - Local SMTP: Mailpit/Mailhog on `localhost:1025` (`SMTP_HOST=localhost`, `SMTP_PORT=1025`) - Tests: `EMAIL_DELIVERY_MODE=memory` (in-memory outbox) ## Profile & Avatar (MinIO) - `GET/PATCH /api/v1/users/me`, `POST /api/v1/users/me/password`, `POST /api/v1/users/me/avatar` - Avatar: jpeg/png/webp, max 2 MB, re-encoded via Pillow (SVG rejected) - Storage env: `STORAGE_MODE` (`s3` or `memory`), `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET` - Local dev: MinIO on `localhost:9000` (console `9001`), bucket `compton` (created on API startup) - Tests: `STORAGE_MODE=memory` (in-memory file store, no MinIO) ## Admin Panel - Route: `/admin` (`AdminGuard`, role `admin`) - UI: WESP-style **Ant Design** shell with fixed left sidebar, content topbar, and a dedicated admin theme system - Topbar actions: - **Light/Dark** — toggles isolated admin theme (preference persisted in `localStorage`) - **«На сайт»** — opens the static landing at `/` (`index.html`) via full navigation - **Logout** — revokes refresh cookie and redirects to `/login` - Reload UX: no blocking «Loading…» screens; admin routes preload the current admin background (`#f6f6f4` in Light, `#1f2229` in Dark) to avoid flash - Sections: - **Users** — list/create/patch role, status, password - **Content** — CMS CRUD - **Security** *(superuser only)* — runtime toggles + Install Secrets reveal - **Diagnostics** *(superuser only)* — security health checks (JWT, CORS, DB credentials, SSL mode, token table size, install secrets lock) - **Activity** — admin audit feed + server log tail - Business rules: last admin protected, no self-demotion, no self-block; critical endpoints require `is_superuser` ### Auth UI (zootech) Auth pages (`/login`, `/register`, `/forgot-password`, `/reset-password`, `/verify`) use a WESP-inspired zootech card layout with organic theme tokens (`#48816d`, centered card, icon inputs). `AppHeader` is hidden on auth routes and in `/admin`. ## Tests ```bash # Frontend unit/component (coverage ≥ 85%) pnpm --filter web test:ci # Same, inside Docker web container (when pnpm is not installed on host) docker compose --profile docker-web exec web sh -c "cd apps/web && pnpm test:ci" # Backend unit/integration (coverage ≥ 90%) cd apps/api && python -m pytest --cov=app --cov-fail-under=90 # E2E regression (Playwright §15.7, 16 critical scenarios) pnpm --filter web e2e ``` ### E2E (Playwright) - `pnpm --filter web e2e` starts a dedicated API (`:8001`) and Vite (`:5175`) — does not conflict with dev on `:5173` or Docker API on `:8000` - Uses SQLite + `EMAIL_DELIVERY_MODE=memory` + `ENABLE_TEST_ROUTES=true` - Test email tokens: `GET /api/v1/test/emails/latest-token` (only when test routes enabled) - Reuse running API: `E2E_START_API=false E2E_API_URL=http://localhost:8000 pnpm --filter web e2e` - Install browsers once: `pnpm --filter web exec playwright install chromium` ## Troubleshooting | Problem | Solution | | ------- | -------- | | **`docker compose up` fails: install.env missing** | Run `python apps/api/scripts/bootstrap_install.py` before first start | | **API exits: `password authentication failed for user "compton_app"`** | Postgres volume was initialized before bootstrap. Reset local dev data: `docker compose --profile docker-web down -v`, bootstrap again, then `docker compose --profile docker-web up -d --build`. See [docs/secrets-recovery.md](docs/secrets-recovery.md) | | **Login failed** with correct credentials | Check API: `curl http://localhost:8000/api/v1/health`. Ensure `apps/web/.env` exists for local frontend. Restart: `docker compose restart web` or `pnpm --filter web dev` | | **401 on `/auth/refresh` in browser console (guest)** | Expected for logged-out users on public pages — refresh is skipped unless a session hint or protected path (`/admin`, `/profile`) triggers bootstrap | | **Logged out after F5 on `/admin`** | Usually a failed refresh (expired cookie) or stale Docker web build. Rebuild: `docker compose --profile docker-web up -d --build web`. Log in again if the refresh cookie expired | | **«На сайт» in admin does nothing / goes to login** | Must use full navigation to `/` (static landing), not React Router. Ensure topbar action is `href="/"`, then rebuild web container if behavior persists | | **White flash on admin reload** | `app.html` preloads admin background before React mount. Verify `localStorage.wespAdminTheme` and rebuild web if stale assets are served | | **Sidebar should stay visible while scrolling** | Admin sidebar is fixed on desktop (`position: fixed`, `height: 100vh`) and switches back to normal flow on mobile (`<=768px`) | | **Port 5173 already in use** | Stop Docker web: `docker compose --profile docker-web stop web`. Or stop local Vite | | **Frontend in Docker: missing modules / esbuild errors** | Rebuild: `docker compose --profile docker-web up -d --build web`. Do not run `pnpm install` on the host for the Docker workflow | | **Hot-reload not working in Docker (Windows/macOS)** | Enabled via `CHOKIDAR_USEPOLLING=true`. Restart: `docker compose --profile docker-web restart web` | | **Admin panel missing Security/Diagnostics tabs** | Log in as `admin@compton.example` (superuser), not `ops@compton.example` | | **Need DB password for DBeaver** | Admin → Security → Install Secrets → Reveal DB password (superuser), then `docker-compose.dev-ports.yml` | | **Lost `install.env` / DB auth failed** | See [docs/secrets-recovery.md](docs/secrets-recovery.md). Do not regenerate secrets if Postgres volume already exists | | **CORS errors in browser console** | Keep `VITE_USE_API_PROXY=true` (default in dev). Do not call `localhost:8000` directly from the browser | ## Security Highlights All cryptographic primitives live in a single module (`apps/api/app/core/crypto.py`); `security.py` and `media_signing.py` re-export from it. | Layer | Mechanism | | ----- | --------- | | **Passwords** | bcrypt cost 12 with per-user salt (embedded in hash); shared denylist | | **Access JWT** | HS256, short TTL, in-memory on frontend only | | **Refresh tokens** | HttpOnly cookie (`Path=/api/v1/auth`), rotation + family reuse detection; SHA-256 hash with server-side **pepper** | | **Email/reset tokens** | Opaque tokens, SHA-256 + pepper in DB; raw token never stored | | **Media URLs** | HMAC-SHA256 signed paths with TTL | | **Install secrets** | `secrets.token_*` generation; generate-once + lock; superuser reveal (audited) | **Auth & API** - Refresh token rotation and reuse-detection (family revoke) - Rate limiting and brute-force lockout (`ENABLE_RATE_LIMIT=true` in Docker compose) - Pending/blocked users rejected on refresh; frontend clears session on `403` (`ACCOUNT_BLOCKED`, `EMAIL_NOT_VERIFIED`) - Origin/Referer validation on cookie-based auth endpoints - `TRUSTED_PROXY_IPS` — only listed proxies may influence client IP via `X-Forwarded-For` - Email bodies omit raw tokens in production (`EMAIL_DELIVERY_MODE=memory` or `ENABLE_TEST_ROUTES=true` only) **Data & infra** - PostgreSQL/Redis/MinIO on internal Docker network by default (no host ports) - DB CHECK constraints, connection pool tuning, startup token cleanup - CMS HTML sanitization (bleach) with allowed URL protocols: `http`, `https`, `mailto` - Signed avatar URLs; RBAC + superuser guards on admin routes - Admin password policy on user create/reset **Production guards** (`APP_ENV=production` — API refuses to start if): - `ENABLE_TEST_ROUTES=true` - `ENABLE_DOCS=true` - `ENABLE_RATE_LIMIT=false` - `COOKIE_SECURE=false` - Default DB credentials (`user:pass`) or placeholder JWT secrets - `DATABASE_URL` without `sslmode=require` ## Related Documentation - [docs/secrets-recovery.md](docs/secrets-recovery.md) — recover from lost install secrets - [docs/security-checklist.md](docs/security-checklist.md) — pre-production security checklist - [docs/TZ.md](docs/TZ.md) — full technical specification ## MVP Status (Phase 1) | # | Feature | Status | | --- | ----------------------------- | ------ | | 1.1 | PostgreSQL + Alembic + seed | done | | 1.2 | Auth + SMTP (verify/reset) | done | | 1.3 | Profile CRUD + avatar (MinIO) | done | | 1.4 | Content pages | done | | 1.5 | Admin panel (Ant Design + zootech auth UI) | done | | 1.6 | E2E regression §15.7 | done |