dd3e069466
SET LOCAL Werte (bypass_rls, company_id) sind transaktions-gebunden. Nach db.commit() ist der Kontext weg – ein nachfolgendes db.refresh() läuft in einer neuen Transaktion ohne RLS-Kontext und liefert 0 Rows. Da expire_on_commit=False gesetzt ist, sind alle Instanz-Attribute nach dem Commit bereits im Speicher vorhanden. Die expliziten db.refresh()-Aufrufe nach db.commit() in allen Routers sind daher redundant und wurden entfernt. test_rls.py: 6 neue Tests beweisen DB-seitige Mandanten-Isolation. conftest.py: _apply_rls() wendet RLS-Policies auf Test-DB an. migrations/0024: korrigiert auf op.execute(text()) API. migrations/env.py: SET LOCAL außerhalb Transaktion entfernt. Ergebnis: 8 failed (pre-existing), 126 passed – identisch zur Baseline vor RLS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
5.3 KiB
Python
142 lines
5.3 KiB
Python
"""
|
|
CalDAV-Konfiguration und manueller Sync-Trigger.
|
|
|
|
Firmenkalender: nur COMPANY_ADMIN / SUPER_ADMIN
|
|
Persönlicher Kalender: jeder eingeloggte Nutzer für sich selbst
|
|
"""
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.dependencies import CurrentUser, require_role
|
|
from app.models.caldav_config import CaldavCompanyConfig, CaldavUserConfig
|
|
from app.models.user import User, UserRole
|
|
from app.schemas.caldav import (
|
|
CaldavCompanyConfigOut,
|
|
CaldavCompanyConfigSave,
|
|
CaldavUserConfigOut,
|
|
CaldavUserConfigSave,
|
|
ResyncResult,
|
|
)
|
|
from app.services.caldav_service import caldav_service, encrypt_password
|
|
|
|
router = APIRouter(prefix="/caldav", tags=["CalDAV"])
|
|
|
|
_admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
|
|
|
|
|
|
# ── Firmenkalender ─────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/company/config", response_model=CaldavCompanyConfigOut | None)
|
|
async def get_company_config(
|
|
current_user: User = require_role(*_admin_roles),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
cfg = await caldav_service.get_company_config(current_user.company_id, db)
|
|
return CaldavCompanyConfigOut.model_validate(cfg) if cfg else None
|
|
|
|
|
|
@router.post("/company/config", response_model=CaldavCompanyConfigOut)
|
|
async def save_company_config(
|
|
data: CaldavCompanyConfigSave,
|
|
current_user: User = require_role(*_admin_roles),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
cfg = await caldav_service.get_company_config(current_user.company_id, db)
|
|
if cfg is None:
|
|
cfg = CaldavCompanyConfig(company_id=current_user.company_id, id=uuid.uuid4())
|
|
db.add(cfg)
|
|
|
|
cfg.enabled = data.enabled
|
|
cfg.principal_url = data.principal_url
|
|
cfg.calendar_url = data.calendar_url
|
|
cfg.username = data.username
|
|
cfg.calendar_display_name = data.calendar_display_name
|
|
cfg.verify_ssl = data.verify_ssl
|
|
if data.password:
|
|
cfg.password_encrypted = encrypt_password(data.password)
|
|
elif not cfg.password_encrypted:
|
|
raise HTTPException(status_code=400, detail="Passwort wird beim ersten Speichern benötigt.")
|
|
|
|
await db.commit()
|
|
return CaldavCompanyConfigOut.model_validate(cfg)
|
|
|
|
|
|
@router.post("/company/test")
|
|
async def test_company_config(
|
|
current_user: User = require_role(*_admin_roles),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
cfg = await caldav_service.get_company_config(current_user.company_id, db)
|
|
if not cfg:
|
|
raise HTTPException(status_code=404, detail="Keine Firmen-CalDAV-Konfiguration vorhanden.")
|
|
result = await caldav_service.test_config(cfg)
|
|
if not result["ok"]:
|
|
raise HTTPException(status_code=502, detail=result["error"])
|
|
return {"message": "Verbindung erfolgreich.", "http_status": result.get("status")}
|
|
|
|
|
|
@router.post("/company/resync", response_model=ResyncResult)
|
|
async def resync_all(
|
|
current_user: User = require_role(*_admin_roles),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Alle genehmigten Abwesenheiten neu in den Firmenkalender synchronisieren."""
|
|
result = await caldav_service.resync_all_approved(current_user.company_id, db)
|
|
await db.commit()
|
|
return ResyncResult(**result)
|
|
|
|
|
|
# ── Persönlicher Kalender ──────────────────────────────────────────────────────
|
|
|
|
@router.get("/user/config", response_model=CaldavUserConfigOut | None)
|
|
async def get_user_config(
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
cfg = await caldav_service.get_user_config(current_user.id, db)
|
|
return CaldavUserConfigOut.model_validate(cfg) if cfg else None
|
|
|
|
|
|
@router.post("/user/config", response_model=CaldavUserConfigOut)
|
|
async def save_user_config(
|
|
data: CaldavUserConfigSave,
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
cfg = await caldav_service.get_user_config(current_user.id, db)
|
|
if cfg is None:
|
|
cfg = CaldavUserConfig(user_id=current_user.id, id=uuid.uuid4())
|
|
db.add(cfg)
|
|
|
|
cfg.enabled = data.enabled
|
|
cfg.principal_url = data.principal_url
|
|
cfg.calendar_url = data.calendar_url
|
|
cfg.username = data.username
|
|
cfg.calendar_display_name = data.calendar_display_name
|
|
cfg.verify_ssl = data.verify_ssl
|
|
if data.password:
|
|
cfg.password_encrypted = encrypt_password(data.password)
|
|
elif not cfg.password_encrypted:
|
|
raise HTTPException(status_code=400, detail="Passwort wird beim ersten Speichern benötigt.")
|
|
|
|
await db.commit()
|
|
return CaldavUserConfigOut.model_validate(cfg)
|
|
|
|
|
|
@router.post("/user/test")
|
|
async def test_user_config(
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
cfg = await caldav_service.get_user_config(current_user.id, db)
|
|
if not cfg:
|
|
raise HTTPException(status_code=404, detail="Keine persönliche CalDAV-Konfiguration vorhanden.")
|
|
result = await caldav_service.test_config(cfg)
|
|
if not result["ok"]:
|
|
raise HTTPException(status_code=502, detail=result["error"])
|
|
return {"message": "Verbindung erfolgreich.", "http_status": result.get("status")}
|