110 lines
4.3 KiB
Python
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)
|