# M-TEER API Reference

All endpoints are versioned under `/api/v1` and return the same envelope:

```json
{
  "success": true,
  "message": "Success",
  "data": {},
  "meta": {}
}
```

Errors use the same shape with `success: false`, a human-readable `message`,
and an `errors` array for validation failures.

There are two surfaces:

| Surface | Prefix | Auth | Consumer |
| --- | --- | --- | --- |
| Public | `/api/v1/public`, `/api/v1/{progress,drawings,notes,favorites,cases,quiz,sessions,annotations}` | none | Flutter app |
| Admin | `/api/v1/admin/**` | JWT bearer + permission | React admin panel |

Only `PUBLISHED` content is ever returned on the public surface.

---

## Authentication (admin)

| Method | Path | Permission | Notes |
| --- | --- | --- | --- |
| POST | `/admin/auth/login` | – | 5 requests/min. Returns `accessToken`, `refreshToken`, and the user's resolved permissions. |
| POST | `/admin/auth/refresh` | – | Rotates the refresh token; the presented token is revoked. |
| POST | `/admin/auth/logout` | authenticated | Revokes the supplied token, or every session for the user. |
| GET | `/admin/auth/me` | authenticated | Current profile, roles and permissions. |
| POST | `/admin/auth/forgot-password` | – | Always returns success; never discloses whether an account exists. |
| POST | `/admin/auth/reset-password` | – | Consumes a reset token and revokes all sessions. |
| POST | `/admin/auth/change-password` | authenticated | Requires the current password. |

Access tokens are short lived (`JWT_EXPIRES_IN_SECONDS`, default 1 hour).
Refresh tokens are stored only as SHA-256 hashes.

---

## Content management (admin)

Sections, modules, lessons, activities and content blocks follow the same CRUD
shape. `POST /reorder` accepts `{ "items": [{ "id": 1, "orderIndex": 0 }] }` and
applies the whole ordering in a single transaction.

| Resource | Base path | Permissions |
| --- | --- | --- |
| Sections | `/admin/sections` | `section.view/create/update/delete/publish` |
| Modules | `/admin/modules` | `module.*` |
| Lessons | `/admin/lessons?moduleId=` | `lesson.*` |
| Activities | `/admin/activities?lessonId=` | `activity.*` |
| Content blocks | `/admin/content-blocks?activityId=` | `activity.*` |
| Quick reference | `/admin/quick-reference` | `section.*` |
| Questions | `/admin/questions?activityId=` | `activity.*` |
| Procedure steps | `/admin/procedure-steps?activityId=` | `activity.*` |
| Cases | `/admin/cases` | `case.*` |
| Decision trees | `/admin/decision-trees` | `decision_tree.*` |
| Media | `/admin/media` | `media.view/upload/update/delete` |
| Drawing templates | `/admin/drawing-templates` | `media.*` |

### Type catalogues

`GET /admin/activities/types` and `GET /admin/content-blocks/types` return the
supported type identifiers so the admin panel builds its pickers from the API
rather than a hard-coded list.

### Publishing workflow

`status` cannot be set through `PUT`. It changes only through these routes, each
of which requires the matching `*.publish` permission:

```
POST /admin/modules/:id/submit-review     DRAFT      -> IN_REVIEW
POST /admin/modules/:id/publish           any        -> PUBLISHED  (creates a version)
POST /admin/modules/:id/unpublish         PUBLISHED  -> DRAFT
POST /admin/modules/:id/archive           any        -> ARCHIVED
```

Publishing snapshots the complete module tree into `module_versions` with a
version number, the publishing user and an optional `changeReason`. Previous
published versions are marked `ARCHIVED` but retained.

| Method | Path | Notes |
| --- | --- | --- |
| GET | `/admin/modules/:id/versions` | Version history. |
| GET | `/admin/modules/:id/versions/preview` | The exact payload the mobile app would render, including drafts. |
| GET | `/admin/modules/:id/versions/:versionId` | A stored snapshot. |

### Decision tree graph

`PUT /admin/decision-trees/:id/graph` replaces the whole graph atomically:

