docs(api): 7 fehlende Router nachgezogen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s

hours_payouts, public_stamp, ical, reseller, tenants,
special_assignments, tls_admin – seit dem letzten Doku-Update
(2026-05-24) neu dazugekommen. projects.py bewusst ausgelassen
(toter Code, nicht registriert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
This commit is contained in:
2026-09-02 23:05:51 +02:00
co-authored by Claude Sonnet 5
parent 9bdce25187
commit c1572c397d
+427
View File
@@ -22,6 +22,13 @@ In der Entwicklungsumgebung ist die interaktive Dokumentation unter `/docs` (Swa
12. [CalDAV-Integration](#caldav-integration)
13. [Busylight-Integration](#busylight-integration)
14. [Kimai-Import](#kimai-import)
15. [Stunden-Auszahlung](#stunden-auszahlung)
16. [Öffentliches QR-Stempeln](#öffentliches-qr-stempeln)
17. [iCal-Abo-Feed](#ical-abo-feed)
18. [Reseller-Selfservice](#reseller-selfservice)
19. [Admin · Mandanten & Reseller](#admin--mandanten--reseller)
20. [Sondervertretungen](#sondervertretungen)
21. [Admin · TLS](#admin--tls)
---
@@ -1373,3 +1380,423 @@ Kimai-CSV-Export vorschauen (keine DB-Änderungen).
Kimai-CSV-Import durchführen.
**Response:** `{ "time_imported": 150, "absence_imported": 12, "skipped": 3, "errors": [] }`
---
## Stunden-Auszahlung
Router-Präfix: `/hr/payouts`
Firmen-Opt-in `payout_request_enabled`: entweder bucht HR eine Auszahlung direkt
(sofort genehmigt, Saldo wird sofort abgezogen), oder ein Mitarbeiter beantragt
seine eigene Auszahlung und HR genehmigt/lehnt ab (Saldo-Abzug erst bei
Genehmigung). Alle Aktionen erzeugen einen `AuditLog`-Eintrag.
### `GET /hr/payouts`
Auszahlungen der eigenen Firma, optional gefiltert (`user_id`, `year`, `month`, `status`).
EMPLOYEE/MANAGER sehen dabei ausschließlich eigene Anträge.
- **Erforderliche Rolle:** authentifiziert (jede Rolle)
**Response:** `HoursPayoutListResponse`
---
### `POST /hr/payouts`
HR bucht eine Auszahlung direkt sofort genehmigt, Saldo wird sofort belastet.
- **Erforderliche Rolle:** HR, COMPANY_ADMIN, SUPER_ADMIN
**Response `201`:** `HoursPayoutOut`
---
### `POST /hr/payouts/request`
Mitarbeiter beantragt Auszahlung eigener Überstunden. Nur wenn die Firma
`payout_request_enabled` gesetzt hat (sonst `403`). Saldo wird erst bei
HR-Genehmigung abgezogen.
- **Erforderliche Rolle:** authentifiziert (jede Rolle)
**Response `201`:** `HoursPayoutOut`
---
### `POST /hr/payouts/{payout_id}/approve`
HR genehmigt einen offenen Antrag erst jetzt wird der Überstunden-Saldo abgezogen.
- **Erforderliche Rolle:** HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `HoursPayoutOut`
---
### `POST /hr/payouts/{payout_id}/reject`
HR lehnt einen offenen Antrag ab (kein Saldo-Abzug).
- **Erforderliche Rolle:** HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `HoursPayoutOut`
---
### `POST /hr/payouts/{payout_id}/cancel`
Antragsteller zieht seinen eigenen, noch offenen Antrag zurück.
- **Erforderliche Rolle:** authentifiziert (jede Rolle, nur eigener Antrag)
**Response:** `HoursPayoutOut`
---
### `DELETE /hr/payouts/{payout_id}`
Auszahlung stornieren. Bei bereits genehmigten/gebuchten Auszahlungen werden die
Stunden dem Saldo zurückgebucht.
- **Erforderliche Rolle:** HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `204 No Content`
---
## Öffentliches QR-Stempeln
Router-Präfix: `/time/public`
Statischer, firmenweiter QR-Code zum Aushängen (z. B. am Eingang). Mitarbeiter
scannen mit dem **eigenen** Handy und stempeln über Personalnummer + PIN.
**Kein Bearer-Token und keine Ed25519-Geräte-Signatur** (anders als der
Kiosk-Modus) ein privates Handy hat keinen Geräteschlüssel. Absicherung
stattdessen über: Opt-in pro Firma (default OFF), PIN + Lockout, IP-Rate-Limit,
gehashtes rotierbares Token in der QR-URL, 120s-Kurzsession, AuditLog. Bewusst
schwächer als der Kiosk-Pfad (kein Replay-Nonce, keine IP-Whitelist) tauscht
Sicherheit gegen BYOD-Komfort.
### `GET /time/public/company`
Firmen-Header für die Stempel-Seite (Name + `enabled`-Flag). Query-Param `t`
(Token aus dem QR-Code). Liefert auch bei deaktiviertem Feature den Namen,
damit die Seite einen Hinweis anzeigen kann.
- **Erforderliche Rolle:** öffentlich (kein Auth), rate-limited 30/min
**Response:** `PublicStampCompanyInfo`
---
### `POST /time/public/auth`
Personalnummer + PIN + Token → 120s-Session + aktueller Stempel-Status.
- **Erforderliche Rolle:** öffentlich (kein Auth), rate-limited 10/min
**Response:** `PublicStampAuthResponse`
---
### `POST /time/public/action`
Stempel-Aktion (`in`/`out`/`break_start`/`break_end`) über eine gültige
Kurz-Session ausführen.
- **Erforderliche Rolle:** öffentlich (kein Auth, gültige Session-Token nötig), rate-limited 30/min
**Response:** `PublicStampActionResponse`
---
## iCal-Abo-Feed
Router-Präfix: `/absences` (Feed) bzw. `/users/me` (Token-Verwaltung)
Abonnierbarer, read-only `text/calendar`-Feed pro Nutzer (Outlook/Apple/Google
pollen die URL selbst) Ergänzung zum CalDAV-Client, der Events aktiv nach
Nextcloud pusht. **Kein JWT**: Kalender-Apps können kein Bearer-Token schicken,
Zugang läuft ausschließlich über ein geheimes, rotierbares Token in der URL
(gehasht in `users.ical_token_hash`). Feed zeigt ausschließlich die eigenen
Abwesenheiten (APPROVED/FIRST_APPROVED/CANCELLATION_REQUESTED) des
Token-Inhabers, rein lesend.
### `GET /absences/ical/{token}.ics`
Öffentlicher Feed liefert eine `.ics`-Datei mit den bestätigten/laufenden
Abwesenheiten des zum Token gehörenden Nutzers.
- **Erforderliche Rolle:** öffentlich (kein JWT, Token in URL)
**Response:** `text/calendar`
---
### `GET /users/me/ical-token`
Status abfragen, ob der Feed aktiviert ist. Das Token selbst wird nie
zurückgegeben (nur der Hash ist gespeichert).
- **Erforderliche Rolle:** authentifiziert (jede Rolle)
**Response:** `{ "enabled": true|false }`
---
### `POST /users/me/ical-token`
Erzeugt ein neues Token (rotiert ein bestehendes) und liefert die Feed-URL
**einmalig** zurück.
- **Erforderliche Rolle:** authentifiziert (jede Rolle)
**Response:** `{ "enabled": true, "url": "https://.../api/v1/absences/ical/<token>.ics" }`
---
### `DELETE /users/me/ical-token`
Feed deaktivieren (Token löschen).
- **Erforderliche Rolle:** authentifiziert (jede Rolle)
**Response:** `204 No Content`
---
## Reseller-Selfservice
Router-Präfix: `/reseller`
Ein Reseller verwaltet ausschließlich die **von ihm selbst angelegten Firmen**
(Firmen-Ebene: Name, Slug, Plan, Aktiv-Status, erster COMPANY_ADMIN) **kein
Zugriff auf personenbezogene Zeit-/Abwesenheitsdaten** (bewusste DSGVO-Abgrenzung).
RLS über `app.reseller_id` sorgt dafür, dass eine fremde Firmen-ID ins Leere läuft (`404`).
### `GET /reseller/companies`
Eigene Firmen mit Kennzahlen.
- **Erforderliche Rolle:** RESELLER
**Response:** `list[TenantOut]`
---
### `POST /reseller/companies`
Neue Firma anlegen (inkl. erstem COMPANY_ADMIN). Ohne E-Mail: interne
Login-Kennung + einmaliges Temp-Passwort, sofort aktiv.
- **Erforderliche Rolle:** RESELLER
**Response `201`:** `TenantOut` (enthält bei Temp-Passwort-Flow `initial_password` einmalig)
---
### `PATCH /reseller/companies/{company_id}`
Eigene Firma bearbeiten. Schreibt kurzzeitig mit RLS-Bypass (nach
Python-seitiger Eigentümerprüfung), da der Reseller keine `app.company_id` besitzt.
- **Erforderliche Rolle:** RESELLER (nur eigene Firma, sonst `404`)
**Response:** `TenantOut`
---
## Admin · Mandanten & Reseller
Router-Präfix: `/admin`
Nur **SUPER_ADMIN** läuft mit vollem RLS-Bypass, sieht daher alle Firmen
firmenübergreifend.
### `GET /admin/tenants`
Alle Firmen mit Kennzahlen (mandantenübergreifend).
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `list[TenantOut]`
---
### `POST /admin/tenants`
Neue Firma anlegen, optional direkt einem Reseller zugeordnet (`reseller_id`-Query-Param).
- **Erforderliche Rolle:** SUPER_ADMIN
**Response `201`:** `TenantOut`
---
### `PATCH /admin/tenants/{company_id}`
Firma bearbeiten (Name, Plan, Aktiv-Status, ...).
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `TenantOut`
---
### `PATCH /admin/tenants/{company_id}/reseller`
Firma einem Reseller zuordnen (oder Zuordnung entfernen mit `reseller_id: null`).
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `TenantOut`
---
### `GET /admin/resellers`
Alle Reseller mit Anzahl zugeordneter Firmen.
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `list[ResellerOut]`
---
### `POST /admin/resellers`
Neuen Reseller anlegen. Mit E-Mail: Einladungs-Mail (7 Tage gültig). Ohne
E-Mail: interne Login-Kennung + einmaliges Temp-Passwort, sofort aktiv.
- **Erforderliche Rolle:** SUPER_ADMIN
**Response `201`:** `ResellerOut` (enthält bei Temp-Passwort-Flow `initial_password` einmalig)
---
### `PATCH /admin/resellers/{reseller_id}`
Reseller aktivieren/deaktivieren (`is_active`-Query-Param).
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `ResellerOut`
---
### `POST /admin/run-retention-purge`
Globaler DSGVO-Purge über **alle** Firmen (Zeiterfassung/Auszahlungen je Firma
gemäß konfigurierter Frist, plus firmenübergreifend: `audit_logs` > 3 Jahre,
abgelaufene Sessions/Password-Resets). Gleiche Logik wie der tägliche
Scheduler-Job, hier manuell für alle Mandanten auf einmal auslösbar.
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `{ "deleted": { ... } }`
---
## Sondervertretungen
Router-Präfix: `/users/{user_id}/special-assignments` (CRUD) bzw. `/reports/special-assignments` (Report)
Zeiträume mit Vertretungs-/Zuschlagsfaktor (z. B. Führungsvertretung) pro
Mitarbeiter, mit Überschneidungs-Prüfung.
### `GET /users/{user_id}/special-assignments`
Alle Zuweisungen eines Mitarbeiters.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `list[SpecialAssignmentOut]`
---
### `POST /users/{user_id}/special-assignments`
Neue Zuweisung anlegen. `409` bei Überschneidung mit vorhandener Zuweisung.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Response `201`:** `SpecialAssignmentOut`
---
### `PATCH /users/{user_id}/special-assignments/{assignment_id}`
Zuweisung bearbeiten (inkl. erneuter Überschneidungs-Prüfung).
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `SpecialAssignmentOut`
---
### `DELETE /users/{user_id}/special-assignments/{assignment_id}`
Zuweisung löschen.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `204 No Content`
---
### `GET /reports/special-assignments/payroll`
Payroll-Report: je Mitarbeiter die Sondervertretungs-Stunden im gewählten
Monat (`year`, `month`), inkl. Normal-/Faktor-/Zuschlagsstunden auf Basis
genehmigter Zeiterfassungs-Einträge.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `PayrollAssignmentReport`
---
## Admin · TLS
Router-Präfix: `/admin/tls`
**Server-lokal, ohne Mandanten-Kontext** zeigt/verwaltet ausschließlich das
TLS-Zertifikat des Servers, auf dem der Request ankommt (kein
Cross-Server-Wissen zwischen 137/164). Läuft mit Root-Rechten
(`timemaster.service` läuft als `root`), deshalb strikt auf SUPER_ADMIN
begrenzt, plus AuditLog und Rate-Limit auf den Renewal-Endpunkten.
### `GET /admin/tls/status`
Aktueller Zertifikats-Status (Modus, Domain, gültig bis, erneuerbar).
- **Erforderliche Rolle:** SUPER_ADMIN
**Response:** `TlsStatusOut`
---
### `POST /admin/tls/renew/certbot`
Zertifikat per Certbot erneuern (öffentliche Domain).
- **Erforderliche Rolle:** SUPER_ADMIN, rate-limited 5/h
**Request:** `{ "domain": "timemaster.example.com" }`
**Response:** `TlsStatusOut`
---
### `POST /admin/tls/renew/internal`
Internes Zertifikat erneuern (Hostname/IP, kein öffentlicher Certbot-Weg).
- **Erforderliche Rolle:** SUPER_ADMIN, rate-limited 5/h
**Request:** `{ "hostname": "timemaster", "ip": "192.168.1.137" }`
**Response:** `TlsStatusOut`