211 lines
6.7 KiB
Python
211 lines
6.7 KiB
Python
"""
|
|
/auth — Sign-up, sign-in (delegates to Keycloak)
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from datetime import datetime, timedelta
|
|
from slugify import slugify
|
|
import secrets
|
|
|
|
from ..core.database import get_db
|
|
from ..core.config import get_settings
|
|
from ..models.tenant import Tenant, User, Plan, TenantTool, ProvisioningJob, AuditLog
|
|
from ..schemas.tenant import SignUpRequest, TenantResponse
|
|
from ..services import keycloak_service, provisioner
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
async def _default_plan(db: AsyncSession) -> Plan:
|
|
settings = get_settings()
|
|
result = await db.execute(select(Plan).where(Plan.name == settings.DEFAULT_TRIAL_DAYS or "trial"))
|
|
plan = result.scalar_one_or_none()
|
|
if not plan:
|
|
result = await db.execute(select(Plan).where(Plan.name == "trial"))
|
|
plan = result.scalar_one()
|
|
return plan
|
|
|
|
|
|
@router.post("/signup", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
|
|
async def signup(
|
|
payload: SignUpRequest,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""
|
|
User signs up with company name → creates Tenant + Keycloak realm + starts trial.
|
|
vCluster provisioning is triggered asynchronously.
|
|
"""
|
|
s = get_settings()
|
|
slug = slugify(payload.company_name, max_length=40, separator="-")
|
|
|
|
# Check slug uniqueness
|
|
existing = await db.execute(select(Tenant).where(Tenant.slug == slug))
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=f"Company name '{payload.company_name}' is already registered."
|
|
)
|
|
|
|
plan = await _default_plan(db)
|
|
now = datetime.utcnow()
|
|
trial_days = plan.trial_days
|
|
|
|
# Create tenant record
|
|
tenant = Tenant(
|
|
slug=slug,
|
|
company_name=payload.company_name,
|
|
plan_id=plan.id,
|
|
status="trial",
|
|
trial_started_at=now,
|
|
trial_ends_at=now + timedelta(days=trial_days),
|
|
trial_duration_days=trial_days,
|
|
vcluster_status="provisioning",
|
|
tools_enabled=plan.features,
|
|
billing_email=payload.email,
|
|
keycloak_realm=f"tenant-{slug}",
|
|
sso_type="system",
|
|
)
|
|
db.add(tenant)
|
|
await db.flush()
|
|
|
|
# Create owner user record
|
|
user = User(
|
|
tenant_id=tenant.id,
|
|
email=payload.email,
|
|
first_name=payload.first_name,
|
|
last_name=payload.last_name,
|
|
role="owner",
|
|
status="active",
|
|
)
|
|
db.add(user)
|
|
|
|
# Default tool rows
|
|
for tool, enabled in plan.features.items():
|
|
db.add(TenantTool(tenant_id=tenant.id, tool_name=tool, enabled=bool(enabled)))
|
|
|
|
# Provisioning job
|
|
job = ProvisioningJob(
|
|
tenant_id=tenant.id,
|
|
job_type="create_vcluster",
|
|
payload={
|
|
"slug": slug,
|
|
"company_name": payload.company_name,
|
|
"plan": plan.name,
|
|
"tools": plan.features,
|
|
"admin_email": payload.email,
|
|
}
|
|
)
|
|
db.add(job)
|
|
db.add(AuditLog(
|
|
tenant_id=tenant.id,
|
|
action="tenant.signup",
|
|
details={"email": payload.email, "plan": plan.name},
|
|
))
|
|
await db.commit()
|
|
await db.refresh(tenant)
|
|
|
|
# Async: create Keycloak realm + provision vCluster
|
|
background_tasks.add_task(
|
|
_provision_async,
|
|
tenant_id=str(tenant.id),
|
|
job_id=str(job.id),
|
|
slug=slug,
|
|
company_name=payload.company_name,
|
|
admin_email=payload.email,
|
|
plan_name=plan.name,
|
|
tools=plan.features,
|
|
)
|
|
|
|
return _to_response(tenant, plan.name)
|
|
|
|
|
|
async def _provision_async(tenant_id, job_id, slug, company_name,
|
|
admin_email, plan_name, tools):
|
|
"""Background task: Keycloak realm + GitOps vCluster write."""
|
|
from ..core.database import get_session_factory
|
|
from sqlalchemy import update as sa_update
|
|
from datetime import datetime
|
|
import logging
|
|
log = logging.getLogger(__name__)
|
|
|
|
factory = get_session_factory()
|
|
async with factory() as db:
|
|
try:
|
|
# Mark job running
|
|
await db.execute(
|
|
sa_update(ProvisioningJob)
|
|
.where(ProvisioningJob.id == job_id)
|
|
.values(status="running", started_at=datetime.utcnow())
|
|
)
|
|
await db.commit()
|
|
|
|
# 1. Create Keycloak realm
|
|
await keycloak_service.create_tenant_realm(slug, company_name, admin_email)
|
|
|
|
# 2. Write gitops manifests
|
|
result = await provisioner.provision_tenant(
|
|
tenant_slug=slug,
|
|
company_name=company_name,
|
|
plan_name=plan_name,
|
|
tools_enabled=tools,
|
|
)
|
|
|
|
# 3. Update tenant with vcluster info
|
|
await db.execute(
|
|
sa_update(Tenant)
|
|
.where(Tenant.id == tenant_id)
|
|
.values(
|
|
vcluster_name=result["vcluster_name"],
|
|
vcluster_url=result["vcluster_url"],
|
|
vcluster_status="ready",
|
|
status="trial",
|
|
)
|
|
)
|
|
await db.execute(
|
|
sa_update(ProvisioningJob)
|
|
.where(ProvisioningJob.id == job_id)
|
|
.values(status="succeeded", completed_at=datetime.utcnow())
|
|
)
|
|
await db.commit()
|
|
log.info("Provisioned tenant %s", slug)
|
|
|
|
except Exception as exc:
|
|
log.exception("Provisioning failed for %s", slug)
|
|
await db.execute(
|
|
sa_update(ProvisioningJob)
|
|
.where(ProvisioningJob.id == job_id)
|
|
.values(status="failed", error=str(exc), completed_at=datetime.utcnow())
|
|
)
|
|
await db.execute(
|
|
sa_update(Tenant)
|
|
.where(Tenant.id == tenant_id)
|
|
.values(vcluster_status="not_created")
|
|
)
|
|
await db.commit()
|
|
|
|
|
|
def _to_response(tenant: Tenant, plan_name: str) -> TenantResponse:
|
|
s = get_settings()
|
|
dp_url = (
|
|
f"https://data.{s.DATA_PLANE_DOMAIN}?tenant={tenant.slug}"
|
|
if tenant.vcluster_status in ("ready", "provisioning")
|
|
else None
|
|
)
|
|
return TenantResponse(
|
|
id=tenant.id,
|
|
slug=tenant.slug,
|
|
company_name=tenant.company_name,
|
|
status=tenant.status,
|
|
plan_name=plan_name,
|
|
trial_ends_at=tenant.trial_ends_at,
|
|
vcluster_status=tenant.vcluster_status,
|
|
vcluster_url=tenant.vcluster_url,
|
|
data_plane_url=dp_url,
|
|
tools_enabled=tenant.tools_enabled,
|
|
keycloak_realm=tenant.keycloak_realm,
|
|
sso_type=tenant.sso_type,
|
|
created_at=tenant.created_at,
|
|
)
|