from datetime import timedelta from datetime import timezone, datetime from fastapi.exceptions import HTTPException from fastapi import APIRouter as FastAPI, Depends, Request from config import ( LIMITER, EMAIL_VERIFICATION_EXPIRATION, UNVERIFIED_MANAGER, ) from responses import ( Response as APIResponse, UnprocessableEntity, BadRequest, NotFound, ) from orm.users import User, UserModel from orm.email_verification import EmailVerification, EmailVerificationType router = FastAPI() @router.get( "/user/resetPassword/{verification_id}", tags=["Users"], status_code=200, responses={ 200: {"model": APIResponse}, 400: {"model": BadRequest}, 422: {"model": UnprocessableEntity}, 404: {"model": NotFound}, }, ) @LIMITER.limit("3/second") async def reset_password( request: Request, verification_id: str, user: UserModel = Depends(UNVERIFIED_MANAGER), ): """ Modifies a user's password. Endpoint is limited to 3 hits per second """ if not ( verification := await EmailVerification.select(*EmailVerification.all_columns()) .where(EmailVerification.id == verification_id) .first() ): raise HTTPException(status_code=404, detail="Request ID is invalid") elif not verification["pending"]: raise HTTPException(status_code=400, detail="This link has already been used") elif datetime.now().astimezone(timezone.utc) - verification[ "creation_date" ].astimezone(timezone.utc) > timedelta(seconds=EMAIL_VERIFICATION_EXPIRATION): raise HTTPException( status_code=400, detail="Verification window has expired. Try again", ) else: # Note how we don't update based on the verification ID: # this way, multiple pending email verification requests # are all cleared at once await EmailVerification.update({EmailVerification.pending: False}).where( EmailVerification.user == user.public_id and EmailVerification.kind == EmailVerificationType.PASSWORD_RESET ) await User.update({User.password_hash: verification["data"]}).where( User.public_id == user.public_id ) return APIResponse(status_code=200, msg="Password updated")