HR-ATS-Portal/backend/forget_password/views.py

110 lines
4.3 KiB
Python

from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
import httpx
import jwt
from forget_password.models import PasswordResetCodes
from forget_password.plugins import (
RESET_CODE_MAX_ATTEMPTS,
RESET_CODE_RESEND_SECONDS,
code_expiry,
generate_code,
hash_code,
now_utc,
render_reset_email,
send_reset_mail,
verify_code,
RESET_CODE_TTL_SECONDS,
)
from forget_password.serializers import serialize_reset_request,serialize_reset_result
from users.models import Users
from users.plugins import decode_token,hash_password
class ForgetPassword:
def __init__(self,session:AsyncSession):
self.session=session
async def request_code(self,email):
user=await Users.get_user_by_email(self.session,email)
if not user or user.is_deleted or not user.is_active:
raise HTTPException(status_code=404,detail="No account found for this email")
active=await PasswordResetCodes.get_active_code_by_email(self.session,email)
if active:
age=(now_utc()-active.created_at).total_seconds()
if age<RESET_CODE_RESEND_SECONDS and active.expires_at>now_utc():
raise HTTPException(status_code=429,detail="Please wait before requesting another code")
await PasswordResetCodes.invalidate_codes_for_email(self.session,email)
code=generate_code()
expires_at=code_expiry()
row=await PasswordResetCodes.insert_code(self.session,{
"email":email,
"code_hash":hash_code(code),
"expires_at":expires_at,
})
subject,html=render_reset_email(code,RESET_CODE_TTL_SECONDS)
try:
await send_reset_mail(email,subject,html)
except (httpx.HTTPError,RuntimeError) as e:
await PasswordResetCodes.mark_used(self.session,str(row.id))
raise HTTPException(status_code=502,detail="Failed to send reset email") from e
return serialize_reset_request(email,expires_at)
async def verify_code(self,email,code):
row=await PasswordResetCodes.get_active_code_by_email(self.session,email)
if not row:
raise HTTPException(status_code=400,detail="No active reset code for this email")
if row.expires_at<=now_utc():
raise HTTPException(status_code=400,detail="Reset code has expired")
if (row.attempts or 0)>=RESET_CODE_MAX_ATTEMPTS:
raise HTTPException(status_code=429,detail="Too many invalid attempts")
if not verify_code(code,row.code_hash):
updated=await PasswordResetCodes.increment_attempts(self.session,str(row.id))
if updated and (updated.attempts or 0)>=RESET_CODE_MAX_ATTEMPTS:
raise HTTPException(status_code=429,detail="Too many invalid attempts")
raise HTTPException(status_code=400,detail="Invalid reset code")
await PasswordResetCodes.mark_verified(self.session,str(row.id))
return email,str(row.id)
async def set_new_password(self,reset_token,password):
try:
payload=decode_token(reset_token,expected_type="reset")
except jwt.PyJWTError:
raise HTTPException(
status_code=401,
detail="Invalid or expired reset token",
headers={"WWW-Authenticate":"Bearer"},
)
email=payload.get("sub")
code_id=payload.get("crid")
if not email or not code_id:
raise HTTPException(
status_code=401,
detail="Invalid or expired reset token",
headers={"WWW-Authenticate":"Bearer"},
)
row=await PasswordResetCodes.get_code_by_id(self.session,code_id)
if not row or row.is_used or row.email!=email or not row.verified_at:
raise HTTPException(
status_code=401,
detail="Invalid or expired reset token",
headers={"WWW-Authenticate":"Bearer"},
)
user=await Users.get_user_by_email(self.session,email)
if not user or user.is_deleted:
raise HTTPException(status_code=404,detail="User not found")
await Users.update_user(self.session,str(user.id),{"password":hash_password(password)})
await PasswordResetCodes.mark_used(self.session,str(row.id))
return serialize_reset_result(email)