diff --git a/.gitignore b/.gitignore index 9176499..c5f46f7 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ resume~ # OS .DS_Store Thumbs.db +CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 69395c6..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,347 +0,0 @@ -# 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=&token= - -timemaster kiosk list [--company X] [--status pending|approved|revoked] -timemaster kiosk approve -timemaster kiosk revoke -timemaster kiosk rotate-key # 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 0003–0016: 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" -```