```json
{
  "nodes": [
    { "key": "start", "nodeType": "START", "payload": { "title": "Begin" } },
    { "key": "q1", "nodeType": "QUESTION", "payload": { "question": "..." } }
  ],
  "edges": [{ "fromKey": "start", "toKey": "q1", "label": "Continue" }]
}
```

Nodes are addressed by a client-supplied `key` so edges can be wired before the
rows exist. The API rejects a graph without exactly one `START` node, one with
duplicate keys, or one with an edge pointing at a node that does not exist.

---

## Administration

| Method | Path | Permission |
| --- | --- | --- |
| GET/POST/PUT | `/admin/users` | `user.view/create/update` |
| POST | `/admin/users/:id/roles` | `user.update` (cannot target yourself) |
| POST | `/admin/users/:id/password` | `user.update` (revokes that user's sessions) |
| POST | `/admin/users/:id/{activate,deactivate}` | `user.update` |
| GET/POST/PUT/DELETE | `/admin/roles` | `role.*` |
| GET | `/admin/permissions`, `/admin/permissions/grouped` | `role.view` |
| GET | `/admin/audit-logs` | `audit.view` |
| GET | `/admin/analytics/dashboard` | `dashboard.view` |
| GET | `/admin/analytics/*` | `analytics.view` |
| GET/PUT | `/admin/settings` | `settings.view` / `settings.update` |

Every mutating request under `/admin` is written to the audit log with the
acting user, entity, request body and source address. Passwords and tokens are
redacted before storage.

---

## Public content (mobile)

| Method | Path | Notes |
| --- | --- | --- |
| GET | `/public/manifest` | Content fingerprint plus per-module `updatedAt`, for offline diffing. |
| GET | `/public/settings` | Whitelisted settings only (disclaimer, support email, cache TTL). |
| GET | `/public/sections` | Published sections. |
| GET | `/public/sections/:id` | Section with its published modules. |
| GET | `/public/modules?sectionId=` | Published modules. |
| GET | `/public/modules/:id` | **Full tree**: lessons → activities → content blocks, plus questions, procedure steps and any embedded decision tree. |
| GET | `/public/lessons/:id` | Lesson with hydrated activities. |
| GET | `/public/activities/:id` | Single hydrated activity. |
| GET | `/public/decision-trees/:id` | Published tree with nodes and edges. |
| GET | `/public/quick-reference` | Published quick reference items. |
| GET | `/public/quick-reference/:slug` | One item. |
| GET | `/public/search?q=` | Modules, lessons, activities and reference. Minimum two characters. |

Quiz payloads on this surface omit `isCorrect` and `explanation`: the answer key
never reaches the device.

---

## Learner data (mobile, anonymous)

There is no login. Ownership is established by `anonymousSessionId`, and the API
rejects any attempt to read or modify another session's data.

| Method | Path | Notes |
| --- | --- | --- |
| POST | `/sessions/register` | Creates or refreshes the anonymous device session. |
| POST | `/progress` | Upsert a single entry; merges with what is already stored. |
| POST | `/progress/sync` | Batch upload of everything captured offline. |
| GET | `/progress/summary?sessionId=` | Server-computed section and module percentages. |
| POST/GET/PUT/DELETE | `/drawings` | One drawing per (session, activity); re-saving updates rather than duplicates. |
| POST/GET/PUT/DELETE | `/annotations` | Stored separately from the source image. |
| POST/GET/PUT/DELETE | `/notes` | |
| POST | `/favorites/toggle` | Idempotent star/unstar. |
| GET | `/cases`, `/cases/published` | The learner's own case log, plus published teaching cases. |
| POST | `/quiz/submit` | Server-side grading; returns per-question verdicts and explanations. |

---

## Security

- JWT access tokens plus rotating, hashed refresh tokens.
- `bcrypt` password hashing (12 rounds).
- Rate limiting globally, with tighter limits on login and password reset.
- `helmet` security headers and an explicit CORS allowlist.
- DTO validation with `whitelist` and `forbidNonWhitelisted`, so unknown fields
  are rejected rather than silently ignored.
- Authorisation resolved from the database on every request; permissions
  presented by the client are never trusted.
- Upload MIME allowlist, size limit and server-generated filenames.
