from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, EmailStr from sqlalchemy import select from sqlalchemy.orm import Session from app.core.db.deps import get_common_db from app.core.security.passwords import hash_password from app.core.security.session_auth import require_login from app.modules.core.audit.service import model_snapshot, pair_before_after, write_audit_log from app.modules.core.iam.lifecycle import activate_user, deactivate_user, disable_login, enable_login, ensure_manageable_lifecycle, lock_user, restore_user, soft_delete_user, unlock_user, LifecycleError from app.modules.core.iam.models import User from app.modules.core.iam.scope import ( build_scope, ensure_assignable_roles, ensure_manageable_existing_user, ensure_users_manage_scope, ensure_users_view_scope, list_scoped_users, resolve_target_tenant_branch, scope_to_http, ) from app.modules.core.rbac.deps import require_permission from app.modules.core.rbac.models import UserRole router = APIRouter(prefix="/users", tags=["users"]) class UserCreateRequest(BaseModel): email: EmailStr full_name: str password: str tenant_id: int | None = None branch_id: int | None = None role_ids: list[int] = [] is_active: bool = True allow_login: bool = True class UserUpdateRequest(BaseModel): full_name: str tenant_id: int | None = None branch_id: int | None = None role_ids: list[int] = [] is_active: bool = True allow_login: bool = True password: str | None = None @router.get("", dependencies=[Depends(require_permission("users.view"))]) def list_users(current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) try: ensure_users_view_scope(scope) except Exception as exc: raise scope_to_http(exc) users = list_scoped_users(db, scope) return [ { "id": u.id, "email": u.email, "full_name": u.full_name, "tenant_id": u.tenant_id, "branch_id": u.branch_id, "is_active": u.is_active, "allow_login": getattr(u, "allow_login", True), "is_locked": getattr(u, "is_locked", False), "deleted_at": (u.deleted_at.isoformat() if getattr(u, "deleted_at", None) else None), } for u in users ] @router.post("", dependencies=[Depends(require_permission("users.manage"))]) def create_user( payload: UserCreateRequest, current_user: User = Depends(require_login), db: Session = Depends(get_common_db), ): scope = build_scope(db, current_user) try: ensure_users_manage_scope(scope) tenant_id, branch_id = resolve_target_tenant_branch(db, scope, payload.tenant_id, payload.branch_id) roles = ensure_assignable_roles(db, scope, payload.role_ids) except Exception as exc: raise scope_to_http(exc) email = payload.email.lower().strip() exists = db.execute(select(User).where(User.email == email)).scalar_one_or_none() if exists: raise HTTPException(status_code=400, detail="Email already exists") user = User( email=email, full_name=payload.full_name.strip(), password_hash=hash_password(payload.password), tenant_id=tenant_id, branch_id=branch_id, is_active=payload.is_active, allow_login=payload.allow_login, is_locked=False, deleted_at=None, ) db.add(user) db.commit() db.refresh(user) for role in roles: db.add(UserRole(user_id=user.id, role_id=role.id)) db.commit() write_audit_log( db, action="user.create.api", entity_type="user", actor=current_user, entity_id=user.id, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id, details={"after": model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"]), "role_ids": [role.id for role in roles]}, ) return {"status": "ok", "id": user.id} @router.get("/{user_id}", dependencies=[Depends(require_permission("users.view"))]) def get_user(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) try: ensure_users_view_scope(scope) except Exception as exc: raise scope_to_http(exc) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_existing_user(db, scope, user) except Exception as exc: raise scope_to_http(exc) role_ids = db.execute(select(UserRole.role_id).where(UserRole.user_id == user.id)).scalars().all() return { "id": user.id, "email": user.email, "full_name": user.full_name, "tenant_id": user.tenant_id, "branch_id": user.branch_id, "is_active": user.is_active, "allow_login": getattr(user, "allow_login", True), "is_locked": getattr(user, "is_locked", False), "deleted_at": (user.deleted_at.isoformat() if getattr(user, "deleted_at", None) else None), "role_ids": list(role_ids), } @router.put("/{user_id}", dependencies=[Depends(require_permission("users.manage"))]) def update_user( user_id: int, payload: UserUpdateRequest, current_user: User = Depends(require_login), db: Session = Depends(get_common_db), ): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_users_manage_scope(scope) ensure_manageable_existing_user(db, scope, user) tenant_id, branch_id = resolve_target_tenant_branch(db, scope, payload.tenant_id, payload.branch_id) roles = ensure_assignable_roles(db, scope, payload.role_ids) except Exception as exc: raise scope_to_http(exc) before_snapshot = model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"]) old_role_ids = list(db.execute(select(UserRole.role_id).where(UserRole.user_id == user.id)).scalars().all()) user.full_name = payload.full_name.strip() user.tenant_id = tenant_id user.branch_id = branch_id user.is_active = payload.is_active user.allow_login = payload.allow_login if payload.password: user.password_hash = hash_password(payload.password) db.execute(UserRole.__table__.delete().where(UserRole.user_id == user.id)) for role in roles: db.add(UserRole(user_id=user.id, role_id=role.id)) db.commit() write_audit_log( db, action="user.update.api", entity_type="user", actor=current_user, entity_id=user.id, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id, details={**pair_before_after(before_snapshot, model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"])), "old_role_ids": old_role_ids, "new_role_ids": [role.id for role in roles]}, ) return {"status": "ok"} def _lifecycle_response(user: User) -> dict: return { "status": "ok", "user_id": user.id, "is_active": user.is_active, "allow_login": getattr(user, "allow_login", True), "is_locked": getattr(user, "is_locked", False), "deleted_at": (user.deleted_at.isoformat() if getattr(user, "deleted_at", None) else None), } def _apply_lifecycle_action(db: Session, current_user: User, user: User, action: str): before = model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"]) if action == "activate": activate_user(user) audit_action = "user.activate.api" elif action == "deactivate": deactivate_user(user) audit_action = "user.deactivate.api" elif action == "enable-login": enable_login(user) audit_action = "user.enable_login.api" elif action == "disable-login": disable_login(user) audit_action = "user.disable_login.api" elif action == "lock": lock_user(user) audit_action = "user.lock.api" elif action == "unlock": unlock_user(user) audit_action = "user.unlock.api" elif action == "delete": soft_delete_user(user) audit_action = "user.soft_delete.api" elif action == "restore": restore_user(user) audit_action = "user.restore.api" else: raise HTTPException(status_code=400, detail="Unknown action") db.commit() write_audit_log( db, action=audit_action, entity_type="user", actor=current_user, entity_id=user.id, entity_name=user.email, target_tenant_id=user.tenant_id, target_branch_id=user.branch_id, details=pair_before_after(before, model_snapshot(user, ["email", "full_name", "tenant_id", "branch_id", "is_active", "allow_login", "is_locked", "deleted_at"])), ) return _lifecycle_response(user) @router.post("/{user_id}/activate", dependencies=[Depends(require_permission("users.manage"))]) def activate_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "activate") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "activate") @router.post("/{user_id}/deactivate", dependencies=[Depends(require_permission("users.manage"))]) def deactivate_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "deactivate") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "deactivate") @router.post("/{user_id}/enable-login", dependencies=[Depends(require_permission("users.manage"))]) def enable_login_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "enable login for") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "enable-login") @router.post("/{user_id}/disable-login", dependencies=[Depends(require_permission("users.manage"))]) def disable_login_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "disable login for") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "disable-login") @router.post("/{user_id}/lock", dependencies=[Depends(require_permission("users.manage"))]) def lock_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "lock") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "lock") @router.post("/{user_id}/unlock", dependencies=[Depends(require_permission("users.manage"))]) def unlock_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "unlock") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "unlock") @router.post("/{user_id}/delete", dependencies=[Depends(require_permission("users.manage"))]) def delete_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "delete") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "delete") @router.post("/{user_id}/restore", dependencies=[Depends(require_permission("users.manage"))]) def restore_user_api(user_id: int, current_user: User = Depends(require_login), db: Session = Depends(get_common_db)): scope = build_scope(db, current_user) user = db.execute(select(User).where(User.id == user_id)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="User not found") try: ensure_manageable_lifecycle(scope, db, current_user, user, "restore") except (Exception, LifecycleError) as exc: raise scope_to_http(exc) return _apply_lifecycle_action(db, current_user, user, "restore")