# Worksonar Mobile API v1

REST-API für die geplante native Mitarbeiter-App (iOS/Android). Separat von der
Admin-API (`/api/v1/...`) und auf Teilnehmer-Sicht zugeschnitten.

> **Basis-URL**: `https://app.worksonar.de/api/mobile/v1`
> **Content-Type**: `application/json; charset=utf-8`
> **Auth**: HMAC-signiertes JWT im `Authorization: Bearer <jwt>`-Header

---

## 1. Authentifizierung

Drei Wege, einen JWT zu erhalten (alle ohne vorherigen Token):

### 1.1 Per Participant-Token (aus Einladungs-Mail)

```http
POST /auth/by-token
Content-Type: application/json

{ "token": "PARTICIPANT_TOKEN_AUS_INVITE_URL" }
```

**Response 200**:
```json
{
  "jwt": "eyJ0eXAi...",
  "expires_at": "2026-06-19T10:30:00.000Z",
  "participant": { "id": "...", "email": "...", "name": "...", "department": "...", "location": "..." },
  "tenant":      { "id": "...", "name": "...", "slug": "...", "primary_color": "#0B3D5C" }
}
```

### 1.2 Anonyme Teilnahme per Access-Code

```http
POST /auth/by-access-code
Content-Type: application/json

{ "access_code": "CON-GBU" }
```

Liefert einen anonymen JWT (Feld `anon: true`), der nur Zugriff auf diese eine
Kampagne hat. Keine personalisierten Endpoints, keine Device-Registration.

### 1.3 Per Magic-Link (E-Mail)

Zwei-Schritt-Verfahren. Anti-Enumeration: Server bestätigt nie, ob die E-Mail
existiert.

```http
POST /auth/magic-link/request
{ "email": "max@firma.de" }
```

→ E-Mail mit 6-stelligem Code + Deep-Link (`worksonar://auth?token=...`) wird
geschickt. **Antwort immer 200 mit gleichem Body, auch wenn die E-Mail
unbekannt ist.**

```http
POST /auth/magic-link/verify
{ "code": "123456", "link_token": "..." }
```

Der 6-stellige Code allein reicht nicht aus (Brute-Force-Schutz). Die App muss
den Deep-Link-Parameter `link_token` mitsenden — der wird per `worksonar://`-
URL-Scheme aus der Mail in die App übernommen.

**Response 200**: identisch zu `/auth/by-token`.

---

## 2. Authentifizierte Endpoints

Alle Endpoints unten erwarten den Header:
```
Authorization: Bearer <jwt>
```

### 2.1 `GET /me`

Profil + aktive Einladungen. Bei anonymem JWT nur die zugehörige Kampagne.

### 2.2 `GET /campaigns/:cid`

Liefert das vollständige Survey-Schema im Mobile-tauglichen JSON-Format.
Antworten je nach Modul:

- **GBU Psyche**: `{ sections: [{ title, type, questions: [{ key, type:'likert', scale, text, labels }] }] }`
- **Benefits**: `{ profile_questions, existing, categories, catalog, category_intros }`
- **Vital/BGM**: identisch zu Benefits + `who5: [{ key, text }]`
- **Custom**: `{ type:'custom', module: <schema> }` mit Sektionen + Fragen + Optionen

Submission-Hinweise:
```json
{
  "submission_rules": {
    "anonymity_threshold": 5,
    "max_freetext_chars": 1500,
    "submit_endpoint": "/api/mobile/v1/campaigns/.../submit"
  }
}
```

### 2.3 `POST /campaigns/:cid/submit`

```http
POST /campaigns/CAMPAIGN_ID/submit
Content-Type: application/json
Authorization: Bearer <jwt>

{
  "answers": {
    "vollst_1": 4,
    "interest_sachbezugskarte": 5,
    "top1": "sachbezugskarte",
    "freetext_1": "Mehr Flexibilität bei der Arbeitszeit."
  },
  "department": "Vertrieb",
  "location": "Berlin"
}
```

- Bei nicht-anonymem JWT werden `department`/`location` aus dem
  Teilnehmer-Datensatz übernommen, der Request-Body überschreibt das nicht.
