- 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>
25 KiB
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
- System
- Authentifizierung & 2FA
- Benutzer
- Firmen & Abteilungen
- Zeiterfassung
- Abwesenheiten
- Dashboard & Reports
- Audit-Log
- Kiosk-Geräte
- LDAP-Integration
- SMTP-Konfiguration
- CalDAV-Integration
- Busylight-Integration
- 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 registriert429– 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 Anmeldedaten403– 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, Feldfile
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 eigenedate_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_idsetzen
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(1–12, 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, defaultcsv)
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": [] }