Files
saas_backend/app/services/auth/user_service.py
T

315 lines
11 KiB
Python

from sqlalchemy.orm import Session
from sqlalchemy import or_, cast, String
from fastapi import HTTPException, status, BackgroundTasks
from datetime import datetime
import uuid
from typing import Optional, List, Dict, Any
from app.models.auth.user_model import User
from app.schemas.auth.user_schema import UserCreate, UserUpdate, UserResponse, UserPaginatedResponse
from app.config.security import security
from app.services.auth.event_service import EventService
import logging
import json
from app.models.auth.role_module_access_model import RoleModuleAccess
from app.models.auth.module_access_model import ModuleAccess
from app.models.auth.tenant_module_model import TenantModule
from app.models.auth.role_model import Role
logger = logging.getLogger(__name__)
class UserService:
@staticmethod
def create_user(db: Session, user_data: UserCreate, tenant_id: uuid.UUID = None, background_tasks: BackgroundTasks = None) -> User:
if db.query(User).filter(User.email == user_data.email).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
if not security.validate_password_strength(user_data.password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password too weak"
)
user = User(
email=user_data.email,
password=security.hash_password(user_data.password),
first_name=user_data.first_name,
last_name=user_data.last_name,
phone_number=user_data.phone_number,
status=user_data.status or "active",
tenant_id=tenant_id,
role_id=user_data.role_id
)
db.add(user)
db.flush()
db.refresh(user)
targets = []
if user.role_id:
targets = UserService._resolve_targets_for_role(db, user.role_id, user.tenant_id)
payload = {
"user_id": str(user.id),
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"phone_number": user.phone_number,
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
"role_id": str(user.role_id) if user.role_id else None,
"status": user.status,
"targets": targets
}
logger.info(f"USER_PROVISION_REQUESTED Payload: {json.dumps(payload, default=str)}")
EventService.emit_event(
db=db,
event_type="USER_PROVISION_REQUESTED",
payload=payload,
tenant_id=user.tenant_id
)
db.commit()
return user
@staticmethod
def get_all_users(db: Session, tenant_id: uuid.UUID = None):
query = db.query(User)
if tenant_id:
query = query.filter(User.tenant_id == tenant_id)
return query.all()
@staticmethod
def get_user_by_id(db: Session, user_id: uuid.UUID, tenant_id: uuid.UUID = None) -> User:
query = db.query(User).filter(User.id == user_id)
if tenant_id:
query = query.filter(User.tenant_id == tenant_id)
user = query.first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
@staticmethod
def update_user(db: Session, user_id: uuid.UUID, user_data: UserUpdate, tenant_id: uuid.UUID = None, background_tasks: BackgroundTasks = None) -> User:
user = UserService.get_user_by_id(db, user_id, tenant_id)
old_role_id = user.role_id
old_targets = []
if old_role_id:
old_targets = UserService._resolve_targets_for_role(db, old_role_id, user.tenant_id)
update_dict = user_data.model_dump(exclude_unset=True)
if tenant_id:
update_dict.pop("tenant_id", None)
if "role_id" in update_dict and update_dict["role_id"] is not None:
role = db.query(Role).filter(Role.id == update_dict["role_id"]).first()
if not role:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Role not found"
)
if tenant_id and role.tenant_id is not None and role.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot assign a role from another tenant"
)
if "email" in update_dict and update_dict["email"] != user.email:
if db.query(User).filter(User.email == update_dict["email"]).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already used"
)
password = update_dict.pop("password", None)
if password:
if not security.validate_password_strength(password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password too weak"
)
user.password = security.hash_password(password)
user.password_updated_at = datetime.utcnow()
for key, value in update_dict.items():
setattr(user, key, value)
db.flush()
db.refresh(user)
new_targets = []
if user.role_id:
new_targets = UserService._resolve_targets_for_role(db, user.role_id, user.tenant_id)
role_changed = (old_role_id != user.role_id)
base_payload = {
"user_id": str(user.id),
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"phone_number": user.phone_number,
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
"role_id": str(user.role_id) if user.role_id else None,
"status": user.status
}
if role_changed:
old_mids = {t["module_id"] for t in old_targets}
new_mids = {t["module_id"] for t in new_targets}
removed_mids = old_mids - new_mids
deprovision_targets = [t for t in old_targets if t["module_id"] in removed_mids]
if deprovision_targets:
payload = {**base_payload, "targets": deprovision_targets}
logger.info(f"USER_DEPROVISION_REQUESTED Payload: {json.dumps(payload, default=str)}")
EventService.emit_event(
db=db,
event_type="USER_DEPROVISION_REQUESTED",
payload=payload,
tenant_id=user.tenant_id
)
if new_targets:
payload = {**base_payload, "targets": new_targets}
logger.info(f"USER_PROVISION_REQUESTED Payload: {json.dumps(payload, default=str)}")
EventService.emit_event(
db=db,
event_type="USER_PROVISION_REQUESTED",
payload=payload,
tenant_id=user.tenant_id
)
else:
if new_targets:
payload = {**base_payload, "targets": new_targets}
logger.info(f"USER_UPDATED Payload: {json.dumps(payload, default=str)}")
EventService.emit_event(
db=db,
event_type="USER_UPDATED",
payload=payload,
tenant_id=user.tenant_id
)
db.commit()
return user
@staticmethod
def delete_user(db: Session, user_id: uuid.UUID, tenant_id: uuid.UUID = None):
user = UserService.get_user_by_id(db, user_id, tenant_id)
targets = []
if user.role_id:
targets = UserService._resolve_targets_for_role(db, user.role_id, user.tenant_id)
if targets:
payload = {
"user_id": str(user.id),
"tenant_id": str(user.tenant_id) if user.tenant_id else None,
"targets": targets
}
logger.info(f"USER_DEPROVISION_REQUESTED Payload: {json.dumps(payload, default=str)}")
EventService.emit_event(
db=db,
event_type="USER_DEPROVISION_REQUESTED",
payload=payload,
tenant_id=user.tenant_id
)
db.delete(user)
db.commit()
return {"message": "User deleted successfully"}
@staticmethod
def get_users_paginated(
db: Session,
tenant_id: Optional[uuid.UUID] = None,
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
status: Optional[str] = None,
) -> UserPaginatedResponse:
query = db.query(User)
if tenant_id:
query = query.filter(User.tenant_id == tenant_id)
if search and search.strip():
search_term = search.strip()
search_term = search_term.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
query = query.filter(
or_(
User.email.ilike(f"%{search_term}%"),
User.first_name.ilike(f"%{search_term}%"),
User.last_name.ilike(f"%{search_term}%"),
User.phone_number.ilike(f"%{search_term}%"),
cast(User.id, String).ilike(f"%{search_term}%"),
)
)
if status is not None:
query = query.filter(User.status == status)
total = query.count()
offset = (page - 1) * page_size
users = query.offset(offset).limit(page_size).all()
total_pages = (total + page_size - 1) // page_size if total > 0 else 0
return UserPaginatedResponse(
items=[UserResponse.model_validate(user) for user in users],
total=total,
page=page,
page_size=page_size,
total_pages=total_pages,
)
@staticmethod
def _resolve_targets_for_role(db: Session, role_id: uuid.UUID, tenant_id: uuid.UUID = None) -> List[Dict[str, Any]]:
"""
Helper to resolve which modules/envs a role targets.
Mirrors logic in RoleService.
"""
active_modules = (
db.query(ModuleAccess.module_id)
.join(RoleModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id)
.filter(RoleModuleAccess.role_id == role_id)
.distinct()
.all()
)
if not active_modules:
return []
module_ids = [m[0] for m in active_modules]
env_map = {}
if tenant_id:
tm_assignments = db.query(TenantModule).filter(
TenantModule.tenant_id == tenant_id,
TenantModule.module_id.in_(module_ids),
TenantModule.is_active == True
).all()
for tm in tm_assignments:
env_map[str(tm.module_id)] = tm.assigned_environment_slug or "prod"
targets = []
for mid_uuid in module_ids:
mid = str(mid_uuid)
env_slug = env_map.get(mid, "prod")
targets.append({
"module_id": mid,
"environment_slug": env_slug
})
return targets