Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
23 KiB
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 SPAapps/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 targetpackages/eslint-config— shared eslint config packageinfra— docker/nginx/ci helper filesdocker-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):
# 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 — 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 for all demo accounts.
Optional — copy env files if you also run API or frontend locally (hybrid mode):
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 withoutinstall.env, the API will fail withpassword authentication failed for user "compton_app". Fix:docker compose --profile docker-web down -v, then bootstrap + up again. See Troubleshooting.
Hybrid setup (Docker API + local frontend):
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 | everything on host |
| Service | URL / Port | Notes |
|---|---|---|
| API | http://localhost:8000 | migrations + seed on startup |
| Web (dev) | 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
webandpnpm --filter web devat 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):
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.
Host access to DB/Redis/MinIO (DBeaver, pgAdmin, MinIO console):
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:
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.
pnpm install
pip install -r apps/api/requirements-dev.txt
Run applications
Option A — Full Docker stack:
python apps/api/scripts/bootstrap_install.py # first run only
docker compose --profile docker-web up -d --build
Option B — Docker API + local frontend:
python apps/api/scripts/bootstrap_install.py # first run only
docker compose up -d
pnpm --filter web dev
Option C — Local API + local frontend:
cd apps/api
alembic upgrade head
uvicorn app.main:app --reload --app-dir .
In another terminal:
pnpm --filter web dev
Open 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):
| 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 —
AuthBootstrapcalls a deduplicatedbootstrapSessionRefresh()when a session hint exists insessionStorageor the current path is protected (/admin,/profile). Guards wait forbootstrappedbefore 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
Database Migrations
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):
CHECKonusers.role,users.status,content_pages.status- Superuser rule:
is_superuser=trueonly whenrole=admin - Indexes on
expires_atfor 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(s3ormemory),S3_ENDPOINT,S3_ACCESS_KEY,S3_SECRET_KEY,S3_BUCKET - Local dev: MinIO on
localhost:9000(console9001), bucketcompton(created on API startup) - Tests:
STORAGE_MODE=memory(in-memory file store, no MinIO)
Admin Panel
- Route:
/admin(AdminGuard, roleadmin) - 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
- Light/Dark — toggles isolated admin theme (preference persisted in
- Reload UX: no blocking «Loading…» screens; admin routes preload the current admin background (
#f6f6f4in Light,#1f2229in 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
# 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 e2estarts a dedicated API (:8001) and Vite (:5175) — does not conflict with dev on:5173or 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 |
| 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. 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=truein 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 viaX-Forwarded-For- Email bodies omit raw tokens in production (
EMAIL_DELIVERY_MODE=memoryorENABLE_TEST_ROUTES=trueonly)
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=trueENABLE_DOCS=trueENABLE_RATE_LIMIT=falseCOOKIE_SECURE=false- Default DB credentials (
user:pass) or placeholder JWT secrets DATABASE_URLwithoutsslmode=require
Related Documentation
- docs/secrets-recovery.md — recover from lost install secrets
- docs/security-checklist.md — pre-production security checklist
- 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 |