Files
timemaster/backend/app/routers/reseller.py
T
patrickandClaude Opus 4.8 98cb2f867a
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled
feat: Mandant/Reseller ohne E-Mail anlegbar (Temp-Passwort statt Einladung)
- admin_email bzw. reseller email optional; ohne E-Mail wird interne Login-
  Kennung (<name>@<slug>.local) + einmaliges Temp-Passwort erzeugt, Account
  sofort aktiv, kein Mailversand
- TenantOut/ResellerOut: initial_password (einmalig) ergänzt
- LoginRequest.email: str statt EmailStr (Kennung muss kein zustellbares
  Postfach sein; .local-Domains sind sonst nicht einloggbar)
- Frontend: E-Mail-Felder optional, CredDialog zeigt Login + Temp-Passwort einmalig
- Test: Anlage ohne E-Mail + Login mit generierten Zugangsdaten; 173/173 grün

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:34:23 +02:00

86 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reseller-Selbstverwaltung: ein Reseller verwaltet ausschließlich die von ihm
angelegten Firmen (Firmen-Ebene, keine personenbezogenen Daten).
RLS (app.reseller_id) erzwingt, dass nur eigene Firmen sichtbar/änderbar sind
ein Zugriff auf eine fremde Firmen-ID läuft ins Leere (404).
"""
from uuid import UUID
from fastapi import APIRouter, Depends
from fastapi import HTTPException
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import require_role
from app.models.audit_log import AuditLog
from app.models.company import Company
from app.models.user import User, UserRole
from app.schemas.reseller import TenantCreate, TenantOut, TenantUpdate
from app.services.tenant_service import tenant_service
router = APIRouter(prefix="/reseller", tags=["Reseller"])
_reseller_roles = (UserRole.RESELLER,)
@router.get("/companies", response_model=list[TenantOut])
async def list_my_companies(
current_user: User = require_role(*_reseller_roles),
db: AsyncSession = Depends(get_db),
):
return await tenant_service.list_tenants(db)
@router.post("/companies", response_model=TenantOut, status_code=201)
async def create_company(
data: TenantCreate,
current_user: User = require_role(*_reseller_roles),
db: AsyncSession = Depends(get_db),
):
company, admin, initial_password = await tenant_service.create_tenant(
data, actor=current_user, db=db, reseller_id=current_user.id,
)
# Antwort aus der frisch erstellten Firma; bei Einladung Account inaktiv,
# bei Temp-Passwort sofort aktiv.
return TenantOut(
id=company.id, name=company.name, slug=company.slug, plan=company.plan,
is_active=company.is_active, country=company.country, reseller_id=company.reseller_id,
reseller_name=f"{current_user.first_name} {current_user.last_name}".strip(),
user_count=1, active_user_count=1 if initial_password else 0,
admin_email=admin.email, initial_password=initial_password,
)
@router.patch("/companies/{company_id}", response_model=TenantOut)
async def update_company(
company_id: UUID,
data: TenantUpdate,
current_user: User = require_role(*_reseller_roles),
db: AsyncSession = Depends(get_db),
):
company = await db.get(Company, company_id)
# RLS gibt nur eigene Firmen frei → fremde/unbekannte ID = nicht sichtbar
if company is None or company.reseller_id != current_user.id:
raise HTTPException(status_code=404, detail="Firma nicht gefunden.")
changes = data.model_dump(exclude_none=True)
# Eigentümerschaft ist geprüft → Schreiben (inkl. AuditLog) unter kurzem Bypass,
# da der Reseller keine app.company_id besitzt (audit_logs ist company-gefenced).
await db.execute(text("SET LOCAL app.bypass_rls = 'on'"))
try:
for field, value in changes.items():
setattr(company, field, value)
if changes:
db.add(AuditLog(
company_id=company.id, user_id=current_user.id,
action="tenant_updated", entity_type="company", entity_id=company.id,
new_value=changes,
))
await db.flush()
finally:
await db.execute(text("SET LOCAL app.bypass_rls = 'off'"))
tenants = {t.id: t for t in await tenant_service.list_tenants(db)}
return tenants[company.id]