Files
site/README.md
T
vlad 86cc3fa541 Update admin theme/layout and refresh README details.
Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
2026-07-14 17:12:28 +03:00

427 lines
23 KiB
Markdown

# 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 |