06bb1c1664
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>
67 lines
2.3 KiB
Python
67 lines
2.3 KiB
Python
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
from pydantic import Field, model_validator
|
||
from functools import lru_cache
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||
|
||
# App
|
||
app_name: str = "TimeMaster"
|
||
app_env: str = "development"
|
||
secret_key: str = "change-me-in-production"
|
||
|
||
# Separater Schlüssel für Fernet-Datenverschlüsselung (CalDAV/LDAP/SMTP-Passwörter, TOTP-Secrets).
|
||
# Empfohlen: in .env als SECRET_KEY_DATA=<zufälliger-string-32+-zeichen> setzen.
|
||
# Wenn nicht gesetzt, wird SECRET_KEY als Fallback verwendet (Warnung beim Start).
|
||
# WICHTIG: Nach erstem Setzen NICHT mehr ändern – alle verschlüsselten DB-Werte werden unlesbar!
|
||
secret_key_data: str | None = Field(None, validation_alias="SECRET_KEY_DATA")
|
||
frontend_url: str = "http://localhost:5173"
|
||
allowed_hosts: list[str] = []
|
||
|
||
# Database
|
||
database_url: str = "postgresql+asyncpg://timemaster:secret@localhost:5432/timemaster_db"
|
||
|
||
# Redis
|
||
redis_url: str = "redis://localhost:6379/0"
|
||
|
||
# JWT
|
||
access_token_expire_minutes: int = 30
|
||
refresh_token_expire_days: int = 30
|
||
algorithm: str = "HS256"
|
||
|
||
# Email
|
||
resend_api_key: str = ""
|
||
email_from: str = "noreply@timemaster.app"
|
||
email_from_name: str = "TimeMaster"
|
||
|
||
# First superadmin
|
||
first_superadmin_email: str = ""
|
||
first_superadmin_password: str = ""
|
||
|
||
# CalDAV / outbound HTTP
|
||
# Kommaseparierte CIDR-Whitelist für interne CalDAV-Server (z.B. Nextcloud im LAN).
|
||
# Diese CIDRs sind vom SSRF-Schutz ausgenommen.
|
||
# Beispiel: CALDAV_ALLOWED_CIDRS=192.168.1.0/24,10.10.5.50/32
|
||
caldav_allowed_cidrs: list[str] = []
|
||
|
||
@model_validator(mode='after')
|
||
def validate_secret_key(self):
|
||
if self.app_env == 'production' and self.secret_key == 'change-me-in-production':
|
||
raise ValueError('SECRET_KEY must be changed in production! Set SECRET_KEY env variable.')
|
||
if len(self.secret_key) < 32:
|
||
raise ValueError('SECRET_KEY must be at least 32 characters long.')
|
||
return self
|
||
|
||
@property
|
||
def is_production(self) -> bool:
|
||
return self.app_env == "production"
|
||
|
||
|
||
@lru_cache
|
||
def get_settings() -> Settings:
|
||
return Settings()
|
||
|
||
|
||
settings = get_settings()
|