Improve auth models
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# services/tenant/app/services/tenant_service.py
|
||||
"""
|
||||
Tenant service business logic
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, and_
|
||||
from fastapi import HTTPException, status
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from app.models.tenants import Tenant, TenantMember
|
||||
from app.schemas.tenants import BakeryRegistration, TenantResponse, TenantAccessResponse, TenantUpdate
|
||||
from app.services.messaging import publish_tenant_created, publish_member_added
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class TenantService:
|
||||
"""Tenant management business logic"""
|
||||
|
||||
@staticmethod
|
||||
async def create_bakery(bakery_data: BakeryRegistration, owner_id: str, db: AsyncSession) -> TenantResponse:
|
||||
"""Create a new bakery/tenant"""
|
||||
|
||||
try:
|
||||
# Generate subdomain if not provided
|
||||
subdomain = bakery_data.name.lower().replace(' ', '-').replace('á', 'a').replace('é', 'e').replace('í', 'i').replace('ó', 'o').replace('ú', 'u')
|
||||
subdomain = ''.join(c for c in subdomain if c.isalnum() or c == '-')
|
||||
|
||||
# Check if subdomain already exists
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.subdomain == subdomain)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
subdomain = f"{subdomain}-{uuid.uuid4().hex[:6]}"
|
||||
|
||||
# Create tenant
|
||||
tenant = Tenant(
|
||||
name=bakery_data.name,
|
||||
subdomain=subdomain,
|
||||
business_type=bakery_data.business_type,
|
||||
address=bakery_data.address,
|
||||
city=bakery_data.city,
|
||||
postal_code=bakery_data.postal_code,
|
||||
phone=bakery_data.phone,
|
||||
owner_id=owner_id,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(tenant)
|
||||
await db.commit()
|
||||
await db.refresh(tenant)
|
||||
|
||||
# Create owner membership
|
||||
owner_membership = TenantMember(
|
||||
tenant_id=tenant.id,
|
||||
user_id=owner_id,
|
||||
role="owner",
|
||||
permissions=json.dumps(["read", "write", "admin", "delete"]),
|
||||
is_active=True,
|
||||
joined_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.add(owner_membership)
|
||||
await db.commit()
|
||||
|
||||
# Publish event
|
||||
await publish_tenant_created(str(tenant.id), owner_id, bakery_data.name)
|
||||
|
||||
logger.info(f"Bakery created: {bakery_data.name} (ID: {tenant.id})")
|
||||
|
||||
return TenantResponse.from_orm(tenant)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Error creating bakery: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create bakery"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def verify_user_access(user_id: str, tenant_id: str, db: AsyncSession) -> TenantAccessResponse:
|
||||
"""Verify if user has access to tenant"""
|
||||
|
||||
try:
|
||||
# Check if user is tenant member
|
||||
result = await db.execute(
|
||||
select(TenantMember).where(
|
||||
and_(
|
||||
TenantMember.user_id == user_id,
|
||||
TenantMember.tenant_id == tenant_id,
|
||||
TenantMember.is_active == True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
membership = result.scalar_one_or_none()
|
||||
|
||||
if not membership:
|
||||
return TenantAccessResponse(
|
||||
has_access=False,
|
||||
role="none",
|
||||
permissions=[]
|
||||
)
|
||||
|
||||
# Parse permissions
|
||||
permissions = []
|
||||
if membership.permissions:
|
||||
try:
|
||||
permissions = json.loads(membership.permissions)
|
||||
except:
|
||||
permissions = []
|
||||
|
||||
return TenantAccessResponse(
|
||||
has_access=True,
|
||||
role=membership.role,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying user access: {e}")
|
||||
return TenantAccessResponse(
|
||||
has_access=False,
|
||||
role="none",
|
||||
permissions=[]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_user_tenants(user_id: str, db: AsyncSession) -> List[TenantResponse]:
|
||||
"""Get all tenants accessible by user"""
|
||||
|
||||
try:
|
||||
# Get user's tenant memberships
|
||||
result = await db.execute(
|
||||
select(Tenant)
|
||||
.join(TenantMember, Tenant.id == TenantMember.tenant_id)
|
||||
.where(
|
||||
and_(
|
||||
TenantMember.user_id == user_id,
|
||||
TenantMember.is_active == True,
|
||||
Tenant.is_active == True
|
||||
)
|
||||
)
|
||||
.order_by(Tenant.name)
|
||||
)
|
||||
|
||||
tenants = result.scalars().all()
|
||||
return [TenantResponse.from_orm(tenant) for tenant in tenants]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user tenants: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
async def get_tenant_by_id(tenant_id: str, db: AsyncSession) -> Optional[TenantResponse]:
|
||||
"""Get tenant by ID"""
|
||||
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
|
||||
tenant = result.scalar_one_or_none()
|
||||
if tenant:
|
||||
return TenantResponse.from_orm(tenant)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tenant: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def update_tenant(tenant_id: str, update_data: TenantUpdate, user_id: str, db: AsyncSession) -> TenantResponse:
|
||||
"""Update tenant information"""
|
||||
|
||||
try:
|
||||
# Verify user has admin access
|
||||
access = await TenantService.verify_user_access(user_id, tenant_id, db)
|
||||
if not access.has_access or access.role not in ["owner", "admin"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to update tenant"
|
||||
)
|
||||
|
||||
# Update tenant
|
||||
update_values = update_data.dict(exclude_unset=True)
|
||||
if update_values:
|
||||
update_values["updated_at"] = datetime.now(timezone.utc)
|
||||
|
||||
await db.execute(
|
||||
update(Tenant)
|
||||
.where(Tenant.id == tenant_id)
|
||||
.values(**update_values)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# Get updated tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
|
||||
tenant = result.scalar_one()
|
||||
logger.info(f"Tenant updated: {tenant.name} (ID: {tenant_id})")
|
||||
|
||||
return TenantResponse.from_orm(tenant)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Error updating tenant: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update tenant"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def add_team_member(tenant_id: str, user_id: str, role: str, invited_by: str, db: AsyncSession) -> TenantMemberResponse:
|
||||
"""Add a team member to tenant"""
|
||||
|
||||
try:
|
||||
# Verify inviter has admin access
|
||||
access = await TenantService.verify_user_access(invited_by, tenant_id, db)
|
||||
if not access.has_access or access.role not in ["owner", "admin"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to add team members"
|
||||
)
|
||||
|
||||
# Check if user is already a member
|
||||
result = await db.execute(
|
||||
select(TenantMember).where(
|
||||
and_(
|
||||
TenantMember.tenant_id == tenant_id,
|
||||
TenantMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
existing_member = result.scalar_one_or_none()
|
||||
if existing_member:
|
||||
if existing_member.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User is already a member of this tenant"
|
||||
)
|
||||
else:
|
||||
# Reactivate existing membership
|
||||
existing_member.is_active = True
|
||||
existing_member.role = role
|
||||
existing_member.joined_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
return TenantMemberResponse.from_orm(existing_member)
|
||||
|
||||
# Create new membership
|
||||
permissions = ["read"]
|
||||
if role in ["admin", "owner"]:
|
||||
permissions.extend(["write", "admin"])
|
||||
if role == "owner":
|
||||
permissions.append("delete")
|
||||
|
||||
member = TenantMember(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
permissions=json.dumps(permissions),
|
||||
invited_by=invited_by,
|
||||
is_active=True,
|
||||
joined_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.add(member)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
|
||||
# Publish event
|
||||
await publish_member_added(tenant_id, user_id, role)
|
||||
|
||||
logger.info(f"Team member added: {user_id} to tenant {tenant_id} as {role}")
|
||||
|
||||
return TenantMemberResponse.from_orm(member)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Error adding team member: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to add team member"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user