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 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 "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() 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