Files
timemaster/docs/api.md
T
patrickandClaude Sonnet 5 c1572c397d
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
docs(api): 7 fehlende Router nachgezogen
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
2026-09-02 23:05:51 +02:00

1803 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TimeMaster API-Referenz
Alle Endpunkte sind unter dem Präfix `/api/v1` erreichbar.
Authentifizierung: `Authorization: Bearer <access_token>` (JWT, 30 min gültig).
In der Entwicklungsumgebung ist die interaktive Dokumentation unter `/docs` (Swagger UI) und `/redoc` verfügbar.
---
## Inhaltsverzeichnis
1. [System](#system)
2. [Authentifizierung & 2FA](#authentifizierung--2fa)
3. [Benutzer](#benutzer)
4. [Firmen & Abteilungen](#firmen--abteilungen)
5. [Zeiterfassung](#zeiterfassung)
6. [Abwesenheiten](#abwesenheiten)
7. [Dashboard & Reports](#dashboard--reports)
8. [Audit-Log](#audit-log)
9. [Kiosk-Geräte](#kiosk-geräte)
10. [LDAP-Integration](#ldap-integration)
11. [SMTP-Konfiguration](#smtp-konfiguration)
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)
---
## Rollen-Hierarchie
| Rolle | Beschreibung |
|-------|-------------|
| `SUPER_ADMIN` | Systemadministrator, sieht alle Firmen, umgeht RLS |
| `COMPANY_ADMIN` | Firmenadministrator, verwaltet alles in seiner Firma |
| `HR` | Personalverwaltung, genehmigt Abwesenheiten, sieht alle Mitarbeiter |
| `MANAGER` | Teamleiter, genehmigt Einträge/Abwesenheiten seiner Mitarbeiter |
| `EMPLOYEE` | Normaler Mitarbeiter, sieht nur eigene Daten |
---
## System
### `GET /health`
Gesundheitsprüfung ohne Authentifizierung.
**Response:**
```json
{ "status": "ok", "app": "TimeMaster", "env": "production" }
```
---
## Authentifizierung & 2FA
Router-Präfix: `/auth`
### `POST /auth/register`
Neue Firma + Admin-Konto anlegen.
- **Rate-Limit:** 3/Stunde
- **Erforderliche Rolle:** keine (öffentlich)
**Request:**
```json
{
"company_name": "Acme GmbH",
"email": "admin@acme.de",
"password": "Sicher123",
"first_name": "Max",
"last_name": "Mustermann"
}
```
**Response `201`:** `TokenResponse` (siehe unten)
**Fehler:**
- `400` E-Mail bereits registriert
- `429` Rate-Limit überschritten
---
### `POST /auth/login`
Einloggen mit E-Mail + Passwort. Gibt bei aktivem TOTP einen `partial_token` zurück.
- **Rate-Limit:** 10/Minute
- **Erforderliche Rolle:** keine
**Request:**
```json
{ "email": "user@example.de", "password": "Passwort123" }
```
**Response:** `TokenResponse`
```json
{
"access_token": "<jwt>",
"refresh_token": "<opaque>",
"token_type": "bearer",
"requires_totp": false,
"partial_token": null
}
```
Wenn `requires_totp: true`, wird `partial_token` zurückgegeben und `access_token`/`refresh_token` sind leer. Anschließend `POST /auth/totp/login` aufrufen.
**Fehler:**
- `401` Ungültige Anmeldedaten
- `403` Konto deaktiviert
---
### `POST /auth/refresh`
Access Token erneuern (Token Rotation der alte Refresh Token wird dabei ungültig).
**Request:** `{ "refresh_token": "..." }`
**Response:** `TokenResponse`
**Fehler:** `401` Ungültiger oder abgelaufener Token
---
### `POST /auth/logout`
Sitzung beenden (Refresh Token invalidieren).
**Request:** `{ "refresh_token": "..." }`
**Response:** `{ "message": "Logged out successfully" }`
---
### `POST /auth/password-reset`
Passwort-Reset anfordern (Link per E-Mail).
- **Rate-Limit:** 3/Stunde
- **Erforderliche Rolle:** keine
**Request:** `{ "email": "user@example.de" }`
**Fehler:** `400` Konto wird über LDAP verwaltet (Passwort-Reset nicht möglich)
---
### `POST /auth/password-reset/confirm`
Neues Passwort setzen mit Reset-Token aus der E-Mail.
- **Rate-Limit:** 5/Stunde
**Request:**
```json
{ "token": "<reset-token>", "new_password": "NeuesPasswort123" }
```
**Fehler:** `400` Token ungültig/abgelaufen, Passwort zu schwach
---
### `POST /auth/invite/accept`
Einladungstoken einlösen und Konto aktivieren.
**Request:**
```json
{
"token": "<invite-token>",
"password": "MeinPasswort1",
"first_name": "Anna",
"last_name": "Beispiel"
}
```
**Response:** `UserOut`
---
### `GET /auth/me`
Eigenes Profil abrufen.
- **Erforderliche Rolle:** beliebig (eingeloggt)
**Response:** `UserOut`
---
### `POST /auth/change-password`
Passwort ändern (benötigt aktuelles Passwort).
- **Erforderliche Rolle:** beliebig
**Request:**
```json
{ "current_password": "Alt123", "new_password": "Neu456" }
```
**Validierung:** min. 8 Zeichen, 1 Großbuchstabe, 1 Ziffer
---
### TOTP / Zwei-Faktor-Authentifizierung
#### `POST /auth/totp/setup`
TOTP-Secret generieren und `otpauth://` URI für den Authenticator zurückgeben. Noch nicht aktiviert.
**Response:** `{ "secret": "BASE32...", "otpauth_uri": "otpauth://..." }`
---
#### `POST /auth/totp/setup/save`
Generiertes Secret in DB persistieren (ohne Aktivierung).
---
#### `POST /auth/totp/confirm`
Ersten Code bestätigen und 2FA aktivieren.
**Request:** `{ "code": "123456" }`
**Fehler:** `400` Kein Secret vorhanden / Ungültiger Code
---
#### `POST /auth/totp/disable`
2FA deaktivieren. Benötigt aktuelles Passwort + gültigen TOTP-Code.
**Request:**
```json
{ "password": "MeinPasswort", "code": "123456" }
```
---
#### `POST /auth/totp/login`
Zweiter Login-Schritt bei aktivem TOTP: `partial_token` + TOTP-Code → vollständige Tokens.
- **Rate-Limit:** 10/Minute
**Request:**
```json
{ "partial_token": "<partial-jwt>", "code": "123456" }
```
**Response:** `TokenResponse`
**Fehler:** `401` Ungültiger partial_token | `400` Ungültiger Code
---
## Benutzer
Router-Präfix: `/users`
### `GET /users/`
Alle Benutzer der eigenen Firma auflisten.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
- **Query-Parameter:**
- `skip` (int, default 0)
- `limit` (int, default 50, max 500)
- `active_only` (bool, default true)
- `search` (str, max 100) sucht in Name, E-Mail und Personalnummer
**Response:**
```json
{ "total": 42, "items": [ /* UserOut */ ] }
```
---
### `POST /users/invite`
Neuen Benutzer einladen (E-Mail mit Einladungslink).
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Request:**
```json
{
"email": "mitarbeiter@example.de",
"first_name": "Maria",
"last_name": "Muster",
"role": "EMPLOYEE",
"department_id": null,
"personnel_number": "0042"
}
```
**Response `201`:** `UserOut`
**Fehler:** `400` E-Mail bereits vergeben | `422` Personalnummer bereits belegt
---
### `GET /users/me`
Eigenes Profil (identisch mit `GET /auth/me`).
---
### `GET /users/next-personnel-number`
Nächste freie Personalnummer vorschlagen (Counter wird nicht erhöht).
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Response:** `{ "next": "0043" }`
---
### `GET /users/by-personnel/{number}`
Benutzer per Personalnummer suchen (Lookup für externe Integrationen).
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Fehler:** `404` Nicht gefunden
---
### `GET /users/import-template.csv`
CSV-Vorlage für Massen-Import herunterladen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Response:** CSV-Datei
---
### `POST /users/import/preview`
CSV-Import vorschauen (keine DB-Änderungen).
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
- **Content-Type:** `multipart/form-data`, Feld `file`
**Response:** `UserImportResult` mit Zeilen-Status (create/reactivate/error)
---
### `POST /users/import/apply`
CSV-Import durchführen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Verhalten:**
- E-Mail existiert aktiv → Fehler
- E-Mail existiert, aber deaktiviert → Reaktivieren mit neuen Daten
- Leere Personalnummer → Auto-Vergabe
---
### `GET /users/{user_id}`
Einzelnen Benutzer abrufen.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
---
### `PATCH /users/{user_id}`
Benutzer bearbeiten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Editierbare Felder:** `first_name`, `last_name`, `role`, `department_id`, `work_schedule_id`, `kuerzel`, `personnel_number`, `can_manual_time_entry`
---
### `POST /users/{user_id}/deactivate`
Benutzer deaktivieren (kein Hard-Delete, `is_active = false`, Personalnummer bleibt reserviert).
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### `POST /users/{user_id}/reactivate`
Deaktivierten Benutzer reaktivieren.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### `POST /users/{user_id}/kiosk-pin`
Kiosk-PIN setzen. Eigene PIN: jeder Benutzer. Fremde PIN: COMPANY_ADMIN/SUPER_ADMIN.
**Request:** `{ "pin": "1234" }`
---
## Firmen & Abteilungen
Router-Präfix: `/companies`
### `GET /companies/me`
Eigene Firmen-Daten abrufen.
- **Erforderliche Rolle:** beliebig
**Response:** `CompanyOut`
---
### `PATCH /companies/me`
Firma bearbeiten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Editierbare Felder:** `name`, `country`, `state`, `logo_url`, `settings`, `personnel_number_required`, `personnel_number_mode`, `sick_note_required_after_days`
---
### `GET /companies/me/departments`
Alle Abteilungen auflisten.
- **Erforderliche Rolle:** beliebig
---
### `POST /companies/me/departments`
Neue Abteilung anlegen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Request:** `{ "name": "Entwicklung", "description": null }`
---
### `PATCH /companies/me/departments/{dept_id}`
Abteilung bearbeiten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### `DELETE /companies/me/departments/{dept_id}`
Abteilung löschen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Fehler:** `404` Nicht gefunden
---
## Zeiterfassung
Router-Präfix: `/time`
### Stempeluhr
#### `POST /time/stamp-in`
Einstempeln startet einen neuen Zeiterfassungseintrag.
- **Erforderliche Rolle:** beliebig
**Request:**
```json
{
"source": "web",
"project_id": null,
"note": "Projekt X"
}
```
**Response:** `TimeEntryWithWarnings`
```json
{
"entry": { /* TimeEntryOut */ },
"warnings": ["Bei mehr als 6h Anwesenheit sind mind. 30 min Pause vorgeschrieben ArbZG §4"]
}
```
**Fehler:** `409` Bereits eingestempelt
---
#### `POST /time/stamp-out`
Ausstempeln schließt den offenen Eintrag. Enthält ArbZG-Warnungen falls zutreffend.
**Request:** `{ "note": null }`
**Fehler:** `404` Kein offener Eintrag
---
#### `POST /time/break-start`
Pause beginnen.
**Fehler:** `404` Kein offener Eintrag | `409` Bereits in Pause
---
#### `POST /time/break-end`
Pause beenden.
---
### Einträge
#### `GET /time/today`
Alle Einträge des heutigen Tages des eingeloggten Benutzers.
**Response:** `list[TimeEntryOut]`
---
#### `GET /time/entries`
Zeiterfassungseinträge auflisten.
- **Query-Parameter:**
- `user_id` (UUID, optional) EMPLOYEE sieht nur eigene
- `date_from` / `date_to` (date)
- `status` (`pending` | `approved` | `rejected`)
- `skip` / `limit` (int)
**Response:** `{ "total": int, "items": [TimeEntryOut] }`
---
#### `POST /time/entries`
Manuellen Eintrag anlegen.
- Mitarbeiter nur mit Berechtigung `can_manual_time_entry`
- MANAGER/HR/ADMIN können `user_id` setzen
**Request:**
```json
{
"date": "2026-05-23",
"start_time": "08:00",
"end_time": "17:00",
"break_minutes": 30,
"note": "Nacherfassung"
}
```
**Response:** `TimeEntryWithWarnings`
---
#### `PATCH /time/entries/{entry_id}`
Eintrag korrigieren. Mitarbeiter: nur eigene ausstehende. Manager: alle der Company.
**Request:** `{ "start_time": "08:30", "correction_note": "..." }`
---
#### `POST /time/entries/{entry_id}/approve`
Eintrag genehmigen. Self-Approval-Schutz: Eigene Einträge können nicht selbst genehmigt werden.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
---
#### `POST /time/entries/{entry_id}/reject`
Eintrag ablehnen.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Request:** `{ "rejection_note": "Bitte korrigieren" }`
---
#### `DELETE /time/entries/{entry_id}`
Eintrag löschen. Mitarbeiter: nur eigene offene/ausstehende. Manager: alle außer genehmigte (außer HR/Admin).
**Response:** `204 No Content`
---
### Überstundenkonto
#### `GET /time/balance/me`
Eigenes Überstundenkonto.
- **Query-Parameter:** `period_start` / `period_end` (date, optional)
**Response:**
```json
{
"user_id": "uuid",
"period_start": "2026-05-01",
"period_end": "2026-05-23",
"total_hours_worked": 92.5,
"expected_hours": 88.0,
"overtime_hours": 4.5,
"approved_entries": 18,
"pending_entries": 2
}
```
---
#### `GET /time/balance/{user_id}`
Überstundenkonto für einen Benutzer.
- **Erforderliche Rolle:** EMPLOYEE sieht nur sich selbst, sonst MANAGER+
---
### Arbeitspläne
#### `GET /time/schedules`
Alle Arbeitspläne der Firma.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
---
#### `POST /time/schedules`
Neuen Arbeitsplan anlegen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Request:**
```json
{
"name": "Vollzeit",
"mon_h": 8.0, "tue_h": 8.0, "wed_h": 8.0, "thu_h": 8.0, "fri_h": 8.0,
"sat_h": 0.0, "sun_h": 0.0,
"valid_from": "2026-01-01"
}
```
---
#### `PATCH /time/schedules/{schedule_id}`
Arbeitsplan bearbeiten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
#### `DELETE /time/schedules/{schedule_id}`
Arbeitsplan löschen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
## Abwesenheiten
Kein Router-Präfix (direkt unter `/api/v1/`).
### Abwesenheitstypen
#### `GET /absence-types/`
Alle Abwesenheitstypen der Firma.
- **Erforderliche Rolle:** beliebig
---
#### `POST /absence-types/`
Neuen Abwesenheitstyp anlegen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Request:**
```json
{
"name": "Urlaub",
"color": "#3B82F6",
"category": "vacation",
"requires_approval": true,
"deducts_vacation": true,
"affects_overtime_balance": false,
"requires_certificate": false,
"certificate_after_days": 3,
"is_paid": true,
"max_days_per_year": 30
}
```
**Kategorien:** `vacation` | `sick` | `overtime_comp` | `training` | `business_trip` | `other`
---
#### `PATCH /absence-types/{type_id}`
Abwesenheitstyp bearbeiten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### Feiertage
#### `GET /public-holidays/`
Feiertage auflisten.
- **Query-Parameter:** `year` (int, Pflicht), `country` (default "DE"), `state` (optional, z.B. "BY")
---
#### `POST /public-holidays/`
Feiertag manuell anlegen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### Abwesenheiten
#### `GET /absences/calendar`
Team-Kalender: alle Abwesenheiten im Zeitraum.
- **Query-Parameter:** `year` (int, Pflicht), `month` (112, optional)
---
#### `GET /absences/balance`
Eigenes Urlaubskonto (Jahr erforderlich).
- **Query-Parameter:** `year` (int, Pflicht)
**Response:**
```json
{
"user_id": "uuid",
"year": 2026,
"base_days": 30,
"special_days": 0,
"carried_over_days": 5,
"carried_over_expires_at": "2026-03-31",
"carried_over_expired": false,
"used_days": 12.0,
"pending_days": 3.0,
"remaining_days": 20.0
}
```
---
#### `GET /absences/balance/{user_id}`
Urlaubskonto eines Mitarbeiters.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
---
#### `POST /absences/quick-sick`
Sofort-Krankmeldung. Automatisch genehmigt, nutzt den ersten aktiven SICK-Abwesenheitstyp.
**Request:**
```json
{ "start_date": "2026-05-23", "end_date": "2026-05-23" }
```
---
#### `GET /absences/sick-stats`
Krankheitsstatistik (rolling 12 Monate) inkl. Bradford-Faktor.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
- **Query-Parameter:** `user_id` (optional), `ref_date` (optional, default heute)
**Response:** Liste von `SickStatsOut`:
```json
[{
"user_id": "uuid",
"bradford_factor": 7.2,
"episodes": 3,
"total_days": 9.0,
"certificate_overdue": false
}]
```
---
#### `GET /absences/overtime-balance`
Eigenes Überstunden-Ausgleichskonto.
---
#### `GET /absences/`
Abwesenheiten auflisten.
- **Query-Parameter:** `user_id`, `type_id`, `status`, `year` (alle optional)
- EMPLOYEE sieht nur eigene Abwesenheiten
---
#### `POST /absences/`
Abwesenheitsantrag stellen. Mit `for_user_id` können MANAGER+ für andere beantragen.
**Request:**
```json
{
"type_id": "uuid",
"start_date": "2026-06-01",
"end_date": "2026-06-05",
"half_day_start": false,
"half_day_end": false,
"substitute_id": null,
"note": "Jahresurlaub"
}
```
---
#### `GET /absences/{absence_id}`
Einzelne Abwesenheit abrufen.
---
#### `PATCH /absences/{absence_id}`
Ausstehenden Antrag bearbeiten. Nur solange Status `pending`.
---
#### `DELETE /absences/{absence_id}`
Eigenen ausstehenden Antrag stornieren.
**Response:** `204 No Content`
---
#### `POST /absences/{absence_id}/approve`
Abwesenheit genehmigen. Self-Approval-Schutz aktiv.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
---
#### `POST /absences/{absence_id}/reject`
Abwesenheit ablehnen.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
**Request:** `{ "rejection_reason": "Zu viele gleichzeitige Abwesenheiten" }`
---
#### `PATCH /absences/{absence_id}/certificate`
AU-Bescheinigung als eingegangen markieren.
- **Erforderliche Rolle:** HR, COMPANY_ADMIN, SUPER_ADMIN
**Request:** `{ "received_at": "2026-05-23" }`
---
#### `PATCH /absences/balance/{user_id}`
Urlaubskonto eines Mitarbeiters anpassen (Grundurlaub, Sondertage, Resturlaub).
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
- **Query-Parameter:** `year` (int, Pflicht)
---
## Dashboard & Reports
Kein Router-Präfix.
### Dashboard
#### `GET /dashboard/me`
Mitarbeiter-Dashboard: eigene Stunden, Urlaubsstand, Status heute.
**Response:** `EmployeeDashboard`
---
#### `GET /dashboard/team`
Team-Dashboard: Anwesenheit heute, ausstehende Genehmigungen.
- **Erforderliche Rolle:** MANAGER, HR, COMPANY_ADMIN, SUPER_ADMIN
---
#### `GET /dashboard/company`
Unternehmens-Dashboard: Gesamtübersicht, Überstunden, kommende Abwesenheiten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### Reports (JSON)
#### `GET /reports/time`
Zeiterfassungsbericht. EMPLOYEE sieht nur eigene Einträge.
- **Query-Parameter:** `date_from`, `date_to` (default: aktueller Monat), `user_id` (optional)
---
#### `GET /reports/absences`
Abwesenheitsbericht. EMPLOYEE sieht nur eigene Abwesenheiten.
---
#### `GET /reports/overtime`
Überstundenbericht.
---
#### `GET /reports/overtime/detail`
Erweiterter Überstundenbericht mit Wochen- und Tagesaufschlüsselung.
---
### Export
#### `GET /reports/time/export`
Zeiterfassungsbericht herunterladen.
- **Query-Parameter:** `format` (`csv` | `xlsx` | `pdf`, default `csv`)
---
#### `GET /reports/absences/export`
Abwesenheitsbericht herunterladen.
- **Query-Parameter:** `format` (`csv` | `xlsx` | `pdf`)
---
#### `GET /reports/overtime/export`
Überstundenbericht herunterladen (Detailansicht).
- **Query-Parameter:** `format` (`csv` | `xlsx` | `pdf`)
---
## Audit-Log
Kein Router-Präfix.
### `GET /audit-logs`
Audit-Log-Einträge auflisten.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
- SUPER_ADMIN sieht alle Firmen, COMPANY_ADMIN nur die eigene
- **Query-Parameter:**
- `user_id` (UUID, optional)
- `action` (string, Teilstring-Suche)
- `entity_type` (string, exakt)
- `date_from` / `date_to` (datetime)
- `limit` (default 50, max 500)
- `offset` (int)
**Response:**
```json
{
"total": 1234,
"items": [{
"id": "uuid",
"user_id": "uuid",
"user_name": "Max Mustermann",
"action": "absence_approved",
"entity_type": "absence",
"entity_id": "uuid",
"old_value": null,
"new_value": { "status": "approved" },
"ip_address": "192.168.1.1",
"created_at": "2026-05-23T10:00:00Z"
}]
}
```
---
### `GET /audit-logs/actions`
Alle vorhandenen Action-Werte für Filter-Dropdown.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Response:** `["absence_approved", "busylight_token_rotated", ...]`
---
### `GET /audit-logs/entity-types`
Alle vorhandenen Entity-Typen für Filter-Dropdown.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
## Kiosk-Geräte
Router-Präfix: `/kiosk`
### `GET /kiosk/devices`
Alle registrierten Kiosk-Geräte der Firma.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
### `POST /kiosk/devices`
Neues Kiosk-Gerät registrieren. Token wird nur einmalig zurückgegeben.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Request:**
```json
{
"name": "Eingang Berlin",
"location": "Hauptgebäude"
}
```
**Response `201`:** `KioskDeviceCreated` (enthält einmalig das Klartext-Token)
---
### `GET /kiosk/devices/{device_id}`
Einzelnes Gerät abrufen.
---
### `PATCH /kiosk/devices/{device_id}`
Gerät bearbeiten (Name, Standort).
---
### `POST /kiosk/devices/{device_id}/rotate-token`
Token rotieren das alte Token wird sofort ungültig.
**Response:** `KioskDeviceCreated` (neues Token)
---
### `DELETE /kiosk/devices/{device_id}`
Gerät löschen.
---
### `GET /kiosk/me`
Kiosk-Gerät authentifiziert sich und aktualisiert `last_seen_at`.
- **Authentifizierung:** Header `X-Kiosk-Token: <token>` (kein JWT)
---
## LDAP-Integration
Router-Präfix: `/ldap`
Alle Endpunkte erfordern: **COMPANY_ADMIN** oder **SUPER_ADMIN**
### `GET /ldap/config`
Aktuelle LDAP-Konfiguration abrufen.
---
### `POST /ldap/config`
LDAP-Konfiguration erstellen oder überschreiben.
**Request:**
```json
{
"enabled": true,
"host": "ldap.example.de",
"port": 389,
"use_ssl": false,
"use_tls": true,
"bind_dn": "cn=admin,dc=example,dc=de",
"bind_password": "secret",
"base_dn": "dc=example,dc=de",
"user_search_filter": "(objectClass=person)",
"attr_email": "mail",
"attr_firstname": "givenName",
"attr_lastname": "sn",
"attr_username": "uid",
"attr_department": "department"
}
```
---
### `PATCH /ldap/config`
LDAP-Konfiguration teilweise bearbeiten.
---
### `POST /ldap/test`
LDAP-Verbindung testen.
**Response:** `{ "success": true, "message": "..." }`
---
### `GET /ldap/preview`
Ersten 50 LDAP-Benutzer zur Vorschau vor dem Sync.
---
### `POST /ldap/sync`
LDAP-Benutzer synchronisieren.
**Request:** `{ "default_role": "EMPLOYEE" }`
**Response:** `{ "created": 5, "updated": 12, "deactivated": 1, "errors": [] }`
---
## SMTP-Konfiguration
Router-Präfix: `/smtp`
Alle Endpunkte erfordern: **COMPANY_ADMIN** oder **SUPER_ADMIN**
### `GET /smtp/config`
SMTP-Konfiguration abrufen.
---
### `POST /smtp/config`
SMTP-Konfiguration erstellen oder überschreiben.
**Request:**
```json
{
"host": "smtp.example.de",
"port": 587,
"use_tls": false,
"use_starttls": true,
"username": "noreply@example.de",
"from_email": "noreply@example.de",
"from_name": "TimeMaster",
"password": "secret",
"is_enabled": true
}
```
---
### `POST /smtp/test`
Test-E-Mail senden.
**Request:** `{ "to": "test@example.de" }`
**Fehler:** `502` SMTP-Fehler
---
## CalDAV-Integration
Router-Präfix: `/caldav`
### Firmenkalender
#### `GET /caldav/company/config`
Firmen-CalDAV-Konfiguration abrufen.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
---
#### `POST /caldav/company/config`
Firmen-CalDAV-Konfiguration speichern.
**Request:**
```json
{
"enabled": true,
"principal_url": "https://cal.example.de/remote.php/dav",
"calendar_url": "https://cal.example.de/remote.php/dav/calendars/user/timemaster/",
"username": "user",
"password": "secret",
"calendar_display_name": "TimeMaster",
"verify_ssl": true
}
```
---
#### `POST /caldav/company/test`
Firmen-CalDAV-Verbindung testen.
---
#### `POST /caldav/company/resync`
Alle genehmigten Abwesenheiten neu in den Firmenkalender synchronisieren.
**Response:** `{ "synced": 42, "errors": 0 }`
---
### Persönlicher Kalender
#### `GET /caldav/user/config`
Persönliche CalDAV-Konfiguration (eigener Benutzer).
---
#### `POST /caldav/user/config`
Persönliche CalDAV-Konfiguration speichern.
---
#### `POST /caldav/user/test`
Persönliche CalDAV-Verbindung testen.
---
## Busylight-Integration
Kein Router-Präfix.
### Token-Verwaltung
#### `GET /companies/me/busylight-token`
Status des Busylight-Pull-Tokens.
- **Erforderliche Rolle:** COMPANY_ADMIN, SUPER_ADMIN
**Response:** `{ "configured": true, "created_at": "2026-05-07T..." }`
---
#### `POST /companies/me/busylight-token/rotate`
Neues Busylight-Token generieren (altes wird ungültig). Token wird einmalig im Klartext zurückgegeben.
**Response:** `{ "token": "abc...", "created_at": "..." }`
---
#### `DELETE /companies/me/busylight-token`
Busylight-Token widerrufen.
---
### Pull-Endpunkt
#### `GET /busylight/users`
Anwesenheitsstatus aller Mitarbeiter mit Personalnummer.
- **Authentifizierung:** `Authorization: Bearer <busylight-token>` (kein JWT, firmenspezifisches Token)
- **Rate-Limit:** 60/Minute
- Nur Mitarbeiter mit gesetzter Personalnummer werden zurückgegeben
**Response:**
```json
{
"date": "2026-05-23",
"users": [{
"personnel_number": "0042",
"full_name": "Maria Muster",
"absences_today": [{ "type": "Urlaub", "category": "vacation" }]
}]
}
```
---
## Kimai-Import
Router-Präfix: `/import`
Erforderliche Rolle: **HR**, **COMPANY_ADMIN**, **SUPER_ADMIN**
### `POST /import/kimai/preview`
Kimai-CSV-Export vorschauen (keine DB-Änderungen).
- **Content-Type:** `multipart/form-data`
- **Felder:** `user_id` (UUID als String), `file` (CSV-Datei)
**Response:**
```json
{
"preview": [{ "date": "...", "start": "...", "end": "...", "type": "time" }],
"time_count": 150,
"absence_count": 12,
"skip_count": 3,
"errors": []
}
```
---
### `POST /import/kimai/apply`
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`