Files
timemaster/CLAUDE.md
T
sysops 1fedd683e0 Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:27 +02:00

348 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TimeMaster Projektkontext für Claude Code
## Was ist das?
Zeiterfassung & HR-Tool (ähnlich Timebutler.de). DSGVO-konform, EU-Server.
## Tech-Stack
- **Backend**: Python 3.12 · FastAPI · SQLAlchemy (async) · PostgreSQL 16 · Redis 7
- **Frontend**: React 18 · TypeScript · Tailwind CSS
- **Deployment**: Nativ auf Ubuntu (systemd + nginx) kein Docker in Phase 1
- **Tests**: pytest + pytest-asyncio
## Aktueller Stand
### ✅ agent-01-auth (FERTIG)
- FastAPI App, CORS, Health-Endpoint
- JWT Access Token (30 min) + Refresh Token Rotation (30 Tage)
- 5 Rollen: SUPER_ADMIN, COMPANY_ADMIN, HR, MANAGER, EMPLOYEE
- `require_role()` FastAPI Dependency
- Models: Company, Department, User, Session, PasswordReset, AuditLog
- Routers: /api/v1/auth · /api/v1/users · /api/v1/companies
- Services: auth_service, user_service, email_service
- Alembic Migration: 0001_initial (alle Auth-Tabellen)
- 20+ pytest Tests
### 🚧 agent-02-zeiterfassung (TODO Woche 3-4)
Dateien anlegen in:
- `backend/app/models/time_entry.py`
- `backend/app/models/work_schedule.py`
- `backend/app/schemas/time_entry.py`
- `backend/app/routers/time_entries.py`
- `backend/app/services/time_service.py` (inkl. ArbZG-Prüfung)
- `backend/migrations/versions/0002_time_entries.py`
- `backend/tests/test_time.py`
API-Endpunkte:
```
POST /api/v1/time/stamp-in
POST /api/v1/time/stamp-out
POST /api/v1/time/break-start
POST /api/v1/time/break-end
GET /api/v1/time/today
GET /api/v1/time/entries
POST /api/v1/time/entries
PATCH /api/v1/time/entries/{id}
POST /api/v1/time/entries/{id}/approve
POST /api/v1/time/entries/{id}/reject
GET /api/v1/time/balance/{user_id}
```
### ✅ agent-03-abwesenheit (FERTIG siehe weiter unten)
### 🚧 agent-02-kiosk (TODO Woche 5, nach agent-02)
- `backend/app/models/kiosk_device.py`
- `backend/app/routers/kiosk.py`
- `backend/app/services/kiosk_service.py`
Auth-Methoden: PIN · NFC (Web NFC API) · QR (jsQR) · Mitarbeiter-Liste
### ✅ agent-03-abwesenheit (FERTIG deployed)
- Models: AbsenceType, Absence, VacationBalance, PublicHoliday, OvertimeBalance
- Rollen-basierte Genehmigung (MANAGER/HR/ADMIN)
- Urlaubskonto: Grundurlaub + Sondertage + Resturlaub (auto carry-over)
- Frontend: AbsencesPage mit Liste-Tab + Jahresplaner (Person×Monat-Grid + Monats-Gantt)
- TOTP/2FA: pyotp, QR-Setup, partial JWT Login-Flow, Profil-Seite
- Migrationen: 0015 (TOTP), 0016 (special_days) deployed
### ✅ agent-05-krankmeldung (FERTIG deployed 2026-05-06)
Erweiterung des Abwesenheits-Moduls speziell für Kranktage:
**Backend:**
- `Company.sick_note_required_after_days` (Default 3) Firmen-Default
- `AbsenceType.certificate_after_days` als Per-Typ-Override (Override gewinnt)
- Auto-Berechnung `certificate_required_by` beim Anlegen einer SICK-Absence
- `POST /absences/quick-sick` Sofort-Krankmeldung (auto-approved, nutzt ersten aktiven SICK-Typ)
- `PATCH /absences/{id}/certificate` HR/Admin markiert Attest als eingegangen
- `GET /absences/sick-stats?user_id=&ref_date=` Bradford-Faktor (rolling 12 Monate), Episoden, Tage, AU-überfällig
**Frontend:**
- 🤒 "Krank melden" Schnell-Button im AbsencesPage-Header (orange, neben "+ Antrag stellen")
- AU-überfällig-Badge in Liste (orange) + grüner "Attest ✓"-Badge wenn eingegangen
- HR/Admin-Action "Attest erhalten" in Liste (sichtbar für HR/COMPANY_ADMIN/SUPER_ADMIN, **nicht** MANAGER)
- ReportsPage: vierter Tab "Krankmeldungen" mit KPIs (⌀ Bradford, Σ Tage, Σ Episoden, AU offen) + sortierte Tabelle pro Mitarbeiter
- Bradford-Heatmap aufgeschoben
**Migration:** `0022_sick_note_config.py` `companies.sick_note_required_after_days INTEGER DEFAULT 3` (chained on 0020, da 0017 nicht in der Alembic-Kette war)
**Tests:** 5 neue pytest-Cases in `test_absences.py` (quick_sick, cert auto-calc, type override, mark_certificate, bradford) alle grün
### 🚧 agent-06-auditlog (TODO)
AuditLog-Einträge sind bereits in der DB (Tabelle `audit_logs`, Model vorhanden seit agent-01).
Fehlend: Backend-Endpunkt + Frontend-Seite für SUPER_ADMIN / COMPANY_ADMIN.
**Backend:**
- Neuer Endpunkt: `GET /audit-logs/` mit Filtern:
- `user_id`, `action`, `entity_type`, `date_from`, `date_to`, `limit`, `offset`
- Nur COMPANY_ADMIN/SUPER_ADMIN, company-isoliert
- Felder die zurückgegeben werden: `id`, `user_id`, `user_name`, `action`, `entity_type`, `entity_id`, `old_value`, `new_value`, `ip_address`, `created_at`
- Router: `backend/app/routers/audit.py`
**Frontend:**
- Neue Seite: `frontend/src/pages/AuditLogPage.tsx`
- Route: `/audit-log` (nur COMPANY_ADMIN, SUPER_ADMIN)
- Filter: Zeitraum, Benutzer, Aktion, Entity-Typ
- Tabelle: Zeitstempel, Benutzer, Aktion, Betroffenes Objekt, IP
- Detail-Expand: old_value / new_value als JSON-Diff
- Export als CSV
- Navigation: Eintrag in Einstellungen-Dropdown in Layout.tsx
### 🚧 agent-04-dashboard (TODO Woche 6-7)
- Mitarbeiter-, Manager-, Admin-Dashboard
- Reports: Anwesenheit, Abwesenheit, Überstunden, Kranktage (Bradford-Faktor)
- Export: PDF (WeasyPrint) · CSV · XLSX (openpyxl)
### 🚧 agent-07-personalnummer (Phase 1 ✅ deployed 2026-05-05; Phase 2-5 offen)
Eindeutige Personalnummer pro Mitarbeiter, in allen API-Antworten, Exports und Kiosk-Login nutzbar.
**Backend:**
- `User.personnel_number: str(50) | None` Format **nur Ziffern** (`^[0-9]+$`), max 50 Stellen
- Partial Unique Index: `UNIQUE (company_id, personnel_number) WHERE personnel_number IS NOT NULL`
- **Reservierung**: Bei Deaktivierung bleibt Nr. am User, wird nie wieder vergeben
- `Company`-Felder:
- `personnel_number_required: bool` (default false → Pflicht erst nach Aktivierung in Firmen-Settings; gilt nur für **neue** User, Bestandsuser bleiben)
- `personnel_number_mode: enum('manual'|'auto')` (default manual)
- `personnel_number_next: int` (Counter, beginnt bei 1, 4-stellig zero-padded → `0001`, kein Präfix wegen „nur Ziffern"-Regel)
- Auto-Modus erlaubt manuelles Override durch Admin (Counter läuft normal weiter)
- Race-Condition-Schutz: `UPDATE companies SET personnel_number_next = personnel_number_next + 1 RETURNING ...` (atomic)
- Endpoints:
- `GET /users/next-personnel-number` Vorschlag basierend auf Counter
- `GET /users/by-personnel/{number}` Lookup für externe Integrationen
- `GET /users/import-template.csv` Vorlage für Bulk-Import
- `POST /users/import` (multipart CSV) Bulk-Import:
- Doppelte E-Mail im Import (zwei Zeilen) → Fehler
- E-Mail existiert aktiv → Fehler
- E-Mail existiert aber User deaktiviert → **Reaktivieren mit neuen Daten**
- Leere Personalnr. → Auto-Vergabe (auch im Manuell-Modus)
- `GET /users` Query-Param `search` filtert auch Personalnr.
- AuditLog bei jeder Personalnr.-Änderung
- LDAP-Sync: bei Konflikt mit reservierter/vergebener Nr. → Fehler, LDAP-Wert wird verworfen (kein Override)
- Personalnr. in CSV/XLSX/PDF-Exports (`reports.py`), CalDAV-Template-Platzhalter `$personalnummer`, LDAP-Sync-Mapping `employeeNumber`
**Frontend:**
- `UsersPage`: Spalte "Pers.-Nr.", Suche schließt Personalnr. ein, Edit-Modal mit Live-Verfügbarkeitsprüfung
- Invite-Modal: Eingabefeld + "Vorschlagen"-Button (Auto-Modus)
- `ProfilePage`: Read-only Anzeige
- `CompanySettingsPage`: Toggles Pflicht/Modus/Präfix/Counter
- `ReportsPage` + Exports: Spalte ergänzt
- CSV-Import-UI mit Vorlagen-Download und Validierungs-Vorschau
**Migration:** `0020_personnel_number.py`
- `users.personnel_number VARCHAR(50) NULL` + Partial Unique Index + CHECK constraint `~ '^[0-9]+$'`
- `companies.personnel_number_required BOOLEAN DEFAULT FALSE`
- `companies.personnel_number_mode VARCHAR(10) DEFAULT 'manual'`
- `companies.personnel_number_next INTEGER DEFAULT 1`
**Tests**:
- pytest: Unit/Integration für Service-Logik, CSV-Import, Race-Condition (parallele Inserts)
- Playwright-Setup wird im Rahmen von agent-08 aufgesetzt CSV-Upload-UI dann mit testen
### 🚧 agent-08-kiosk-haertung (TODO kritisch vor Produktiv-Einsatz)
MITM-resistente Absicherung des Kiosk-Modus. Aktueller Token-basierter Auth ist nicht ausreichend gegen TLS-Inspection-Proxies und Insider-Angriffe (siehe Security-Audit).
**Architektur-Entscheidung: Browser bleibt Kiosk-Plattform** native App mittelfristig, aber nicht in dieser Phase. Trust-Boundary: IT-Admin der Kiosk-Geräte.
**Krypto-Entscheidung: Ed25519-Public-Key statt mTLS+HMAC**
Statt mTLS-Client-Zertifikate + HMAC-Shared-Secret nutzt jedes Kiosk-Gerät ein **Ed25519-Keypair** (gleicher Algo wie moderne SSH-Keys). Public Key wird beim Server registriert, Private Key bleibt non-extractable im Kiosk-Browser. Vorteile: kein CA-Management, kein Shared Secret, Remote-Enrollment trivial via CLI auf Server, einfacheres nginx (kein mTLS), kryptographisch gleichwertig oder besser.
**Backend:**
- `KioskDevice` erweitern:
- `status: enum('pending','approved','revoked')` (löst `is_active` ab)
- `public_key: text` Ed25519 Public Key (PEM oder OpenSSH-Format)
- `key_algorithm: str default 'ed25519'` (Vorbereitung für spätere Algos)
- `last_heartbeat_at: datetime | None`
- `client_version: str | None`
- `current_user_id: uuid | null` (wer aktuell eingestempelt ist DSGVO: pro Firma deaktivierbar)
- `offline_queue_size: int default 0`
- `ip_whitelist: text | null` (CIDR-Liste, in Production verpflichtend)
- `enrollment_token_hash: str | None` + `enrollment_expires_at` (Setup-URL, max 30 min nicht Token, nur Geräte-ID-Bindung)
- `Company.kiosk_require_approval: bool default true`
- `Company.kiosk_track_current_user: bool default true` (DSGVO-Opt-Out)
**Sichere Stempel-Auth pro Request** (neue Dependency `verify_kiosk_request()`):
- TLS-Pflicht: nginx HTTPS-only + HSTS, App lehnt non-TLS in Production ab
- HTTP-Header pro Request:
- `X-Kiosk-Key-Id`: UUID des Geräts
- `X-Kiosk-Timestamp`: Unix-Sekunden (max 30s Drift; Server-Time autoritativ)
- `X-Kiosk-Nonce`: UUID, einmalig (Replay-Schutz)
- `X-Kiosk-Signature`: Base64(Ed25519-Signatur über `METHOD + PATH + TIMESTAMP + NONCE + sha256(BODY)`)
- Backend verifiziert mit gespeichertem Public Key (Python: `cryptography.hazmat.primitives.asymmetric.ed25519`)
- Nonce-Cache 60s in Redis (Replay-Window = Drift-Window)
- IP-Whitelist-Check pro Gerät
**CLI-Tool (neu: `backend/cli.py`, mit Typer)**:
```bash
# Auf Server ausführen (root@192.168.1.137)
timemaster kiosk add \
--company "Acme GmbH" \
--name "Eingang Berlin" \
--location "Hauptgebäude" \
--pubkey ~/.ssh/kiosk_berlin.pub \
--ip-whitelist 10.0.0.0/24
# Output: device_id + Setup-URL für Kiosk-Browser
# https://timemaster.example.com/kiosk/setup?id=<uuid>&token=<one-time>
timemaster kiosk list [--company X] [--status pending|approved|revoked]
timemaster kiosk approve <device_id>
timemaster kiosk revoke <device_id>
timemaster kiosk rotate-key <device_id> # Setup-URL für neuen Pubkey
```
CLI nutzt direkten DB-Zugriff über bestehende SQLAlchemy-Models, keine HTTP-API.
**Enrollment-Flow** (Remote-tauglich):
1. **Vor Ort/Remote**: IT-Person erzeugt auf Kiosk-Gerät ein Ed25519-Keypair (entweder im Browser via WebCrypto oder per `ssh-keygen -t ed25519` lokal). Private Key bleibt am Gerät.
2. **Public Key** wird an Admin gesendet (E-Mail/Chat reicht ist public)
3. **Admin** SSH'ed auf Server, registriert per CLI: `timemaster kiosk add --pubkey ...`
4. CLI gibt **Setup-URL** + Geräte-ID zurück → Admin schickt URL an IT-Person
5. Kiosk-Browser öffnet Setup-URL → bindet bestehenden privaten Key an Geräte-ID, signiert ersten Test-Request
6. Admin sieht Gerät in WebGUI als `pending` → Klick „Approve" → ab jetzt aktiv
7. **Privater Key verlässt nie das Kiosk-Gerät**, **Server kennt nur Public Key**
Optional Web-Enrollment-Variante (für lokale Filiale, Admin steht daneben):
- Admin generiert in WebGUI Setup-URL → QR-Code → Kiosk scannt → Browser generiert Keypair → Public Key per HTTP-POST an Backend → Admin approved.
**Key-Rotation**: kein Auto-Rotate (Public Key altert nicht). Manueller Re-Enrollment via `timemaster kiosk rotate-key` falls Verdacht auf Kompromittierung.
**Heartbeat & Liveness**:
- Endpoint `POST /kiosk/heartbeat` alle 30s (signiert wie Stempel-Requests sonst Fake-Online möglich)
- Body: `{ uptime_seconds, current_user_id?, browser_version, queued_offline_entries, client_version }`
- Server-Status: **online** (<90s), **stale** (<5min), **offline** (älter)
- Heartbeat-Intervall pro Firma einstellbar (default 30s, max 120s)
**Kiosk-Frontend (separate Route `/kiosk`)**:
- ServiceWorker übernimmt Ed25519-Signierung aller Requests
- WebCrypto Ed25519 mit `extractable: false` (XSS kann Key nicht exfiltrieren)
- Browser-Anforderung: Chrome 113+, Safari 16.4+, Firefox 130+ (alle ab 2023/2024)
- Heartbeat-Loop alle 30s
- Offline-Queue mit IndexedDB (Sync sobald wieder online) Queue-Größe an Server reportet
- BroadcastChannel zur Tab-Koordination (nur ein Tab sendet Heartbeat)
- Kiosk-Login per Personalnummer + PIN als zusätzliche Methode (nur Kiosk, nicht Web)
- Server-Time wird im Heartbeat-Response zurückgeliefert → UI zeigt Server-Zeit, Stempel-Timestamp serverseitig erzwungen
**Frontend-Verwaltung (`KioskDevicesPage`)**:
- Tab "Wartet auf Freigabe" + Tab "Aktive Geräte" mit Live-Status (Auto-Refresh 30s)
- Status-Ampel-Spalte (🟢/🟡/🔴), letzter Heartbeat, aktueller User, Client-Version
- Public-Key-Fingerprint anzeigen (zur Verifikation)
- Sortier-/Filter-Option "nur offline"
- `Layout.tsx`: Health-Badge oben rechts für COMPANY_ADMIN + HR ("2/3 Kiosks online")
**Anomalie-Detection** (Phase 2 nach Grundgerüst):
- Stempel-Frequenz-Threshold pro Gerät
- IP-Sprung-Detection (Geo-Inkonsistenz)
- Ungewöhnlicher User-Mix pro Gerät → AuditLog-Alarm
**nginx-Konfiguration** (`nginx.conf`): keine mTLS-Änderungen nötig. Standard-HTTPS + HSTS reicht Auth läuft komplett auf Application-Layer.
**Migration:** `0021_kiosk_security.py`
- `kiosk_devices`: neue Spalten + Drop von `is_active` (durch `status` ersetzt)
- Bestehende Geräte → Default-Status `revoked` (müssen neu enrolled werden, sicher)
- `companies.kiosk_require_approval`, `kiosk_track_current_user`, `kiosk_heartbeat_interval_sec`
**Sicherheits-Bewertung (Ed25519-Variante)**:
| Schutz | Erreicht | Restrisiko |
|---------------------------------|----------|------------|
| Passiver MITM (mitlesen) | ✅ TLS + Signaturen | Public Key + signierte Requests sichtbar (nicht ausnutzbar) |
| Aktiver MITM (Replay/Modify) | ✅ Signatur+Nonce+Timestamp | |
| Shared-Secret-Diebstahl | ✅ entfällt kein Secret | |
| PrivKey-Diebstahl (XSS) | ✅ non-extractable WebCrypto | Key bleibt im Browser-Speicher |
| Enrollment-Phishing | ✅ Public Key offen austauschbar | |
| Remote-Filialen-Enrollment | ✅ trivial via CLI | |
| CA-Verlust = alle Kiosks tot | ✅ entfällt keine CA | |
| IT-Admin als Innentäter | ⚠️ teilweise | nur native App + Hardware-Keystore |
| Zeit-Manipulation am Kiosk | ✅ Server-Time autoritativ | |
| Browser-Cache-Clear → Re-Enroll | ⚠️ ja | manueller Re-Enroll nötig (akzeptiert) |
**Aufteilung in PRs**:
1. Backend: Ed25519-Verifizierung + CLI-Tool + Migration (kein Frontend)
2. Frontend Kiosk-Mode: ServiceWorker + WebCrypto + Setup-Flow + Offline-Queue
3. Frontend Verwaltung: KioskDevicesPage-Erweiterung + Health-Badge im Layout
4. Phase 2: Anomalie-Detection
## Wichtige Konventionen
### Backend
- Alle DB-Operationen async (SQLAlchemy `AsyncSession`)
- Services enthalten Business-Logik, Router nur HTTP-Handling
- Pydantic v2 Schemas mit `model_validate()` statt `.from_orm()`
- Fehler immer als `HTTPException` mit sprechendem `detail`
- Tokens nie im Klartext in DB immer `hash_token()` aus `core/security.py`
- Kein Hard-Delete bei Users nur `is_active = False`
### Sicherheit
- Passwort-Validierung: min. 8 Zeichen, 1 Großbuchstabe, 1 Ziffer
- Rate-Limiting für Auth-Endpunkte (TODO: slowapi einbauen)
- Audit-Log bei allen sensitiven Aktionen
### ArbZG-Regeln (für agent-02)
- Max. 8h Arbeitszeit / Tag (Ausnahme bis 10h möglich)
- Pause ab 6h: 30 min · ab 9h: 45 min
- Mindestruhezeit zwischen Schichten: 11h
- Warnung bei Überschreitung, kein hartes Blockieren
## Datenbankschema Status
- Migration 0001: companies, departments, users, sessions, password_resets, audit_logs ✅
- Migration 0002: time_entries, work_schedules (TODO)
- Migration 00030016: absence_types, absences, vacation_balances, public_holidays, overtime_balance, totp, special_days ✅
- Migration 0017: (übersprungen Nummer war nie in der Alembic-Kette)
- Migration 0018: kiosk_devices ✅
- Migration 0019: manual_time_entry_permission ✅
- Migration 0020: personnel_number (users + companies) ✅ (agent-07 Phase 1 deployed)
- Migration 0021: kiosk_security (mTLS, HMAC, heartbeat, enrollment) TODO (agent-08)
- Migration 0022: sick_note_config (companies.sick_note_required_after_days) ✅ (agent-05 deployed 2026-05-06)
## Umgebung
### Lokal (Claude Code läuft hier)
- Entwicklung & Dateien bearbeiten: `~/projects/timemaster/`
- Tests lokal ausführen: `pytest -v`
- Kein lokaler Python/Postgres nötig alles läuft auf dem Server
### Deployment-Server
- **Host**: `root@192.168.1.137`
- **Projektpfad**: `/opt/timemaster/`
- **venv**: `/opt/timemaster/backend/venv/`
- **Service**: `systemctl restart timemaster`
- **Logs**: `journalctl -u timemaster -f`
### Deployment-Workflow (nach jeder Änderung)
```bash
# Dateien auf Server synchronisieren
rsync -avz --exclude='__pycache__' --exclude='*.pyc' --exclude='.env' \
~/projects/timemaster/backend/ \
root@192.168.1.137:/opt/timemaster/backend/
# Migration ausführen (falls neue Alembic-Version)
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic upgrade head"
# Service neu starten
ssh root@192.168.1.137 "systemctl restart timemaster"
# Logs prüfen
ssh root@192.168.1.137 "journalctl -u timemaster -n 50"
```
### Erstes Setup auf dem Server (einmalig)
```bash
ssh root@192.168.1.137 "bash /opt/timemaster/setup_server.sh"
```