from fastapi import HTTPException from notifications.views import Confirmation from role.models import EnumRoles,Roles from users.models import Users from users.permissions import PermissionTag,has_permission from users.serializers import serialize_user from users.plugins import clean_user_payload,verify_password,decode_token from dotenv import load_dotenv load_dotenv() from sqlalchemy.ext.asyncio import AsyncSession import jwt from typing import Optional class User: def __init__(self,session:AsyncSession): self.session=session async def _check_role_assignment(self,current_user,role_id,existing_role_id=None): if role_id==existing_role_id: return #"Take the current user's permission list. Check if rbac_users.manage is in it. If it is not → reject with 403." if not has_permission(current_user.get("permissions") or [],PermissionTag.RBAC_USERS_MANAGE): raise HTTPException(status_code=403,detail="Assigning a role requires rbac_users.manage") if role_id is None: return role=await Roles.get_role_by_id(self.session,role_id) if role is None or role.is_deleted: raise HTTPException(status_code=404,detail="Role not found") if not role.is_active: raise HTTPException(status_code=400,detail="Role is not active") target=set(await Roles.resolve_tags(self.session,role)) missing=sorted(target-set(current_user.get("permissions") or [])) if missing: raise HTTPException(status_code=403,detail=f"Cannot assign a role with permissions you do not hold: {', '.join(missing)}") async def create_user(self,payload,current_user): existing=await Users.get_user_by_email(self.session,payload.get("email")) if existing: raise HTTPException(status_code=409,detail="Email already registered") await self._check_role_assignment(current_user,payload.get("role_id"),None) # this is for password hasshing fields=clean_user_payload(payload) if not fields.get("password"): raise HTTPException(status_code=400,detail="Password is required") # Admin-created accounts skip the signup approval queue. fields["is_approved"]=True return await Users.insert_user(self.session,fields) async def signup_user(self,payload): existing=await Users.get_user_by_email(self.session,payload.get("email")) if existing: raise HTTPException(status_code=409,detail="Email already registered") fields=clean_user_payload(payload) if not fields.get("password"): raise HTTPException(status_code=400,detail="Password is required") fields["role_id"]=4 fields["is_approved"]=False user=await Users.insert_user(self.session,fields) # Signup lands inactive; the mailed link is what flips is_active. service=Confirmation(session=self.session) await service.send_confirmation(user) return user async def get_users(self,top:Optional[int]=None,skip:Optional[int]=None,search:Optional[str]=None,role_id:Optional[int]=None): if role_id: users=await Users.get_users(self.session,top=top,skip=skip,search=search,role_id=role_id) else: users=await Users.get_users(self.session,top=top,skip=skip,search=search) return [serialize_user(u) for u in users] async def get_user_by_id(self,record_id): user=await Users.get_user_by_id(self.session,record_id) if not user: raise HTTPException(status_code=404,detail="User not found") return serialize_user(user) async def update_user(self,record_id,payload): user=await Users.get_user_by_id(self.session,record_id) if not user: raise HTTPException(status_code=404,detail="User not found") # this is for password hashing and dehashing fields=clean_user_payload(payload,partial=True) email=fields.get("email") if email and email!=user.email: clash=await Users.get_user_by_email(self.session,email) if clash: raise HTTPException(status_code=409,detail="Email already registered") updated=await Users.update_user(self.session,record_id,fields) return serialize_user(updated) async def assign_role(self,record_id,role_id,current_user): user=await Users.get_user_by_id(self.session,record_id) if not user: raise HTTPException(status_code=404,detail="User not found") await self._check_role_assignment(current_user,role_id,user.role_id) updated=await Users.update_user(self.session,record_id,{"role_id":role_id}) return serialize_user(updated) async def remove_role(self,record_id,current_user): user=await Users.get_user_by_id(self.session,record_id) if not user: raise HTTPException(status_code=404,detail="User not found") await self._check_role_assignment(current_user,None,user.role_id) updated=await Users.update_user(self.session,record_id,{"role_id":None}) return serialize_user(updated) async def delete_user(self,record_id): user=await Users.soft_delete_user(self.session,record_id) if not user: raise HTTPException(status_code=404,detail="User not found") return serialize_user(user) async def get_managers(self): """Hiring-manager directory for Jobs/Candidates callers who do not hold rbac_users.view.""" from job.assignment.models import JobAssignments role=await Roles.get_role_by_name(self.session,EnumRoles.HIRING_MANAGER.value) if role is None: raise HTTPException(status_code=500,detail="Role hiring_manager is not seeded") rows=await Users.get_users(self.session,top=500,role_id=role.id) counts=await JobAssignments.count_open_reqs_by_users(self.session,[u.id for u in rows]) data=[ { "id": str(u.id), "name": u.name, "email": u.email, "role_name": role.role_name, "open_reqs": int(counts.get(u.id,0)), "department": None, "title": None, "team_size": None, } for u in rows ] return data,len(data) async def count_users(self,search=None,role_id=None): return await Users.count_users(self.session,search,role_id=role_id) async def authenticate_user(self,email,password): user=await Users.get_user_by_email(self.session,email) if not user or not verify_password(password,user.password): raise HTTPException( status_code=401, detail="Incorrect email or password", headers={"WWW-Authenticate":"Bearer"}, ) if user.is_deleted: raise HTTPException(status_code=401,detail="User is inactive") if not user.is_active: raise HTTPException(status_code=401,detail="Please confirm your email address to activate your account") if not user.is_approved: raise HTTPException(status_code=403,detail="Your Approval is at Pending") return await Users.get_user_by_id(self.session,user.id) async def get_pending_approvals(self): users=await Users.get_pending_approvals(self.session) return [serialize_user(u) for u in users] async def approve_user(self,record_id): user=await Users.get_user_by_id(self.session,record_id) if not user: raise HTTPException(status_code=404,detail="User not found") if user.is_deleted: raise HTTPException(status_code=400,detail="User is inactive") if not user.is_active: raise HTTPException(status_code=400,detail="User must confirm their email before approval") if user.is_approved: return serialize_user(user) updated=await Users.update_user(self.session,record_id,{"is_approved":True}) return serialize_user(updated) async def refresh_access_token(self,refresh_token): try: payload=decode_token(refresh_token,expected_type="refresh") except jwt.PyJWTError: raise HTTPException(status_code=401,detail="Invalid or expired refresh token") user=await Users.get_user_by_id(self.session,payload.get("sub")) if not user or user.is_deleted or not user.is_active: raise HTTPException(status_code=401,detail="User is inactive or does not exist") if not user.is_approved: raise HTTPException(status_code=403,detail="Your Approval is at Pending") return user