Files
PecHub/backend/app/services/tenant_service.py
T
mgiustini 58a233236c feat: Fase 1 – Fondamenta complete (backend FastAPI + auth + permessi)
- docker-compose.yml: PostgreSQL 16, Redis 7, MinIO, Nginx
- backend FastAPI: struttura monorepo, config pydantic-settings
- modelli SQLAlchemy: tutti i modelli (tenants, users, mailboxes, messages, archival, permissions, labels, audit_log)
- migrazione Alembic 0001: schema completo in pure SQL
- auth API: login JWT, refresh token rotation, logout, 2FA TOTP (setup/verify/disable)
- CRUD utenti: lista, crea, modifica, reset password, soft delete
- permessi granulari (Fase 1-A): mailbox_permissions, assegna/revoca/lista
- CRUD tenant: gestione super-admin
- sicurezza: AES-256-GCM cifratura credenziali IMAP/SMTP, bcrypt password
- RLS PostgreSQL: isolamento multi-tenant per request
- seed sviluppo: tenant demo + admin + operator
- test unit: security (bcrypt, JWT, AES), auth_service
- test integration: auth endpoints, users endpoints
- CI GitHub Actions: lint (ruff), test (pytest), build Docker, security scan
- infra: nginx.conf, redis.conf
- Makefile con comandi make dev/test/migrate/seed

Definition of Done:
 Login, refresh token e TOTP funzionanti
 make dev porta in piedi tutto lo stack locale
 CI configurata
2026-03-18 16:42:01 +01:00

82 lines
2.5 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.
"""
Servizio tenant gestione organizzazioni (solo super_admin).
"""
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import ConflictError, NotFoundError
from app.core.security import hash_password
from app.models.tenant import Tenant
from app.models.user import User
from app.schemas.tenant import TenantCreateRequest, TenantUpdateRequest
class TenantService:
def __init__(self, db: AsyncSession) -> None:
self.db = db
async def create_tenant(self, data: TenantCreateRequest) -> tuple[Tenant, User]:
"""Crea un nuovo tenant con il suo utente admin iniziale."""
# Verifica slug univoco
existing = await self.db.execute(
select(Tenant).where(Tenant.slug == data.slug)
)
if existing.scalar_one_or_none():
raise ConflictError(f"Slug '{data.slug}' già in uso")
tenant = Tenant(
slug=data.slug,
name=data.name,
plan=data.plan,
max_mailboxes=data.max_mailboxes,
max_users=data.max_users,
)
self.db.add(tenant)
await self.db.flush() # ottieni tenant.id
# Crea utente admin iniziale
admin = User(
tenant_id=tenant.id,
email=data.admin_email.lower(),
password_hash=hash_password(data.admin_password),
full_name=data.admin_full_name,
role="admin",
)
self.db.add(admin)
await self.db.flush()
return tenant, admin
async def get_tenant(self, tenant_id: uuid.UUID) -> Tenant:
tenant = await self.db.get(Tenant, tenant_id)
if not tenant:
raise NotFoundError("tenant")
return tenant
async def list_tenants(self) -> list[Tenant]:
result = await self.db.execute(
select(Tenant).order_by(Tenant.created_at.desc())
)
return list(result.scalars().all())
async def update_tenant(
self, tenant_id: uuid.UUID, data: TenantUpdateRequest
) -> Tenant:
tenant = await self.get_tenant(tenant_id)
if data.name is not None:
tenant.name = data.name
if data.plan is not None:
tenant.plan = data.plan
if data.is_active is not None:
tenant.is_active = data.is_active
if data.max_mailboxes is not None:
tenant.max_mailboxes = data.max_mailboxes
if data.max_users is not None:
tenant.max_users = data.max_users
return tenant