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>
207 lines
8.3 KiB
Markdown
207 lines
8.3 KiB
Markdown
# 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)
|