swiftops-backend / src /app /api /v1 /clients.py
kamau1's picture
Enhance authorization for client and contractor creation and updates
c9509b3
"""
Client Management Endpoints
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List
from uuid import UUID
import logging
from app.api.deps import get_db, get_current_active_user
from app.models.client import Client
from app.models.user import User
from app.schemas.client import ClientCreate, ClientUpdate, ClientResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/clients", tags=["Clients"])
@router.post("", response_model=ClientResponse, status_code=status.HTTP_201_CREATED)
async def create_client(
client_data: ClientCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Create a new client organization
**Requires:** platform_admin, client_admin, or contractor_admin role
**Business Logic:**
- Platform admins can create any client
- Client admins can create clients (to track their own organization or partners)
- Contractor admins can create clients they work with
This enables self-service onboarding: when a contractor needs to work with a
new client not yet in the system, they can create the client record themselves.
- **name**: Client name (must be unique)
- **industry**: Industry type (e.g., 'Telecommunications', 'Utilities')
- **main_email**: Primary contact email
- **main_phone**: Primary contact phone
- **default_sla_days**: Default SLA window in days
"""
# Check authorization - platform_admin, client_admin, contractor_admin, project_manager, sales_manager, dispatcher
if current_user.role not in ['platform_admin', 'client_admin', 'contractor_admin', 'project_manager', 'sales_manager', 'dispatcher']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only platform administrators, organization admins, project managers, sales managers, and dispatchers can create clients"
)
# Check if client name already exists
existing_client = db.query(Client).filter(
Client.name == client_data.name,
Client.deleted_at == None
).first()
if existing_client:
# Return existing client with flag
logger.info(f"Client '{client_data.name}' already exists, returning existing record")
return existing_client
# Check if email already exists
if client_data.main_email:
existing_email = db.query(Client).filter(
Client.main_email == client_data.main_email,
Client.deleted_at == None
).first()
if existing_email:
logger.info(f"Client with email '{client_data.main_email}' already exists")
return existing_email
# Create new client
new_client = Client(**client_data.model_dump())
# Generate SwiftOps code if not provided
if not new_client.swiftops_code:
from app.utils.org_code_generator import generate_org_code
new_client.swiftops_code = generate_org_code(new_client.name, db)
db.add(new_client)
db.commit()
db.refresh(new_client)
# Audit log
from app.services.audit_service import AuditService
from fastapi import Request
AuditService.log_action(
db=db,
action='create',
entity_type='client',
entity_id=str(new_client.id),
description=f"Client organization created: {new_client.name} by {current_user.role}",
user=current_user,
request=None,
changes={'new': {'name': new_client.name, 'industry': new_client.industry}}
)
logger.info(f"New client created: {new_client.name} by {current_user.email} ({current_user.role})")
return new_client
@router.get("", response_model=List[ClientResponse])
async def list_clients(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
is_active: bool = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
List all clients with pagination
**Authorization:**
- Platform admins see all clients
- Client admins see only their own client organization
- Contractor admins see all clients (their business partners)
- **skip**: Number of records to skip (default: 0)
- **limit**: Maximum number of records to return (default: 100)
- **is_active**: Filter by active status (optional)
"""
query = db.query(Client).filter(Client.deleted_at == None)
# Org scoping for client_admin
if current_user.role == 'client_admin' and current_user.client_id:
query = query.filter(Client.id == current_user.client_id)
if is_active is not None:
query = query.filter(Client.is_active == is_active)
clients = query.order_by(Client.created_at.desc()).offset(skip).limit(limit).all()
return clients
@router.get("/{client_id}", response_model=ClientResponse)
async def get_client(
client_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Get a specific client by ID
"""
client = db.query(Client).filter(
Client.id == client_id,
Client.deleted_at == None
).first()
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Client not found"
)
return client
@router.put("/{client_id}", response_model=ClientResponse)
async def update_client(
client_id: UUID,
client_data: ClientUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Update a client
**Requires:** platform_admin or client_admin role
"""
# Get client
client = db.query(Client).filter(
Client.id == client_id,
Client.deleted_at == None
).first()
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Client not found"
)
# Check authorization
if current_user.role not in ['platform_admin', 'client_admin', 'project_manager', 'sales_manager', 'dispatcher']:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to update client"
)
# If client_admin or manager/dispatcher, ensure they belong to this client
if current_user.role in ['client_admin', 'project_manager', 'sales_manager', 'dispatcher'] and current_user.client_id and current_user.client_id != client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You can only update your own client organization"
)
# Update fields
update_data = client_data.model_dump(exclude_unset=True)
# Check for name uniqueness if updating name
if 'name' in update_data and update_data['name'] != client.name:
existing = db.query(Client).filter(Client.name == update_data['name']).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Client with name '{update_data['name']}' already exists"
)
# Check for email uniqueness if updating email
if 'main_email' in update_data and update_data['main_email'] != client.main_email:
existing = db.query(Client).filter(Client.main_email == update_data['main_email']).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Client with email '{update_data['main_email']}' already exists"
)
for field, value in update_data.items():
setattr(client, field, value)
db.commit()
db.refresh(client)
return client
@router.delete("/{client_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_client(
client_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""
Soft delete a client
**Requires:** platform_admin role
"""
# Check authorization
if current_user.role != 'platform_admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only platform administrators can delete clients"
)
# Get client
client = db.query(Client).filter(
Client.id == client_id,
Client.deleted_at == None
).first()
if not client:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Client not found"
)
# Soft delete
from datetime import datetime, timezone
client.deleted_at = datetime.now(timezone.utc)
db.commit()
return None