186 lines
8.6 KiB
Python
186 lines
8.6 KiB
Python
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.job_post.models import JobPosts
|
|
|
|
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 JobPosts.count_open_reqs_by_hiring_managers(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
|