Files
patrick dd3e069466 fix: router db.refresh() nach commit bricht RLS-Kontext
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>
2026-05-23 22:34:48 +02:00

92 lines
2.9 KiB
Python

"""
SMTP-Konfiguration pro Firma.
Nur COMPANY_ADMIN / SUPER_ADMIN darf lesen und schreiben.
"""
import base64
import hashlib
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 require_role
from app.models.smtp_config import SmtpConfig
from app.models.user import User, UserRole
from app.schemas.smtp import SmtpConfigOut, SmtpConfigSave, SmtpTestRequest
from app.services.email_service import email_service
router = APIRouter(prefix="/smtp", tags=["SMTP-Konfiguration"])
_admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
def _encrypt(plain: str) -> str:
from app.core.config import settings
from cryptography.fernet import Fernet
key = hashlib.sha256(settings.secret_key.encode()).digest()
f = Fernet(base64.urlsafe_b64encode(key))
return f.encrypt(plain.encode()).decode()
async def _get_config(company_id: uuid.UUID, db: AsyncSession) -> SmtpConfig | None:
return await db.scalar(select(SmtpConfig).where(SmtpConfig.company_id == company_id))
@router.get("/config", response_model=SmtpConfigOut | None)
async def get_smtp_config(
current_user: User = require_role(*_admin_roles),
db: AsyncSession = Depends(get_db),
):
cfg = await _get_config(current_user.company_id, db)
if not cfg:
return None
return SmtpConfigOut.model_validate(cfg)
@router.post("/config", response_model=SmtpConfigOut)
async def save_smtp_config(
data: SmtpConfigSave,
current_user: User = require_role(*_admin_roles),
db: AsyncSession = Depends(get_db),
):
"""Erstellt oder überschreibt die SMTP-Konfiguration der Firma."""
cfg = await _get_config(current_user.company_id, db)
if cfg is None:
cfg = SmtpConfig(company_id=current_user.company_id, id=uuid.uuid4())
db.add(cfg)
cfg.host = data.host
cfg.port = data.port
cfg.use_tls = data.use_tls
cfg.use_starttls = data.use_starttls
cfg.username = data.username
cfg.from_email = data.from_email
cfg.from_name = data.from_name
cfg.is_enabled = data.is_enabled
if data.password is not None:
cfg.password_encrypted = _encrypt(data.password)
await db.commit()
return SmtpConfigOut.model_validate(cfg)
@router.post("/test")
async def test_smtp(
data: SmtpTestRequest,
current_user: User = require_role(*_admin_roles),
db: AsyncSession = Depends(get_db),
):
"""Sendet eine Test-E-Mail mit der aktuellen Konfiguration."""
cfg = await _get_config(current_user.company_id, db)
if not cfg:
raise HTTPException(status_code=404, detail="Keine SMTP-Konfiguration vorhanden.")
try:
await email_service.send_test(cfg, data.to)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"SMTP-Fehler: {exc}")
return {"message": f"Test-E-Mail an {data.to} verschickt."}