Files
timemaster/docs/api.md
T
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

25 KiB
Raw Blame History

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
  2. Authentifizierung & 2FA
  3. Benutzer
  4. Firmen & Abteilungen
  5. Zeiterfassung
  6. Abwesenheiten
  7. Dashboard & Reports
  8. Audit-Log
  9. Kiosk-Geräte
  10. LDAP-Integration
  11. SMTP-Konfiguration
  12. CalDAV-Integration
  13. Busylight-Integration
  14. 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:

{ "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:

{
  "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:

{ "email": "user@example.de", "password": "Passwort123" }

Response: TokenResponse

{
  "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:

{ "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:

{
  "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:

{ "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:

{ "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:

{ "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:

{ "total": 42, "items": [ /* UserOut */ ] }

POST /users/invite

Neuen Benutzer einladen (E-Mail mit Einladungslink).

  • Erforderliche Rolle: COMPANY_ADMIN, SUPER_ADMIN

Request:

{
  "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:

{
  "source": "web",
  "project_id": null,
  "note": "Projekt X"
}

Response: TimeEntryWithWarnings

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{ "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:

[{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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": [] }