docs: add product overview and engineering documentation
Document architecture, security, deployment, and development for contributors.
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
||||
# Pargar — domain summary
|
||||
|
||||
> Full source of truth: [`docs/`](docs/README.md) · repo entry: [`README.md`](README.md)
|
||||
|
||||
Gated LMS: verified watch → quiz with hearts → skill-tree unlock (staff) → mentor chat, events, weekly league, monthly XP challenges, certificates.
|
||||
|
||||
## Stack (short)
|
||||
|
||||
- **Backend:** Go (`backend/`) · Postgres · Redis · S3/RustFS · WS hub
|
||||
- **Frontend:** Next.js App Router · React Query · Tailwind · Vazirmatn FD
|
||||
- **Edge:** Traefik v3.7 — Compose and Helm/k3s
|
||||
|
||||
## Student path (short)
|
||||
|
||||
Login/CAPTCHA or register (public/invite) → `/cap` → `/player` → `/quiz` → staff unlock → certificate `/c/{publicId}` · profile `/profile` · hub `/unwrap`.
|
||||
|
||||
Public password recovery is disabled for students; resets are admin-only. Browser sessions use HttpOnly cookies.
|
||||
|
||||
## Docs
|
||||
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Architecture | [docs/architecture.md](docs/architecture.md) |
|
||||
| Product | [docs/product.md](docs/product.md) |
|
||||
| Development | [docs/development.md](docs/development.md) |
|
||||
| Deployment | [docs/deployment.md](docs/deployment.md) |
|
||||
| Security | [docs/security.md](docs/security.md) |
|
||||
| Operations | [docs/operations.md](docs/operations.md) |
|
||||
| API | [docs/api.md](docs/api.md) |
|
||||
| Env | [docs/configuration.md](docs/configuration.md) |
|
||||
|
||||
Phone registration, unlisted certificates, chat-export, rate limits, and production notes live in those docs (not duplicated here to avoid drift).
|
||||
@@ -0,0 +1,67 @@
|
||||
# Pargar
|
||||
|
||||
A gated LMS for bootcamps: verified watch → quiz with hearts → skill-tree unlock → mentor chat, events, weekly league, monthly challenges, and certificates.
|
||||
|
||||
| Layer | Stack |
|
||||
|-------|--------|
|
||||
| API | Go 1.27+ · Postgres · Redis · S3/RustFS · WebSocket |
|
||||
| UI | Next.js 15 · React Query · Tailwind · Vazirmatn FD |
|
||||
| Edge | Traefik v3.7 (Compose and k3s) |
|
||||
|
||||
## Quick start (local)
|
||||
|
||||
```bash
|
||||
cp .env.example .env # fill in real secrets
|
||||
./scripts/compose-dev.sh up -d --build
|
||||
```
|
||||
|
||||
- App: **https://localhost** (Traefik default cert → one browser warning is expected)
|
||||
- Traefik dashboard: http://127.0.0.1:8088
|
||||
- Health: `curl -k https://localhost/api/health`
|
||||
|
||||
k3s:
|
||||
|
||||
```bash
|
||||
./scripts/k3s-load-images.sh
|
||||
./scripts/helm-k3s.sh
|
||||
curl -k https://pargar.local/api/health
|
||||
```
|
||||
|
||||
Deployment details: [docs/deployment.md](docs/deployment.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
| Doc | Contents |
|
||||
|-----|----------|
|
||||
| [docs/README.md](docs/README.md) | Doc index and reading map |
|
||||
| [docs/architecture.md](docs/architecture.md) | System architecture and data flow |
|
||||
| [docs/product.md](docs/product.md) | Student / admin paths and features |
|
||||
| [docs/development.md](docs/development.md) | Development, tests, code conventions |
|
||||
| [docs/deployment.md](docs/deployment.md) | Compose · Helm/k3s · production |
|
||||
| [docs/security.md](docs/security.md) | Auth, secrets, rate limits, uploads |
|
||||
| [docs/operations.md](docs/operations.md) | Backup, metrics, troubleshooting |
|
||||
| [docs/api.md](docs/api.md) | API contract and key routes |
|
||||
| [docs/configuration.md](docs/configuration.md) | Environment variable reference |
|
||||
|
||||
Domain summary for agents: [CONTEXT.md](CONTEXT.md)
|
||||
|
||||
## Repository layout
|
||||
|
||||
```text
|
||||
backend/ Go API, migrations, seed, media/sample.mp4
|
||||
frontend/ Next.js App Router
|
||||
deploy/ Compose + Traefik + Helm chart
|
||||
scripts/ compose / k3s / backup / regenerate-sample
|
||||
docs/ engineering documentation
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
cd backend && go test ./internal/api/... ./internal/config/...
|
||||
cd frontend && bunx tsc --noEmit
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the [GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0).
|
||||
@@ -0,0 +1,28 @@
|
||||
# Pargar documentation
|
||||
|
||||
This folder is the engineering source of truth. [`README.md`](../README.md) is only the entry point; details live here.
|
||||
|
||||
## Reading map
|
||||
|
||||
1. **Newcomer / product** → [product.md](product.md)
|
||||
2. **Architecture** → [architecture.md](architecture.md)
|
||||
3. **Local development** → [development.md](development.md)
|
||||
4. **Deployment** → [deployment.md](deployment.md) (plus the ops shortcut in [`deploy/README.md`](../deploy/README.md))
|
||||
5. **Security & launch** → [security.md](security.md)
|
||||
6. **Day-to-day ops** → [operations.md](operations.md)
|
||||
7. **API contract** → [api.md](api.md)
|
||||
8. **Env & settings** → [configuration.md](configuration.md)
|
||||
|
||||
## Writing conventions
|
||||
|
||||
- File paths, env names, shell commands, and route names stay in **exact English**.
|
||||
- Prefer linking over long duplication across files.
|
||||
- When behavior changes, update the matching doc in the same PR.
|
||||
|
||||
## Related files outside `docs/`
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| [`CONTEXT.md`](../CONTEXT.md) | Short domain summary for agents / fast onboarding |
|
||||
| [`deploy/README.md`](../deploy/README.md) | Deploy command shortcut |
|
||||
| [`.env.example`](../.env.example) / [`.env.prod.example`](../.env.prod.example) | Secret templates |
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# API
|
||||
|
||||
Base: `/api`. Signed media: `/media/…`. Health: `GET /api/health` (outside the rate-limited mux).
|
||||
|
||||
## Response contract
|
||||
|
||||
### Success
|
||||
|
||||
Ordinary JSON; standard 2xx codes.
|
||||
|
||||
### Error
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "user-facing message",
|
||||
"code": "unauthorized",
|
||||
"requestId": "…"
|
||||
}
|
||||
```
|
||||
|
||||
Response header: `X-Request-ID` (generated by the server if the client did not send one).
|
||||
|
||||
The frontend maps these via `toUserError` and `ApiError`.
|
||||
|
||||
## Authentication
|
||||
|
||||
| Method | Use |
|
||||
|--------|-----|
|
||||
| Cookie `pargar_access` | Same-origin browser |
|
||||
| `Authorization: Bearer <access>` | Tests, tools, in-memory token frontends |
|
||||
| Cookie / body `refreshToken` | `POST /api/auth/refresh` |
|
||||
|
||||
WS: Bearer → subprotocol `bearer` + token → access cookie. Query string is forbidden.
|
||||
|
||||
## Public (no login)
|
||||
|
||||
| Method | Path | Notes |
|
||||
|--------|------|--------|
|
||||
| GET | `/api/auth/captcha` | Image challenge |
|
||||
| POST | `/api/auth/login` | Login |
|
||||
| POST | `/api/auth/refresh` | Session rotation |
|
||||
| POST | `/api/auth/recovery/challenge` | Disabled (404) — public student recovery is off |
|
||||
| POST | `/api/auth/recovery/reset` | Disabled (404) |
|
||||
| GET | `/api/auth/register-status` | Whether public registration is on |
|
||||
| POST | `/api/auth/register` | Public registration |
|
||||
| GET/POST | `/api/auth/register-invite/{token}` | Invite status / consume |
|
||||
| GET | `/api/certificates/{publicId}` | Public certificate |
|
||||
| GET | `/api/community` | Donation / support |
|
||||
| GET | `/api/sponsors` | Landing sponsors |
|
||||
| GET | `/api/exports/chats/{token}` | Single-use chat archive download |
|
||||
|
||||
## Authenticated (student+)
|
||||
|
||||
Key examples:
|
||||
|
||||
- `GET /api/auth/me` · `POST /api/auth/logout`
|
||||
- Tree and lessons: `/api/tree` · `/api/lessons/{id}` · heartbeat · quiz
|
||||
- `GET /api/leaderboard/weekly` · `GET /api/challenges`
|
||||
- Events and RSVP: `/api/events…`
|
||||
- Chat: `/api/chats/…` · upload `/api/chats/upload`
|
||||
- Notifications, unlock/reset requests
|
||||
- My certificate: `/api/me/certificate` · issue · physical
|
||||
- Avatar: `PUT /api/me/avatar` · `POST /api/me/avatar-photo`
|
||||
- `GET /api/ws`
|
||||
|
||||
Full handler list: [`backend/internal/api/router.go`](../backend/internal/api/router.go).
|
||||
|
||||
## Staff / admin
|
||||
|
||||
Prefix `/api/admin/…`:
|
||||
|
||||
- **admin:** stats, users (CRUD, lock, role, reset-password), challenges, announcements, settings, sponsors, physical orders, invites, phone whitelist/blacklist, chat-export (+ tokens)
|
||||
- **staff (admin + mentor):** learning overview/students, student unlock, chapters / lessons / questions / videos, events
|
||||
- `GET /api/media/sign` staff only
|
||||
|
||||
Mentors do not manage accounts, registration, challenges, announcements, or site settings; they handle learning status and teaching content.
|
||||
|
||||
## Versioning
|
||||
|
||||
No version prefix for now; call out breaking changes in CHANGELOG/PR. External clients should prefer same-origin cookies or short-lived Bearer tokens.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
```text
|
||||
Browser
|
||||
│ HTTPS (same origin)
|
||||
▼
|
||||
Traefik ──► frontend (Next.js :3000) paths: UI
|
||||
└──► backend (Go :8080) paths: /api/*, /media/*, /c/* (via FE + API)
|
||||
│
|
||||
├── Postgres (stateful: users, progress, chats, certs, …)
|
||||
├── Redis (rate limit, captcha, refresh metadata, pub/sub WS)
|
||||
└── Object storage (RustFS/S3) videos, avatars, chat attachments
|
||||
```
|
||||
|
||||
- **One public origin:** the browser talks only to the public domain; Traefik splits paths.
|
||||
- **Backend** owns business logic, auth, media signing, and WebSocket.
|
||||
- **Frontend** is an App Router SPA; data comes from React Query with `credentials: "include"`.
|
||||
|
||||
Edge path details: [deployment.md](deployment.md#routing)
|
||||
|
||||
## Backend packages
|
||||
|
||||
| Package | Role |
|
||||
|---------|------|
|
||||
| `internal/api` | HTTP handlers, middleware, captcha, rate limit, cookies |
|
||||
| `internal/store` | Postgres access |
|
||||
| `internal/config` | Env loading and `Validate()` |
|
||||
| `internal/storage` | Local / S3; `EnsureSampleMP4` at startup |
|
||||
| `internal/seed` | Admin bootstrap + optional curriculum |
|
||||
| `internal/db/migrations` | Ordered SQL |
|
||||
| `cmd/server` | Process entrypoint |
|
||||
|
||||
## Learning flow (core)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
login[Login_CAPTCHA]
|
||||
tree[Skill_tree_cap]
|
||||
watch[Verified_watch]
|
||||
quiz[Quiz_hearts]
|
||||
unlock[Staff_unlock]
|
||||
cert[Digital_cert]
|
||||
login --> tree --> watch --> quiz
|
||||
quiz -->|fail_hearts| lockout[Lockout]
|
||||
quiz -->|pass| unlock
|
||||
unlock --> watch
|
||||
unlock -->|curriculum_done| cert
|
||||
```
|
||||
|
||||
1. Watch accumulates verified time via anti-cheat heartbeats until the completion threshold.
|
||||
2. Quizzes spend hearts; zero → `/lockout` until staff reopens the account.
|
||||
3. Unlocking the next lesson is **staff-only**; the student requests it and follows up in chat.
|
||||
4. Completing the curriculum → unlisted digital certificate; physical copy is optional and offline.
|
||||
|
||||
## Sessions and realtime
|
||||
|
||||
- Browser session: HttpOnly cookies `pargar_access` / `pargar_refresh` (+ JSON tokens for non-browser clients). Details: [security.md](security.md).
|
||||
- WebSocket: `GET /api/ws` — Bearer, `Sec-WebSocket-Protocol: bearer,<jwt>`, or same-origin access cookie.
|
||||
- Hub coordinates across replicas via Redis pub/sub (online presence is per-process).
|
||||
|
||||
## Media storage
|
||||
|
||||
- Lesson keys (e.g. `sample.mp4`) live in object storage; browser URLs are short-lived HMAC-signed.
|
||||
- At startup, if the sample key is missing, `backend/media/sample.mp4` is uploaded.
|
||||
- Chat / avatar / admin video uploads use content sniffing (not extension alone).
|
||||
|
||||
## Errors and observability
|
||||
|
||||
- Error body: JSON `{ error, code, requestId }` plus `X-Request-ID`.
|
||||
- Frontend: `toUserError`, toasts for mutations, empty vs error states in lists.
|
||||
- Prometheus metrics on `/metrics` (requires `METRICS_TOKEN` outside DevMode).
|
||||
- Optional OTLP via `OTEL_*`.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Configuration
|
||||
|
||||
Templates: [`.env.example`](../.env.example) (Compose dev), [`.env.prod.example`](../.env.prod.example) (Compose prod). In Kubernetes, values come from Secret / chart values.
|
||||
|
||||
## Required in production (`DEV_MODE=false`)
|
||||
|
||||
| Variable | Notes |
|
||||
|----------|--------|
|
||||
| `JWT_SECRET` | ≥32 characters; not a placeholder |
|
||||
| `ADMIN_PASSWORD` | Strong; only for initial missing-admin bootstrap |
|
||||
| `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` | When `STORAGE_DRIVER=s3` |
|
||||
| `PUBLIC_URL` | Public origin with scheme |
|
||||
| `FRONTEND_ORIGINS` | CSV of allowed CORS origins |
|
||||
|
||||
`config.Validate()` fails boot if secrets look like `change-me` / `local-dev-only`.
|
||||
|
||||
## Database and Redis
|
||||
|
||||
One of two patterns:
|
||||
|
||||
**A) Ready-made URLs**
|
||||
|
||||
- `DATABASE_URL`
|
||||
- `REDIS_URL`
|
||||
|
||||
**B) Parts (Helm in-cluster)** — URL is built with correct encoding:
|
||||
|
||||
| Variable | Conceptual default |
|
||||
|----------|--------------------|
|
||||
| `POSTGRES_USER` | pargar |
|
||||
| `POSTGRES_PASSWORD` | — |
|
||||
| `POSTGRES_HOST` | postgres service |
|
||||
| `POSTGRES_PORT` | 5432 |
|
||||
| `POSTGRES_DB` | pargar |
|
||||
| `POSTGRES_SSLMODE` | disable |
|
||||
| `REDIS_HOST` | redis service |
|
||||
| `REDIS_PORT` | 6379 |
|
||||
| `REDIS_PASSWORD` | — |
|
||||
| `REDIS_DB` | optional |
|
||||
|
||||
## Storage
|
||||
|
||||
| Variable | Notes |
|
||||
|----------|--------|
|
||||
| `STORAGE_DRIVER` | `s3` (default prod/compose) or local in tests |
|
||||
| `STORAGE_ENDPOINT` | e.g. `http://rustfs:9000` |
|
||||
| `STORAGE_REGION` | us-east-1 |
|
||||
| `STORAGE_BUCKET` | pargar |
|
||||
| `STORAGE_DIR` | Local driver only |
|
||||
|
||||
## Admin bootstrap
|
||||
|
||||
| Variable | Notes |
|
||||
|----------|--------|
|
||||
| `ADMIN_NAME` | Display name |
|
||||
| `ADMIN_USERNAME` | e.g. admin |
|
||||
| `ADMIN_EMAIL` | Login identity |
|
||||
| `ADMIN_PASSWORD` | Applied only if the admin user does not exist |
|
||||
|
||||
## Product behavior
|
||||
|
||||
| Variable | Logical default | Notes |
|
||||
|----------|-----------------|--------|
|
||||
| `DEV_MODE` | false | Open metrics without token, limited fail-open behavior |
|
||||
| `SEED_CURRICULUM` | false (empty in DevMode → true) | Seed lessons/events |
|
||||
| `CAPTCHA_EXPOSE_ANSWER` | DevMode only | For tests; never in production |
|
||||
| `DONATION_ENABLED` | true | Support card |
|
||||
| `DONATION_NOTE` | Copy text | |
|
||||
| `PHYSICAL_CERT_ENABLED` | false in config/chart | Physical certificate |
|
||||
| `PHYSICAL_CERT_PRICE_IRR` | 2500000 | |
|
||||
| `PHYSICAL_CERT_WINDOW_DAYS` | 30 | |
|
||||
| `HEART_REGEN_HOURS` | 4 | |
|
||||
|
||||
Many boolean/text settings can later be overridden from Admin → site settings (`app_settings`).
|
||||
|
||||
## Network and observability
|
||||
|
||||
| Variable | Notes |
|
||||
|----------|--------|
|
||||
| `DOMAIN` | Traefik Host (Compose) |
|
||||
| `TRUSTED_PROXY_CIDRS` | CSV of networks trusted for X-Forwarded-* |
|
||||
| `METRICS_TOKEN` | Guards `/metrics` |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Empty = off |
|
||||
| `OTEL_SERVICE_NAME` | pargar-api |
|
||||
| `PORT` | 8080 |
|
||||
|
||||
## Frontend
|
||||
|
||||
| Build variable | Notes |
|
||||
|----------------|--------|
|
||||
| `NEXT_PUBLIC_API_URL` | Empty = same-origin |
|
||||
| `NEXT_PUBLIC_WS_URL` | Empty = `ws(s)://host/api/ws` |
|
||||
|
||||
## Helm (summary)
|
||||
|
||||
See `deploy/helm/pargar/values.yaml`:
|
||||
|
||||
- `global.domain` / `publicUrl` / `frontendOrigins` / `trustedProxyCIDRs`
|
||||
- `backend.env.*` → container env
|
||||
- Empty `backend.secrets` → generate-once
|
||||
- `backup.*` → CronJob
|
||||
- `postgres` / `redis` / `rustfs` or `external.*`
|
||||
@@ -0,0 +1,105 @@
|
||||
# Deployment
|
||||
|
||||
Fuller command reference: [`deploy/README.md`](../deploy/README.md). This document covers deployment models and production requirements.
|
||||
|
||||
## Options
|
||||
|
||||
| Environment | Tooling | TLS | Data |
|
||||
|-------------|---------|-----|------|
|
||||
| Laptop | Compose + `compose.dev.yaml` | Traefik default cert | Postgres/Redis/RustFS in compose |
|
||||
| Single-host VPS | Compose + `compose.prod.yaml` | Let's Encrypt HTTP-01 | Same + no published DB ports |
|
||||
| Local k3s | Helm `values-k3s.yaml` | Chart self-signed | In-cluster |
|
||||
| Production K8s | Helm `values-prod.yaml` | cert-manager | Usually external DB/Redis/S3 |
|
||||
|
||||
## Routing
|
||||
|
||||
| Path | Service |
|
||||
|------|---------|
|
||||
| `/api/*`, `/media/*` | backend |
|
||||
| `/api/certificates/*`, `/c/*` | backend (+ noindex middleware) |
|
||||
| everything else | frontend |
|
||||
| `/api/ws` | backend WebSocket via Traefik |
|
||||
|
||||
## Compose — development
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
./scripts/compose-dev.sh up -d --build
|
||||
```
|
||||
|
||||
- App: https://localhost
|
||||
- Traefik dashboard: http://127.0.0.1:8088
|
||||
- DB ports only on `127.0.0.1`
|
||||
|
||||
## Compose — production
|
||||
|
||||
```bash
|
||||
cp .env.prod.example .env.prod
|
||||
./scripts/compose-prod.sh up -d --build
|
||||
```
|
||||
|
||||
DNS must point at the host. `DOMAIN`, `ACME_EMAIL`, and strong secrets are required.
|
||||
|
||||
## k3s + Helm
|
||||
|
||||
```bash
|
||||
# Install k3s and kubeconfig once (see deploy/README.md)
|
||||
./scripts/k3s-load-images.sh # build and import local images
|
||||
./scripts/helm-k3s.sh # upgrade --install with values-k3s.yaml
|
||||
echo "127.0.0.1 pargar.local" | sudo tee -a /etc/hosts # if needed
|
||||
curl -k https://pargar.local/api/health
|
||||
```
|
||||
|
||||
### Values
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `deploy/helm/pargar/values.yaml` | Defaults |
|
||||
| `values-k3s.yaml` | Local: seed, backup CronJob, self-signed TLS |
|
||||
| `values-prod.yaml` | Registry, HPA, PDB, NetworkPolicy, `existingSecret` |
|
||||
|
||||
```bash
|
||||
helm lint ./deploy/helm/pargar -f ./deploy/helm/pargar/values-k3s.yaml
|
||||
helm template pargar ./deploy/helm/pargar -f ./deploy/helm/pargar/values-k3s.yaml
|
||||
```
|
||||
|
||||
Helm 4 rejects negation patterns in `.helmignore` (`!…`); do not add them.
|
||||
|
||||
### Chart secrets
|
||||
|
||||
- Without `backend.existingSecret`: the chart generate-once via `lookup` + `randAlphaNum` and keeps values across upgrades.
|
||||
- `change-me-*` placeholders in values are ignored.
|
||||
- An existing Postgres password is **never rotated** (data depends on it).
|
||||
- Production: always pre-create the Secret and set `backend.existingSecret`.
|
||||
|
||||
Read a generated admin password:
|
||||
|
||||
```bash
|
||||
kubectl -n pargar get secret pargar-app -o jsonpath='{.data.admin-password}' | base64 -d; echo
|
||||
```
|
||||
|
||||
### DB/Redis wiring in Helm
|
||||
|
||||
Instead of a raw URL with an unencoded password, the backend builds from parts:
|
||||
|
||||
- `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_HOST` / `POSTGRES_DB` / `POSTGRES_SSLMODE`
|
||||
- `REDIS_HOST` / `REDIS_PASSWORD`
|
||||
|
||||
Or for external production: `DATABASE_URL` / `REDIS_URL` inside the Secret.
|
||||
|
||||
### In-cluster backup
|
||||
|
||||
With `postgres.enabled` and `backup.enabled=true` (on for k3s): daily CronJob `pg_dump | gzip` onto a PVC with retention. For managed Postgres in production, set `backup.enabled=false`.
|
||||
|
||||
## Production checklist
|
||||
|
||||
1. `DEV_MODE=false`
|
||||
2. Strong `JWT_SECRET` and `ADMIN_PASSWORD` (no placeholders) — otherwise `config.Validate()` refuses to boot
|
||||
3. Exact public `PUBLIC_URL` and `FRONTEND_ORIGINS`
|
||||
4. `TRUSTED_PROXY_CIDRS` limited to the proxy network
|
||||
5. `METRICS_TOKEN` to expose `/metrics`
|
||||
6. Images from a private registry + `imagePullSecrets`
|
||||
7. cert-manager / real TLS
|
||||
8. DB and object-storage backups defined
|
||||
9. `PHYSICAL_CERT_ENABLED` only if the offline payment process is ready
|
||||
10. NetworkPolicy / HPA per `values-prod.yaml`
|
||||
@@ -0,0 +1,66 @@
|
||||
# Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker + Compose
|
||||
- Go (aligned with the backend Dockerfile; currently 1.27-alpine)
|
||||
- Node 22+ / bun for the frontend (image builds use `npm ci`)
|
||||
- Optional: k3s, Helm 3/4, kubectl, ffmpeg (only to regenerate the sample video)
|
||||
|
||||
## Local run (recommended)
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Fill POSTGRES_PASSWORD, REDIS_PASSWORD, RUSTFS_*, JWT_SECRET, ADMIN_PASSWORD
|
||||
./scripts/compose-dev.sh up -d --build
|
||||
```
|
||||
|
||||
Equivalent:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/compose/compose.yaml -f deploy/compose/compose.dev.yaml --env-file .env up -d --build
|
||||
```
|
||||
|
||||
The root `docker-compose.yml` includes the same pair.
|
||||
|
||||
For UI iteration without rebuilding the image, run `bun run dev` inside `frontend/` and keep `PUBLIC_URL` / `FRONTEND_ORIGINS` aligned; the standard path is still behind Traefik.
|
||||
|
||||
## Code structure
|
||||
|
||||
- Copy naming and patterns from sibling files; do not invent new top-level folders without agreement.
|
||||
- Backend tests use Go `testing`; frontend typechecks with `tsc`.
|
||||
- Frontend calls the API mainly via `http` / `@/lib/api`.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
# With Postgres/Redis available (compose) and a correct REDIS_URL
|
||||
cd backend
|
||||
export GOTMPDIR="${GOTMPDIR:-$HOME/.cache/go-tmp}"
|
||||
go test ./internal/config/ -count=1
|
||||
go test ./internal/api/ -count=1
|
||||
# or filter:
|
||||
go test ./internal/api/ -count=1 -run 'TestPasswordRecovery|TestSessionCookie|TestAdminEvent'
|
||||
|
||||
cd ../frontend
|
||||
bunx tsc --noEmit
|
||||
```
|
||||
|
||||
Note: if Redis has a password, build `REDIS_URL=redis://:${REDIS_PASSWORD}@localhost:6379` from `.env`.
|
||||
|
||||
## Seed and sample video
|
||||
|
||||
- `SEED_CURRICULUM=true` seeds curriculum/events at startup.
|
||||
- `backend/media/sample.mp4` is tracked in git; the Docker image copies it; startup PutObjects if the key is missing.
|
||||
- Regenerate a corrupted file with `./scripts/regenerate-sample-mp4.sh` (needs ffmpeg; not a runtime dependency).
|
||||
|
||||
## UI error contract
|
||||
|
||||
- Empty list ≠ network error: show error UI + retry.
|
||||
- Mutations: `toUserError`.
|
||||
- Player: no infinite skeleton; explicit error state.
|
||||
|
||||
## Fonts
|
||||
|
||||
- Persian body: **Vazirmatn FD** (Persian digits)
|
||||
- Latin fields (password, username, captcha): regular Vazirmatn / `font-latin`
|
||||
@@ -0,0 +1,82 @@
|
||||
# Operations
|
||||
|
||||
## Health
|
||||
|
||||
```bash
|
||||
curl -k "$PUBLIC_URL/api/health" # {"status":"ok"}
|
||||
kubectl -n pargar get pods,ingressroute,svc
|
||||
kubectl -n pargar logs deploy/pargar-backend --tail=100
|
||||
```
|
||||
|
||||
## Backup
|
||||
|
||||
### Compose
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh [outdir]
|
||||
```
|
||||
|
||||
Output: `postgres.sql.gz` + RustFS data archive (when possible).
|
||||
|
||||
### Helm / k3s
|
||||
|
||||
With `backup.enabled=true`:
|
||||
|
||||
- CronJob `pargar-pg-backup` on `backup.schedule` (default daily 03:00 UTC)
|
||||
- Files on PVC `pargar-backups`
|
||||
- Auto-delete older than `backup.retentionDays`
|
||||
|
||||
Manual restore (summary):
|
||||
|
||||
```bash
|
||||
# Example: copy dump from pod/PVC then
|
||||
gunzip -c pargar-YYYYMMDD.sql.gz | psql "$DATABASE_URL"
|
||||
```
|
||||
|
||||
Back up object storage separately (bucket versioning / RustFS PVC snapshot).
|
||||
|
||||
## Metrics and tracing
|
||||
|
||||
- `GET /metrics` — without `METRICS_TOKEN` and outside DevMode → 404
|
||||
- Header `X-Metrics-Token` or `Authorization: Bearer …`
|
||||
- OTLP: `OTEL_EXPORTER_OTLP_ENDPOINT` + `OTEL_SERVICE_NAME`
|
||||
|
||||
## Updating images on k3s
|
||||
|
||||
```bash
|
||||
./scripts/k3s-load-images.sh
|
||||
./scripts/helm-k3s.sh
|
||||
kubectl -n pargar rollout restart deploy/pargar-backend deploy/pargar-frontend
|
||||
kubectl -n pargar rollout status deploy/pargar-backend
|
||||
```
|
||||
|
||||
The `local` tag uses `IfNotPresent`; always restart after import.
|
||||
|
||||
## Common troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Action |
|
||||
|---------|--------------|--------|
|
||||
| Seed video will not play | `sample.mp4` missing in storage | Check `sample media` logs; ensure file is in the image; restart backend |
|
||||
| Sudden 401 after upgrade | JWT secret changed | Do not rotate the secret unless intentional; users must log in again |
|
||||
| Backend CrashLoop after changing DB password in Secret | Old PVC password ≠ new Secret | Restore previous Secret password or re-seed DB |
|
||||
| Repeated 429 | Rate limit | Wait; honor `Retry-After`; DevMode behaves differently |
|
||||
| Empty CAPTCHA in tests | `CAPTCHA_EXPOSE_ANSWER` only with DevMode | Tests set both true |
|
||||
| Helm lint/template differs from cluster | `lookup` empty without API | Install/upgrade against a real cluster |
|
||||
| Frontend does not show changes | Stale image | Rebuild + import + rollout restart |
|
||||
|
||||
## Logging
|
||||
|
||||
- Requests with `request_id`, `user_id`, status, duration
|
||||
- Security events: login, recovery, password reset, …
|
||||
- Critical-path internals: `writeInternalErr` (log cause, generic user message)
|
||||
|
||||
## Useful scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/compose-dev.sh` | Development Compose |
|
||||
| `scripts/compose-prod.sh` | Production Compose |
|
||||
| `scripts/k3s-load-images.sh` | Build + import into k3s containerd |
|
||||
| `scripts/helm-k3s.sh` | helm upgrade --install |
|
||||
| `scripts/backup.sh` | Compose backup |
|
||||
| `scripts/regenerate-sample-mp4.sh` | Rebuild color bars with ffmpeg |
|
||||
@@ -0,0 +1,54 @@
|
||||
# Product
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Access |
|
||||
|------|--------|
|
||||
| `student` | Learning path, mentor chat, events, league, challenges, profile, certificate |
|
||||
| `mentor` | Same + curriculum/events/challenges content (staff) + student learning views |
|
||||
| `admin` | All staff + users, site settings, invites, whitelist/blacklist, chat archive, physical orders |
|
||||
|
||||
## Student journey
|
||||
|
||||
1. **Login** `/login` — self-hosted image CAPTCHA + adaptive rate limits; “remember me” lengthens refresh lifetime.
|
||||
2. **Public password recovery** — disabled for students. Password changes only from the admin panel (requires the account security answer).
|
||||
3. **Registration**
|
||||
- Public `/register` when `registration_enabled` is on; phone must be on the one-time whitelist.
|
||||
- Invite `/register/invite/[token]` with capacity limits; works even if public registration is off.
|
||||
4. **Skill tree** `/cap` — “next step”; progress and unlock requests.
|
||||
5. **Player** `/player/[lessonId]` — verified watch.
|
||||
6. **Quiz** `/quiz/[lessonId]` — wrong answers spend hearts; zero → `/lockout`.
|
||||
7. **Hub** `/unwrap` — events, chat, league, support.
|
||||
8. **Weekly league** `/leaderboard` — resets on Monday (ISO week); next reset shown in Jalali. Separate from monthly challenges.
|
||||
9. **Monthly challenge** — XP goal in a time window; progress from quiz XP; cards on league/unwrap/profile.
|
||||
10. **Profile** `/profile` — identity, stats, active challenge, certificate slot, avatar edit.
|
||||
11. **Certificate** `/c/{publicId}` — unlisted link; physical copy only if admin enabled it and payment is **offline**.
|
||||
|
||||
## Registration and phone
|
||||
|
||||
- Whitelist and blacklist are **mutually exclusive**; import resolves conflicts.
|
||||
- A phone already tied to an account cannot re-enter either list as a fresh entry.
|
||||
- One account per phone and per IP on registration paths.
|
||||
- Blacklist blocks all registration paths (public + invite) and keeps a failed-attempt history tab.
|
||||
|
||||
## Certificates
|
||||
|
||||
- `publicId` is a random 32-char hex (non-sequential).
|
||||
- Anti-crawl: `X-Robots-Tag`, meta robots, `robots.txt` for `/c/` and `/api/certificates/`.
|
||||
- Fixed slot on `/profile` (not inside the avatar dialog).
|
||||
- Physical: price/window from site settings; chart default off; no online payment gateway.
|
||||
|
||||
## Admin panel (summary)
|
||||
|
||||
- Users, role, lock/unlock, password reset (requires target security answer)
|
||||
- Chapters / lessons / questions / videos
|
||||
- Events (Jalali date picker; real `isActive`)
|
||||
- Monthly challenges (XP goal + window)
|
||||
- Membership invites, whitelist/blacklist
|
||||
- Site settings: public registration, donations, physical cert, landing sponsors
|
||||
- Chat archives (ZIP export/restore + single-use external links)
|
||||
- Physical certificate orders
|
||||
|
||||
## Support / donations
|
||||
|
||||
The community page is `chat` mode: no public payment card; coordination happens in private messages with the team.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Security
|
||||
|
||||
## Authentication and sessions
|
||||
|
||||
| Mechanism | Details |
|
||||
|-----------|---------|
|
||||
| Login | username/email + password + image CAPTCHA |
|
||||
| Access JWT | Short-lived (~15 minutes) |
|
||||
| Refresh | Hashed in DB; remember-me ≈ 30 days, short session ≈ 24 hours |
|
||||
| Browser cookies | `pargar_access` / `pargar_refresh` — `HttpOnly`, `SameSite=Lax`, `Secure` when `PUBLIC_URL` is https |
|
||||
| Frontend storage | Only `user` in `localStorage` (`pargar.auth`); tokens are not persisted |
|
||||
| API | `credentials: "include"`; optional Bearer for tests/clients |
|
||||
| Logout | Revoke refresh + clear cookies |
|
||||
|
||||
Public password recovery (`/api/auth/recovery/*` and `/recovery`) is disabled for students (404). Resets are admin-only and require the target account’s security answer.
|
||||
|
||||
## Secrets
|
||||
|
||||
- Outside `DEV_MODE`, `Validate()` rejects values like `change-me` / `local-dev-only` and JWTs shorter than 32 characters.
|
||||
- Local Helm: generate-once; production: `existingSecret` required.
|
||||
- `ADMIN_PASSWORD` only bootstraps when the admin user is missing; restarts do not overwrite the password.
|
||||
|
||||
## Proxy and CORS
|
||||
|
||||
- Only peers in `TRUSTED_PROXY_CIDRS` are trusted for `X-Forwarded-*` / `X-Real-IP`.
|
||||
- CORS: `FRONTEND_ORIGINS` + `PUBLIC_URL` with `AllowCredentials: true`.
|
||||
|
||||
## Rate limiting
|
||||
|
||||
| Scope | Approximate behavior |
|
||||
|-------|----------------------|
|
||||
| Auth (login/register/captcha/refresh/recovery) | Redis per-IP budget; tighter after failures |
|
||||
| Global `/api/*` | ~240 req/min/IP; 429 + user message + `Retry-After`; does not revoke tokens |
|
||||
| External chat export | ~10/min/IP |
|
||||
| Per-user uploads | Separately limited |
|
||||
|
||||
`/api/health` is excluded from the global ceiling.
|
||||
|
||||
## Uploads
|
||||
|
||||
| Type | Controls |
|
||||
|------|----------|
|
||||
| Admin video | Extension + MIME + magic (MP4/WebM) |
|
||||
| Avatar | JPEG/PNG/WebP via magic only; client Content-Type is not trusted |
|
||||
| Chat | Extension allow-list + sniff for image/video/audio/PDF/Office; raw public zip removed |
|
||||
|
||||
## Certificates and privacy
|
||||
|
||||
- Certificate URLs are unlisted; noindex via API, Traefik, Next, and robots.txt.
|
||||
- Chat-export tokens are single-use, revocable, and redacted from logs/metric labels.
|
||||
|
||||
## Known trade-offs (current model)
|
||||
|
||||
- SameSite=Lax + same-origin instead of a separate CSRF token (adequate for this model; revisit if FE and API split domains).
|
||||
- Donations and physical certificates have no online payment gateway — intentional.
|
||||
- XSS remains dangerous; HttpOnly cookies reduce token theft from localStorage but do not eliminate XSS.
|
||||
Reference in New Issue
Block a user