- Bei anonymem JWT (`anon: true`) werden Werte aus dem Body genutzt.
- Antwort: `{ "ok": true, "message": "..." }`.

### 2.4 Push-Notifications

```http
POST /devices/register
{
  "push_token": "fcm-token-string...",
  "platform":   "ios",          // ios | android | web
  "device_name": "iPhone von Max",
  "app_version": "1.0.3",
  "locale":      "de"
}
```

- Upsert: Bestehender Token wird aktualisiert (last_seen).
- Nur für nicht-anonyme Sessions.

```http
GET    /devices              → Liste eigener Geräte
DELETE /devices/:id          → Token widerrufen (Logout)
```

---

## 3. Fehler-Format

Einheitlich für alle Endpoints:

```json
{ "error": { "code": "<machine_code>", "message": "<human readable>", "details": {...} } }
```

| HTTP | Codes                                                            |
|------|------------------------------------------------------------------|
| 400  | `validation`                                                     |
| 401  | `unauthenticated` · `invalid_token`                              |
| 403  | `plan_restriction` · `tenant_inactive`                           |
| 404  | `not_found`                                                      |
| 422  | `campaign_inactive`                                              |
| 429  | `rate_limited` (30 Auth-Versuche/IP/Minute, Retry-After-Header)  |

---

## 4. JWT-Format (HS256 signiert)

Wir nutzen ein bewusst minimales Format ohne JWT-Header (sparen ~30 Byte pro
Token), aber kompatibel zur Standard-Verarbeitung:

```
<base64url(payload)>.<base64url(hmac-sha256(payload, secret))>
```

Payload-Felder:
```json
{
  "sub": "participant-id",     // optional bei anonymer Session
  "tid": "tenant-id",
  "anon": false,                // true bei anonymer Session
  "cid": "campaign-id",         // nur bei anonymer Session
  "iat": 1718208000,
  "exp": 1718812800             // +7 Tage
}
```

Constant-Time-Vergleich der Signatur gegen Timing-Angriffe. JWT-Secret wird
deterministisch aus `SESSION_SECRET` per HMAC abgeleitet.

---

## 5. Anonymitäts-Schutz

Identisch zur Web-API: Aggregierte Auswertungen erfordern n ≥ 5 pro Segment.
Antworten werden personenneutral gespeichert (kein Bezug zu Push-Token-Daten).
Push-Token werden ausschließlich für Notifications genutzt, nicht für
Auswertungs-Korrelation.

---

## 6. Beispiel: End-to-End-Flow

```javascript
// 1) Anmelden mit Deep-Link aus Einladungs-Mail
const auth = await fetch(API + '/auth/by-token', {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token: deepLinkParams.token }),
}).then(r => r.json());
storeJwt(auth.jwt);

// 2) Aktive Befragungen laden
const me = await fetch(API + '/me', {
  headers: { Authorization: 'Bearer ' + auth.jwt }
}).then(r => r.json());

// 3) Schema einer Befragung laden
const survey = await fetch(API + `/campaigns/${me.campaigns[0].id}`, {
  headers: { Authorization: 'Bearer ' + auth.jwt }
}).then(r => r.json());

// 4) Push-Token registrieren
await fetch(API + '/devices/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + auth.jwt },
  body: JSON.stringify({ push_token: fcmToken, platform: 'ios', app_version: '1.0.3' }),
});

// 5) Antworten submitten
await fetch(API + `/campaigns/${campaignId}/submit`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + auth.jwt },
  body: JSON.stringify({ answers: collectedAnswers }),
});
```

---

## 7. Versions-Politik

- URL-Versionierung: `/api/mobile/v1/...`
- Breaking Changes erscheinen ausschließlich in einer neuen Major-Version
  (`/api/mobile/v2/...`) mit mindestens **6 Monaten Parallelbetrieb**.
- Additive Erweiterungen (neue Felder in Responses, neue Endpoints) gelten als
  non-breaking; die App muss unbekannte Felder ignorieren.

---

## 8. Roadmap

Siehe [`docs/mobile-roadmap.md`](mobile-roadmap.md) für Phase 2 (native App MVP)
und Phase 3 (Funktionsausbau).
