GET /portal/summary liefert Resturlaub und Überstundenguthaben als Firmensummen, keine Einzelpersonen und keine
Krankheitsdaten. Firmen-Token (SHA-256) wie beim Busylight, Verwaltung unter /companies/me/portal-token, Migration 0049.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Security-Audit 2026-09-10 (siehe Ed25519-Reinstall-Commits):
- Rate-Limit 10/min auf POST /kiosk/my-qr-token (Q-1) – kompromittiertes
Employee-JWT konnte sonst unbegrenzt QR-Login-Tokens erzeugen.
- _get_client_ip vertraut X-Real-IP nur noch, wenn die Verbindung selbst
von 127.0.0.1 kommt (I-1) – sonst könnte ein Client den Header selbst
setzen und die Kiosk-IP-Whitelist umgehen.
- Migration 0047: Unique-Index auf kiosk_devices.device_token_hash (B-2,
war Seq-Scan pro Bearer-Request), Index auf time_entries.kiosk_device_id
(K-1). RLS auf kiosk_devices bereits seit Migration 0024 vorhanden,
K-1-Verdacht einer RLS-Lücke damit ausgeräumt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
Bisher wurde bei Kiosk-Stempelungen nur die grobe Quelle ("kiosk")
gespeichert, nicht welches Gerät/welcher Standort. Bei mehreren Eingängen/
Filialen war nicht nachvollziehbar, wo ein Mitarbeiter sich eingestempelt
hat. Migration 0046: time_entries.kiosk_device_id (FK kiosk_devices,
SET NULL). Wird nur beim Einstempeln gesetzt (Ursprung des Eintrags), nicht
bei Aus-/Pause-Aktionen überschrieben. TimeEntryOut.kiosk_device_id neu.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
POST /kiosk/my-qr-token (normale JWT-Auth) – Mitarbeiter fordert einen 5min
gültigen, einmalig einlösbaren QR-Token an, den die Employee-App als QR-Code
zeigt. Kiosk scannt ihn (POST /kiosk/auth/qr, bereits vorhanden) und meldet
den Mitarbeiter an. Backend-Funktion (generate_qr_token) existierte schon,
war aber nie über einen Endpunkt erreichbar. Test für den kompletten Ablauf
inkl. Einmaligkeit (zweiter Versuch mit selbem Token → 401).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
KioskDevicesPage: Modus-Umschalter im Create-Modal (Ed25519 PC-Terminal vs.
Bearer/VPN-Tablet). Bearer-Modus ruft neuen POST /kiosk/devices/bearer auf,
zeigt Token danach einmalig als QR-Code (JSON {url, token}, scanbar von der
Kiosk-App) + Klartext mit Kopier-Button. Kein Public-Key-Feld im Bearer-Modus.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
Neuer Endpunkt POST /kiosk/devices/bearer (Admin-JWT statt CLI) – erzeugt
Token serverseitig, gibt ihn nur in dieser einen Antwort im Klartext
zurück (KioskBearerDeviceCreated{device, token}), DB speichert nur den
Hash. Ermöglicht QR-Code-Einrichtung direkt aus dem Portal statt CLI-only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
Analog zur Mobile-App-Sektion, aber auf COMPANY_ADMIN/SUPER_ADMIN
beschränkt – Kiosk-Geräte werden von Admins per CLI eingerichtet, keine
Endnutzer-Selbstbedienung wie bei der Employee-App. Liest
kiosk-version.txt aus dem gleichen static/downloads/-Verzeichnis.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
session.refresh() nach commit scheitert an RLS (SET LOCAL app.bypass_rls
verfällt mit dem commit, siehe project_rls_bypass_commit_pitfall) – trat
beim ersten echten Bearer-Geräte-Test auf ("Could not refresh instance").
Beide refresh()-Aufrufe entfernt, nicht nötig: alle in der Erfolgsmeldung
angezeigten Felder sind bereits im Python-Objekt gesetzt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
Direkte DB-Inserts über db_session brauchen SET LOCAL app.bypass_rls='on'
pro Transaktion (verfällt nach commit, siehe [[project_rls_bypass_commit_pitfall]]).
db_session.refresh() nach commit schlug aus demselben Grund fehl – entfernt,
nicht nötig. Bestehender test_heartbeat_missing_headers erwartete 422,
liefert jetzt korrekt 401 (Header sind seit Bearer-Auth-Modus optional).
229/229 Tests grün auf 137.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
Neuer, einfacherer Geräte-Auth-Modus (key_algorithm="bearer_vpn") als
Alternative zum bestehenden Ed25519-Request-Signing. Bestehende PC-Terminals
(key_algorithm="ed25519") bleiben unverändert – verify_kiosk_request()
verzweigt jetzt am Authorization-Header: "Bearer <token>" → einfacher Pfad
(nur Token-Hash + Status + IP-Whitelist, kein Timestamp/Nonce/Signatur),
sonst weiterhin die 4 X-Kiosk-*-Header wie bisher.
Hintergrund: Terminals+Tablets laufen jetzt hinter WireGuard-VPN, die
ursprüngliche MITM-Härtung (Ed25519-Signing) ist für Tablets keine
Kernanforderung mehr. Ermöglicht ein deutlich einfacheres Flutter-Kiosk-App-
Projekt ohne Client-seitige Krypto.
- Migration 0045: kiosk_devices.device_token_hash
- backend/cli.py: `kiosk add --auth-mode bearer` generiert Token, gibt es
einmalig aus (analog Enrollment-Setup-URL bisher)
- 4 neue Tests in test_kiosk_security.py (gültig/ungültig/IP-Whitelist/revoked)
Vollständiger Plan: /home/sysops/.claude/plans/staged-marinating-dove.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
login_qr() las den Redis-Token per GET und löschte ihn danach separat per
DELETE – zwei parallele Requests mit demselben QR-Token konnten beide vor
dem Löschen lesen und je eine Session erzeugen (Doppel-Login statt echtem
Einmal-Token). Fix: GETDEL (atomar seit Redis 6.2, Server läuft 8.0.2).
K-2 (Cross-Tenant-Redeem) aus demselben Audit betrifft keinen realen
Code-Pfad – company_id-Vergleich in login_qr() war bereits korrekt, und die
Kiosk-Geräte-Enrollment "QR-Redeem"-Annahme existiert nicht als HTTP-Flow
(Enrollment läuft ausschließlich über authentifizierte CLI). Verifiziert
vor Weiterplanung der Android-Tablet-Kiosk-Architektur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
start_time/end_time/break_start wurden mit vollen Mikrosekunden gespeichert
(datetime.now(timezone.utc).time() ohne microsecond=0), was Clients ohne
Mikrosekunden-Parsing brechen konnte (siehe Mobile-App-Crash). Sekunden-
Genauigkeit reicht für Zeiterfassung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
Liest version.txt aus dem gleichen static/downloads/-Verzeichnis wie die
APK und zeigt sie neben "Mobile App (Android)" in der ProfilePage. Muss bei
jedem APK-Deploy manuell mitgepflegt werden (kein automatischer Abgleich
mit der mobilen App-Version).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
get_async_redis() hatte keine socket_connect_timeout/socket_timeout/
max_connections – ein gehängtes Redis hätte Requests unbegrenzt
blockiert statt schnell zu failen. Zusätzlich einmaliger Ping im
Lifespan-Startup, nur zur Log-Sichtbarkeit (blockiert Boot nicht).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
Login-, TOTP- und Kiosk-PIN-Lockout zählten Fehlversuche mit separaten
INCR/EXPIRE-Calls. Zwischen beiden konnte der Fail-Counter-Key kurzzeitig
ohne TTL bestehen bleiben (Crash-Fenster) - nicht atomar. Jetzt per
redis.pipeline(transaction=True) als MULTI/EXEC.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
Redis-Review deckte auf, dass der Pool-Fix vom letzten Commit nur
totp_login/kiosk_security erreichte. Login/Refresh (auth_service.py) und
PIN/NFC/QR-Kiosk-Login (kiosk_auth_service.py) öffneten weiterhin pro
Request eine neue aioredis-Verbindung. Zusätzlich nutzten
kiosk_session_service.py und public_stamp_session_service.py den
*sync* Redis-Client aus async-Code – blockierender Socket-Call im
Event-Loop bei jedem Kiosk-/Stempel-Request.
Alle auf get_async_redis() umgestellt. TOTP-Lockout wirft jetzt 503
statt eines ungefangenen 500 bei Redis-Ausfall (RedisError explizit
gefangen, eigene HTTPExceptions unberührt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
create_absence() gibt (absence, warnings) zurück, der Router verwarf die
Warnungen bisher (im Unterschied zu approve_absence, das sie via
AbsenceApproveOut.warnings durchreicht). Neues AbsenceCreateOut-Schema
analog dazu. Regressionstest über Überschneidungs-Warnung.
Gefunden während Coverage-Arbeit an absence_service.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
_get_holiday_dates() fragte nur PublicHoliday für data.start_date.year ab.
Ein Antrag über den Jahreswechsel (z.B. 28.12.-01.01.) rechnete den
Neujahrsfeiertag im Folgejahr daher nicht heraus -> Mitarbeiter verlor
einen Urlaubstag, den er nicht verbraucht hat. Neuer year_to-Parameter
deckt den Jahres-Range ab, beide Aufrufstellen (create_absence,
update_absence) angepasst. Regressionstest ergänzt.
Gefunden während Coverage-Arbeit an absence_service.py/report_service.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
report_service.datev_monthly_report compares user.company_id (uuid.UUID)
against the passed company_id with != — a plain str from the JSON API
response never equals the UUID, so it always looked like a mismatch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
SET LOCAL app.bypass_rls only holds for the current transaction; a prior
commit() silently re-enabled RLS, causing db.get(User/Company, ...) to
return None on the raw db_session (no app.company_id context).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
create_absence's warnings are discarded by the router (only approve_absence
passes them through AbsenceOut.warnings) — test now calls the service
directly and documents this as a separate finding. DATEV test kept within
a single year since absence_service._get_holiday_dates only looks up
holidays for start_date.year (cross-year ranges miss the boundary holiday).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
setup_server.sh schrieb eine veraltete Minimal-nginx-Config ohne
/redoc, /static/-Alias (nötig für lokale Swagger/ReDoc-Assets) und
Security-Header. Bei einem Neu-Setup wäre /docs sofort wieder kaputt
gewesen. Jetzt 1:1 an die auf 137/164 laufende Config angeglichen.
Zusätzlich inkonsistente Step-Nummerierung ([1/6]...[5/7]) korrigiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
/docs blieb leer wenn das CDN vom Client-Netzwerk aus nicht erreichbar
war (Firewall/Proxy) – Backend lieferte korrektes HTML, aber Swagger-UI-
JS/CSS und ReDoc-JS kamen nicht an. Jetzt unter app/static/swagger-ui/
gebündelt und über eigene /docs+/redoc-Routen ausgeliefert (nur Dev,
Production weiterhin ohne interaktive Docs).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
hours_payouts, public_stamp, ical, reseller, tenants,
special_assignments, tls_admin – seit dem letzten Doku-Update
(2026-05-24) neu dazugekommen. projects.py bewusst ausgelassen
(toter Code, nicht registriert).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
Baseline liegt bei 55% (verifiziert auf 137). Gate mit 5%-Puffer,
kein CI-Job dafür – Tests brauchen echtes Postgres+Redis mit RLS-Setup,
läuft nur remote (SSH), nicht in Gitea Actions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn