Auth — API key
Wszystkie endpointy /api/v1/clinic/* wymagają API key Bearer z odpowiednim
scope. Klucze są generowane per klinika w panelu Klinika → API Keys.
Format klucza
pk_live_XXXXXXXX_<secret>
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
│ │ └─ 32-znakowy losowy secret (verify argon2)
│ └─ 8-znakowy publiczny prefix (lookup w bazie + audit log)
└─ environment marker (zawsze `pk_live_`)
- Prefix widoczny w audit logu, panelu klinki, error messages
- Secret — zhashowany argon2 w bazie, plaintext widzisz tylko raz przy create
- Klucz testowy (
pk_test_*) — nie zaimplementowane w MVP; użyj dev env
Wysyłanie klucza
W headerze Authorization:
POST /api/v1/clinic/doctors HTTP/1.1
Host: api.pandoktorek.pl
Authorization: Bearer pk_live_XXXXXXXX_<secret>
Content-Type: application/json
:::warning NIE w URL
Klucze NIGDY w query params (?api_key=...). To naruszenie G-AUTH-004 — klucze
trafiają do logów serwerów / proxy / browser history.
:::
Scopes
Klucz dostaje listę scopes przy create. Każdy endpoint wymaga konkretnego scope — 401 jeśli brak.
| Scope | Endpoints |
|---|---|
doctors:write | POST /api/v1/clinic/doctors |
services:write | POST /api/v1/clinic/services |
slots:write | POST /api/v1/clinic/slots, DELETE /api/v1/clinic/slots/{external_id} |
bookings:read | (Faza 2) — pull rezerwacji ze stanem |
webhooks:manage | (Faza 2) — CRUD webhook subscriptions przez API |
Zasada minimum: generuj klucz tylko z tymi scope'ami, które są ci potrzebne.
Push slotów = klucz tylko z slots:write. Pełen ingest = trzy scope'y.
Lifecycle
Generate
Panel → API Keys → Nowy klucz
└─► nazwa (do identyfikacji)
└─► scopes (lista checkbox)
└─► [Zapisz]
└─► WYŚWIETL plaintext (`pk_live_XXX_secret`) — TYLKO RAZ
└─► copy to clipboard
└─► zapisz w secrets manager / .env / vault
Revoke
Panel → API Keys → klucz → Revoke. Status revoked_at != NULL → kolejne
requesty zwracają 401. Aktywne sesje webhook delivery nie są przerywane —
osobny system.
Rotation
Brak auto-rotation w MVP. Best practice ręczna rotacja co 90 dni:
- Generuj nowy klucz z tymi samymi scopes
- Update
.envw swoim systemie + redeploy - Verify że requests idą z nowym kluczem (audit log → filter prefix)
- Revoke stary
last_used_at na liście kluczy pomaga wykryć martwe klucze (G-DEV — security panel
flag'uje klucze niewykorzystane >90 dni jako "dormant").
Bezpieczeństwo
| Reguła | Status w MVP |
|---|---|
| Plaintext widoczny tylko raz przy create | ✅ |
| argon2 hash w DB | ✅ |
| Per-key scopes | ✅ |
| Per-key audit log (last_used_at) | ✅ |
| Rate limit per klucz (10000 req/dobę) | ✅ |
Authorization: Bearer header only (NIE URL) | ✅ |
Block po subscription.past_due >7 dni | ✅ (G-FIN-004) |
| Auto-rotation | ❌ (Faza 2) |
| Webhook signing key (osobny od API key) | ✅ — generowany per subscription |
Błędy auth
| Status | Detail | Co robić |
|---|---|---|
| 401 | Wymagana autoryzacja API key | Brak headera Authorization |
| 401 | Nieprawidłowy format klucza API | Klucz nie zaczyna się od pk_live_ lub ma zły format |
| 401 | Klucz API nieprawidłowy lub wycofany | Klucz revoked, expired albo secret się nie zgadza |
| 401 | Brak uprawnień (scope) | Klucz nie ma wymaganego scope dla tego endpointu |
| 402 | Subskrypcja kliniki została anulowana | Stripe billing — clinic_admin musi reaktywować |
| 402 | Płatność za subskrypcję zaległa >7 dni | past_due grace period minął |
| 429 | Rate limit exceeded (X req/dobę) | Wyczerpałeś dobową pulę — patrz rate limits |