Files
pwa/backend/README.md
ar.badieiandCursor 8c37c15697 Fix minimum age 12 and ensure assignment catalog seeds on startup.
Persian age validation messages, clearer homework entry points, and seed assignment definitions so «تکالیف من» is not empty after deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 07:24:40 +03:30

207 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Novya Backend
Independent HTTP API for the Novya learning PWA. Authenticated progress, PCQ-24 assessments, course sessions, chat, and AI coach run through this service; MySQL is the source of truth.
## Tech Stack
- **Runtime:** Node.js 20+ + TypeScript (ESM)
- **HTTP:** Hono
- **Database:** MySQL 8 via Prisma (`backend/prisma/schema.prisma`)
- **Validation:** Zod
- **Auth:** Argon2 passwords, opaque Bearer sessions (SHA-256 hashed at rest)
- **Logging:** Pino
- **AI:** OpenAI-compatible client (default GapGPT via `OPENAI_BASE_URL`)
## Prerequisites
- Node.js 20+
- npm
- MySQL 8+ (local or `docker compose -f docker-compose.dev.yml up -d`)
## Getting Started
```bash
cd backend
npm install
cp .env.example .env
# docker compose -f docker-compose.dev.yml up -d
npm run prisma:migrate:dev
npm run prisma:seed
npm run dev
```
API default: `http://localhost:3001`
PWA (CORS): `http://localhost:5173`
`prisma:seed` upserts the 11 course sessions and 9 assignment definitions. Server startup also upserts both as a safety net.
## Scripts
| Script | Description |
|--------|-------------|
| `npm run dev` | Dev server with hot reload |
| `npm run build` | Compile TypeScript → `dist/` |
| `npm run start` | Run compiled server |
| `npm run test` | Domain + e2e API tests |
| `npm run lint` | ESLint |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run prisma:generate` | Generate Prisma Client |
| `npm run prisma:validate` | Validate schema |
| `npm run prisma:migrate:dev` | Dev migrations |
| `npm run prisma:migrate:deploy` | Apply migrations (prod) |
| `npm run prisma:seed` | Upsert 11 `course_sessions` + 9 assignment definitions |
## Project Structure
```
backend/
├── prisma/
│ ├── schema.prisma # Active schema (identity + learning domain)
│ ├── seed.ts # Catalog seed (11 sessions)
│ └── migrations/
├── src/
│ ├── app.ts / server.ts # App factory + listen / shutdown
│ ├── config/ # Env (Zod)
│ ├── lib/ # prisma, logger, response, clientIp
│ ├── middleware/ # requestId, CORS, rate-limit, auth, errors
│ ├── routes/ # health + module mounts
│ ├── modules/
│ │ ├── auth/ # register, login, guest, me, logout
│ │ ├── users/ # profile name/age
│ │ ├── journey/ # learning step + permissions
│ │ ├── assessments/ # PCQ-24 attempts + scoring
│ │ ├── sessions/ # catalog, progress, complete
│ │ ├── chat/ # chat sessions + messages
│ │ └── ai/ # config, runs, coach prompt/client
│ └── shared/ # errors, types, validation, constants
├── tests/
├── docker-compose.dev.yml
└── .env.example
```
Root `prisma/schema.prisma` is a **legacy design artifact** — see `../prisma/README.md`. Use only `backend/prisma/`.
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NODE_ENV` | No | `development` | `development`, `test`, `production` |
| `PORT` | No | `3001` | HTTP port |
| `HOST` | No | `0.0.0.0` | Bind host |
| `API_VERSION` | No | `1.0.0` | Returned by `/version` |
| `BUILD_TIMESTAMP` | No | — | Optional build stamp for `/version` |
| `DATABASE_URL` | **Yes** | — | `mysql://...` (use `novya`; keep `novya_test` for tests only) |
| `LOG_LEVEL` | No | `info` | Pino level |
| `CORS_ORIGIN` | No | `http://localhost:5173` | Comma-separated origins |
| `TRUST_PROXY` | No | `false` | Proxy-aware client IP |
| `RATE_LIMIT_WINDOW_MS` | No | `60000` | Rate-limit window |
| `RATE_LIMIT_MAX` | No | `100` | Max requests / window / IP |
| `AUTH_RATE_LIMIT_MAX` | No | `20` | Cap for register/login/guest |
| `AUTH_SESSION_TTL_HOURS` | No | `168` | Opaque session TTL (7 days); no refresh tokens |
| `OPENAI_API_KEY` | For AI | `""` | Backend-only; empty → AI routes return 503 |
| `OPENAI_BASE_URL` | No | `https://api.gapgpt.app/v1` | OpenAI-compatible base URL |
| `OPENAI_MODEL` | No | `gpt-4o-mini` | Model id |
| `OPENAI_ORG_ID` | No | — | Optional OpenAI org header |
| `OPENAI_PROJECT_ID` | No | — | Optional OpenAI project header |
Do **not** put `VITE_*` secrets in the backend.
## Auth
- Bearer token from register/login/guest → header `Authorization: Bearer <token>`
- Token stored hashed in `auth_sessions`; revoke on logout or expiry
- Profile age must be **at least 12** (no upper age limit)
- One PCQ attempt per user per phase (`UNIQUE(userId, phase)`)
## Learning journey (`currentStep`)
Timestamps are authoritative. Derived steps:
`registered` → `pre_test_done` / `training_in_progress` → `training_completed` → `completed`
Permissions on `GET /journey`: `canTakePreTest`, `canOpenTraining`, `canTakePostTest`, `isCompleted`.
`canTakePostTest` is true after pre-test when either training is fully completed **or** the learner has completed sessions 1–10 (reached Session 11), and the post-test is not yet submitted.
## API Endpoints
### Health
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/health` | No | Liveness |
| `GET` | `/ready` | No | Readiness (MySQL) |
| `GET` | `/version` | No | Version info |
### Auth & users
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `POST` | `/auth/register` | No | Email/password (min 8) |
| `POST` | `/auth/login` | No | Login |
| `POST` | `/auth/guest` | No | Guest user + session |
| `GET` | `/auth/me` | Yes | Current user |
| `POST` | `/auth/logout` | Yes | Revoke session |
| `GET` | `/users/me` | Yes | User + profile |
| `PATCH` | `/users/me/profile` | Yes | Name + age |
### Journey & assessments
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/journey` | Yes | Step + permissions |
| `GET` | `/assessments/scales/pcq24` | Yes | Scale metadata |
| `POST` | `/assessments/attempts` | Yes | Submit pre/post |
| `GET` | `/assessments/attempts` | Yes | History |
| `GET` | `/assessments/attempts/latest?phase=` | Yes | Latest by phase |
| `GET` | `/assessments/summary` | Yes | Pre/post + journey |
### Sessions (catalog)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/sessions` | Yes | Catalog + unlock/complete flags |
| `GET` | `/sessions/progress` | Yes | Progress summary |
| `GET` | `/sessions/:order` | Yes | One session |
| `POST` | `/sessions/:order/complete` | Yes | Mark complete (idempotent) |
### Chat & AI
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/chat/sessions` | Yes | List chats (`?sessionOrder=` optional) |
| `POST` | `/chat/sessions` | Yes | Create/reuse chat for a session order |
| `GET` | `/chat/sessions/:id/messages` | Yes | Message history |
| `POST` | `/chat/sessions/:id/messages` | Yes | User message → coach reply |
| `POST` | `/chat/sessions/:id/start` | Yes | Opening coach turn |
| `GET` | `/ai/config` | No | Public AI config (no secrets) |
| `GET` | `/ai/runs` | Yes | User’s AI run log |
Response envelope:
```json
{ "success": true|false, "data"|"error": ..., "meta": { "requestId", "timestamp" } }
```
## Schema enums (MVP usage)
| Enum | Values | Runtime usage |
|------|--------|----------------|
| `AttemptStatus` | `submitted`, `scored` | Always `scored`. `submitted` reserved for future drafts. |
| `AiRunStatus` | `queued`, `running`, `succeeded`, `failed` | Create as `queued` → `running` → terminal |
| `SessionProgressStatus` | `not_started`, `in_progress`, `completed` | Chat opens as `in_progress`; `not_started` is default only |
| `ChatRole` | `system`, `user`, `assistant` | Only `user`/`assistant` persisted; `system` is prompt-only |
## Catalog integrity
`session_progress.sessionOrder` and `chat_sessions.sessionOrder` FK → `course_sessions.order` (`ON DELETE RESTRICT`). Seed the catalog before creating progress/chat rows (`npm run prisma:seed`).
## Deferred (not in this API)
OAuth, phone OTP, guest→account merge, refresh tokens, versioned PCQ tables in DB, production-grade distributed rate limiting.
## Related docs
- [`BACKEND_DATABASE_ARCHITECTURE_SUMMARY.md`](../BACKEND_DATABASE_ARCHITECTURE_SUMMARY.md) — architecture overview
- Auth history: `BACKEND_AUTH_FOUNDATION_REPORT.md` (may be partially historical)