feat: FZA Einzelstunden + Security-Fixes (K-1–K-5, H-2–H-4, M-1/M-3/M-6)

FZA Einzelstunden:
- Absence.fza_hours (Numeric 5,2) — FZA in Stunden statt Tagen
- Migration 0032: fza_hours Spalte in absences
- AbsenceCreate/AbsenceOut Schema um fza_hours erweitert
- absence_service: _deduct/_refund_overtime nutzt fza_hours direkt wenn gesetzt
- Frontend: Tage/Stunden-Toggle im FZA-Antrag-Modal

Security K-1: Privilege Escalation via PATCH /users/{id}.role
- user_service: Whitelist für Rollenänderungen, SUPER_ADMIN nur durch SUPER_ADMIN
- Letzter COMPANY_ADMIN gegen Selbst-Demotion gesichert

Security K-2: Kiosk-IP-Whitelist hinter nginx
- kiosk_security: _get_client_ip() liest X-Real-IP statt request.client.host

Security K-3: Kiosk-PIN Brute-Force-Schutz
- kiosk_auth_service: Redis-Lockout nach 5 Fehlversuchen (15 min)

Security K-4: TOTP-Setup-Hijacking
- auth router: /totp/setup abgelehnt wenn TOTP bereits aktiv

Security K-5: Separater Fernet-Key
- config: SECRET_KEY_DATA Feld (optional, Fallback auf SECRET_KEY)
- crypto: get_fernet_key() mit Warning bei fehlendem SECRET_KEY_DATA

Security H-2: Vacation Balance nur HR/Admin
- absences router: PATCH /balance nur noch HR/COMPANY_ADMIN/SUPER_ADMIN + AuditLog

Security H-3: Rate-Limits auf /auth/refresh + /auth/logout
- auth router: 30/min auf refresh, 60/min auf logout

Security H-4: Login-Failure-Logging + Lockout
- auth_service: Redis-Counter, Lockout nach 10 Versuchen (15 min)
- AuditLog für login_success und login_failed

Security M-1: Nginx Security-Header
- nginx.conf: X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy, X-XSS-Protection, Permissions-Policy

Security M-3: AuditLog bei Rollenänderungen
- user_service: action=role_changed mit old/new role

Security M-6: create_all nur in Development
- main.py: Base.metadata.create_all nur wenn not settings.is_production

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 11:13:42 +02:00
co-authored by Claude Sonnet 4.6
parent a17f71475f
commit 4cce09521e
19 changed files with 693 additions and 109 deletions
+27 -2
View File
@@ -43,6 +43,8 @@ export function AbsencesPage() {
half_day_start: false, half_day_end: false,
note: '', for_user_id: '',
})
const [fzaMode, setFzaMode] = useState<'days' | 'hours'>('days')
const [fzaHours, setFzaHours] = useState<number>(4)
const year = new Date().getFullYear()
@@ -107,6 +109,8 @@ export function AbsencesPage() {
setShowCreate(true)
setError('')
setForm({ type_id: '', start_date: '', end_date: '', half_day_start: false, half_day_end: false, note: '', for_user_id: '' })
setFzaMode('days')
setFzaHours(4)
if (isManager) await loadColleaguesIfNeeded()
}
@@ -119,11 +123,21 @@ export function AbsencesPage() {
await saveEdit(editAbsence, editForm, isManager, () => setEditAbsence(null), setSubmitting)
}
const isFzaType = (typeId: string) => {
const t = types.find(t => t.id === typeId)
if (!t) return false
const lower = t.name?.toLowerCase() ?? ''
return lower.includes('fza') || lower.includes('freizeitausgleich')
}
const handleCreate = async () => {
const useFzaHours = isFzaType(form.type_id) && fzaMode === 'hours'
await createAbsence(form, () => {
setShowCreate(false)
setForm({ type_id: '', start_date: '', end_date: '', half_day_start: false, half_day_end: false, note: '', for_user_id: '' })
}, setSubmitting)
setFzaMode('days')
setFzaHours(4)
}, setSubmitting, useFzaHours ? fzaHours : undefined)
}
const handleSaveBalance = async () => {
@@ -777,8 +791,19 @@ export function AbsencesPage() {
submitting={submitting}
error={error}
overtimeBalance={overtimeBalance}
fzaMode={fzaMode}
setFzaMode={setFzaMode}
fzaHours={fzaHours}
setFzaHours={setFzaHours}
isFzaType={isFzaType}
onCreate={handleCreate}
onClose={() => { setShowCreate(false); setError(''); setForm({ type_id: '', start_date: '', end_date: '', half_day_start: false, half_day_end: false, note: '', for_user_id: '' }) }}
onClose={() => {
setShowCreate(false)
setError('')
setForm({ type_id: '', start_date: '', end_date: '', half_day_start: false, half_day_end: false, note: '', for_user_id: '' })
setFzaMode('days')
setFzaHours(4)
}}
/>
)}