
- Implemented comprehensive multi-tenant data isolation using database-level security - Built JWT authentication system with role-based access control (Super Admin, Org Admin, User, Viewer) - Created RESTful API endpoints for user and organization operations - Added complete audit logging for all data modifications with IP tracking - Implemented API rate limiting and input validation with security middleware - Built webhook processing engine with async event handling and retry logic - Created external API call handlers with circuit breaker pattern and error handling - Implemented data synchronization between external services and internal data - Added integration health monitoring and status tracking - Created three mock external services (User Management, Payment, Communication) - Implemented idempotency for webhook processing to handle duplicates gracefully - Added comprehensive security headers and XSS/CSRF protection - Set up Alembic database migrations with proper SQLite configuration - Included extensive documentation and API examples Architecture features: - Multi-tenant isolation at database level - Circuit breaker pattern for external API resilience - Async background task processing - Complete audit trail with user context - Role-based permission system - Webhook signature verification - Request validation and sanitization - Health monitoring endpoints Co-Authored-By: Claude <noreply@anthropic.com>
109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
from sqlalchemy.orm import Session
|
|
from sqlalchemy.exc import IntegrityError
|
|
from fastapi import HTTPException, status
|
|
from typing import Optional
|
|
from app.models.tenant import Organization
|
|
from app.models.user import User, UserRole
|
|
from app.schemas.organization import OrganizationUpdate
|
|
from app.services.audit import AuditService
|
|
|
|
|
|
class OrganizationService:
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
self.audit_service = AuditService(db)
|
|
|
|
def get_organization(self, organization_id: int) -> Optional[Organization]:
|
|
return self.db.query(Organization).filter(
|
|
Organization.id == organization_id
|
|
).first()
|
|
|
|
def update_organization(
|
|
self,
|
|
organization_id: int,
|
|
organization_update: OrganizationUpdate,
|
|
current_user: User,
|
|
ip_address: str,
|
|
user_agent: str
|
|
) -> Organization:
|
|
# Only org admins and super admins can update organization
|
|
if current_user.role not in [UserRole.ORG_ADMIN, UserRole.SUPER_ADMIN]:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Not enough permissions to update organization"
|
|
)
|
|
|
|
# Ensure user can only update their own organization
|
|
if organization_id != current_user.organization_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Cannot update different organization"
|
|
)
|
|
|
|
db_org = self.get_organization(organization_id)
|
|
if not db_org:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Organization not found"
|
|
)
|
|
|
|
update_data = organization_update.dict(exclude_unset=True)
|
|
old_values = {field: getattr(db_org, field) for field in update_data.keys()}
|
|
|
|
for field, value in update_data.items():
|
|
setattr(db_org, field, value)
|
|
|
|
try:
|
|
self.db.commit()
|
|
self.db.refresh(db_org)
|
|
|
|
# Log organization update
|
|
self.audit_service.log_user_activity(
|
|
user=current_user,
|
|
action="update",
|
|
resource_type="organization",
|
|
resource_id=str(db_org.id),
|
|
details={
|
|
"updated_fields": list(update_data.keys()),
|
|
"old_values": old_values,
|
|
"new_values": update_data
|
|
},
|
|
ip_address=ip_address,
|
|
user_agent=user_agent
|
|
)
|
|
|
|
return db_org
|
|
|
|
except IntegrityError:
|
|
self.db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Domain or subdomain already exists"
|
|
)
|
|
|
|
def get_organization_stats(self, organization_id: int) -> dict:
|
|
org = self.get_organization(organization_id)
|
|
if not org:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Organization not found"
|
|
)
|
|
|
|
# Get user count
|
|
user_count = self.db.query(User).filter(
|
|
User.organization_id == organization_id
|
|
).count()
|
|
|
|
# Get active user count
|
|
active_user_count = self.db.query(User).filter(
|
|
User.organization_id == organization_id,
|
|
User.is_active
|
|
).count()
|
|
|
|
return {
|
|
"total_users": user_count,
|
|
"active_users": active_user_count,
|
|
"organization_name": org.name,
|
|
"organization_domain": org.domain,
|
|
"created_at": org.created_at
|
|
} |