diff --git a/docs/api.md b/docs/api.md index e774d4d..eb68140 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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/.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`