# TimeMaster – API-Referenz Alle Endpunkte sind unter dem Präfix `/api/v1` erreichbar. Authentifizierung: `Authorization: Bearer ` (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": "", "refresh_token": "", "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": "", "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": "", "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": "", "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` (1–12, 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: ` (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 ` (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": [] }`