727d86614e
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
101 lines
2.6 KiB
Python
101 lines
2.6 KiB
Python
"""Tenant management service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.tenant import Tenant
|
|
from app.models.user import User, UserTenant
|
|
|
|
|
|
class TenantService:
|
|
"""Handles tenant CRUD and user-tenant membership."""
|
|
|
|
async def list_tenants_for_user(
|
|
self,
|
|
db: AsyncSession,
|
|
user_id: uuid.UUID,
|
|
) -> list[dict[str, Any]]:
|
|
"""List all tenants a user belongs to."""
|
|
q = (
|
|
select(Tenant, UserTenant.is_default)
|
|
.join(UserTenant, UserTenant.tenant_id == Tenant.id)
|
|
.where(UserTenant.user_id == user_id)
|
|
)
|
|
result = await db.execute(q)
|
|
rows = result.all()
|
|
return [
|
|
{
|
|
"id": str(t.id),
|
|
"name": t.name,
|
|
"slug": t.slug,
|
|
"is_default": is_default,
|
|
}
|
|
for t, is_default in rows
|
|
]
|
|
|
|
async def create_tenant(
|
|
self,
|
|
db: AsyncSession,
|
|
name: str,
|
|
slug: str,
|
|
) -> Tenant:
|
|
"""Create a new tenant."""
|
|
tenant = Tenant(name=name, slug=slug)
|
|
db.add(tenant)
|
|
await db.flush()
|
|
return tenant
|
|
|
|
async def get_tenant(self, db: AsyncSession, tenant_id: uuid.UUID) -> Tenant | None:
|
|
"""Get a tenant by ID."""
|
|
q = select(Tenant).where(Tenant.id == tenant_id)
|
|
result = await db.execute(q)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def list_tenant_users(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: uuid.UUID,
|
|
) -> list[dict[str, Any]]:
|
|
"""List users in a tenant via UserTenant association."""
|
|
q = (
|
|
select(User, UserTenant)
|
|
.join(UserTenant, UserTenant.user_id == User.id)
|
|
.where(UserTenant.tenant_id == tenant_id)
|
|
)
|
|
result = await db.execute(q)
|
|
rows = result.all()
|
|
return [
|
|
{
|
|
"id": str(u.id),
|
|
"email": u.email,
|
|
"name": u.name,
|
|
"role": ut.role,
|
|
"is_active": u.is_active,
|
|
}
|
|
for u, ut in rows
|
|
]
|
|
|
|
async def assign_user_to_tenant(
|
|
self,
|
|
db: AsyncSession,
|
|
tenant_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
) -> UserTenant:
|
|
"""Assign a user to a tenant."""
|
|
ut = UserTenant(
|
|
user_id=user_id,
|
|
tenant_id=tenant_id,
|
|
is_default=False,
|
|
)
|
|
db.add(ut)
|
|
await db.flush()
|
|
return ut
|
|
|
|
|
|
tenant_service = TenantService()
|