Files
patrick ada1b51f33 docs: vollständige Projektdokumentation hinzugefügt
- docs/api.md: komplette API-Referenz (1375 Zeilen, alle Endpunkte)
- docs/architecture.md: Tech-Stack, DB-Schema, RLS-Architektur, Auth-Flow
- docs/deployment.md: Setup, nginx, systemd, update.sh, Backup/Rollback
- docs/development.md: Dev-Setup, Test-Workflow, Code-Konventionen, Fallstricke

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:29:44 +02:00

1376 lines
25 KiB
Markdown
Raw Permalink 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)
---
## 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": [] }`