LetsBeBiz-Redesign/letsbe-orchestrator/app/routes/tenants.py

186 lines
5.2 KiB
Python
Raw Normal View History

"""Tenant management endpoints."""
import hashlib
import secrets
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from app.db import AsyncSessionDep
from app.dependencies import AdminAuthDep
from app.models.tenant import Tenant
from app.schemas.tenant import TenantCreate, TenantResponse
class SetDashboardTokenRequest(BaseModel):
"""Request body for setting dashboard token."""
token: str | None = Field(
None,
min_length=32,
max_length=128,
description="Dashboard token (32-128 chars). If None, generates a new token.",
)
class SetDashboardTokenResponse(BaseModel):
"""Response after setting dashboard token."""
token: str = Field(..., description="The dashboard token (only shown once)")
message: str = Field(default="Dashboard token configured successfully")
router = APIRouter(prefix="/tenants", tags=["Tenants"])
# --- Helper functions (embryonic service layer) ---
async def create_tenant(db: AsyncSessionDep, tenant_in: TenantCreate) -> Tenant:
"""Create a new tenant in the database."""
tenant = Tenant(
name=tenant_in.name,
domain=tenant_in.domain,
)
db.add(tenant)
await db.commit()
await db.refresh(tenant)
return tenant
async def get_tenants(db: AsyncSessionDep) -> list[Tenant]:
"""Retrieve all tenants from the database."""
result = await db.execute(select(Tenant).order_by(Tenant.created_at.desc()))
return list(result.scalars().all())
async def get_tenant_by_id(db: AsyncSessionDep, tenant_id: uuid.UUID) -> Tenant | None:
"""Retrieve a tenant by ID."""
result = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
return result.scalar_one_or_none()
# --- Route handlers (thin controllers) ---
@router.post("", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
async def create_tenant_endpoint(
tenant_in: TenantCreate,
db: AsyncSessionDep,
) -> Tenant:
"""
Create a new tenant.
- **name**: Unique tenant name (required)
- **domain**: Optional domain for the tenant
"""
return await create_tenant(db, tenant_in)
@router.get("", response_model=list[TenantResponse])
async def list_tenants_endpoint(db: AsyncSessionDep) -> list[Tenant]:
"""
List all tenants.
Returns a list of all registered tenants.
"""
return await get_tenants(db)
@router.get("/{tenant_id}", response_model=TenantResponse)
async def get_tenant_endpoint(
tenant_id: uuid.UUID,
db: AsyncSessionDep,
) -> Tenant:
"""
Get a tenant by ID.
Returns the tenant with the specified UUID.
"""
tenant = await get_tenant_by_id(db, tenant_id)
if tenant is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tenant {tenant_id} not found",
)
return tenant
@router.post(
"/{tenant_id}/dashboard-token",
response_model=SetDashboardTokenResponse,
dependencies=[AdminAuthDep],
)
async def set_dashboard_token_endpoint(
tenant_id: uuid.UUID,
db: AsyncSessionDep,
request: SetDashboardTokenRequest | None = None,
) -> SetDashboardTokenResponse:
"""
Set or regenerate dashboard token for a tenant.
**Admin-only endpoint** - requires X-Admin-Api-Key header.
This token is used by the tenant's dashboard (Hub Dashboard or Control Panel)
to authenticate requests to the Orchestrator.
- If `token` is provided, it will be used (must be 32-128 characters)
- If `token` is None or not provided, a secure 48-character token is generated
**IMPORTANT**: The plaintext token is only returned once. Store it securely.
"""
# Get tenant
tenant = await get_tenant_by_id(db, tenant_id)
if tenant is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tenant {tenant_id} not found",
)
# Generate or use provided token
if request and request.token:
token = request.token
else:
# Generate secure random token (48 chars = 192 bits of entropy)
token = secrets.token_hex(24)
# Store SHA-256 hash of token
token_hash = hashlib.sha256(token.encode()).hexdigest()
tenant.dashboard_token_hash = token_hash
await db.commit()
return SetDashboardTokenResponse(
token=token,
message="Dashboard token configured successfully. Store this token securely - it will not be shown again.",
)
@router.delete(
"/{tenant_id}/dashboard-token",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[AdminAuthDep],
)
async def revoke_dashboard_token_endpoint(
tenant_id: uuid.UUID,
db: AsyncSessionDep,
) -> None:
"""
Revoke/remove dashboard token for a tenant.
**Admin-only endpoint** - requires X-Admin-Api-Key header.
After revocation, the tenant's dashboard will no longer be able to
authenticate with the Orchestrator until a new token is set.
"""
tenant = await get_tenant_by_id(db, tenant_id)
if tenant is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tenant {tenant_id} not found",
)
tenant.dashboard_token_hash = None
await db.commit()