Geplante Erinnerungen (Feature-Parität mit Urlaubsverwaltung): - APScheduler (AsyncIOScheduler) in der FastAPI-Lifespan; tägliche Jobs ab settings.reminder_hour. Redis-Tageslock gegen Doppelversand bei mehreren Prozessen; jeder Job mit eigener Session + RLS-Bypass. - Drei Jobs (auch einzeln aufrufbar): offene Anträge an Genehmiger, Resturlaub-Verfall-Vorwarnung an Mitarbeiter, fehlende AU an HR. - Pro-User notification_prefs (JSONB, opt-out); GET/PATCH /users/me/notification-prefs + ProfilePage-UI; Vertreter-Mail respektiert die Prefs. - Manueller Trigger POST /companies/me/run-reminders (Admin) – gleiche Logik, firmen-scoped (testbar ohne Warten). - Bugfix: GET-/PATCH-Urlaubskonto (update_balance) nutzte nicht existente Felder (base_days/carried_over_days/ip_address) → korrigiert auf entitled_days/carried_over/ip + company_id; available_days ergänzt. Migration 0037 (users.notification_prefs). 188/188 Tests grün. Deployed 137 + 164. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.5 KiB
Python
72 lines
2.5 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"
|
||
|
||
# Scheduler (Erinnerungs-Mails, agent-11 PR3)
|
||
scheduler_enabled: bool = True
|
||
reminder_hour: int = 7 # Server-Stunde für tägliche Erinnerungen
|
||
carryover_reminder_days: int = 14 # Vorlauf für Resturlaub-Verfall-Erinnerung
|
||
|
||
# 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()
|