import json import uuid 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, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_USE_TLS, SMTP_TIMEOUT, SMTP_FROM_USER, SMTP_TEMPLATES_DIRECTORY, PLATFORM_NAME, HAS_HTTPS, HOST, PORT, EMAIL_VERIFICATION_EXPIRATION, UNVERIFIED_MANAGER, ) from responses import ( Response as APIResponse, UnprocessableEntity, BadRequest, NotFound, InternalServerError, ) from orm.users import ( User, UserModel, ) from orm.email_verification import EmailVerification, EmailVerificationType from util.email import send_email router = FastAPI() @router.get( "/user/verifyEmail/{verification_id}", tags=["Users"], status_code=200, responses={ 200: {"model": APIResponse}, 404: {"model": NotFound}, 422: {"model": UnprocessableEntity}, }, ) @LIMITER.limit("3/second") async def verify_email( request: Request, verification_id: str, user: UserModel = Depends(UNVERIFIED_MANAGER), ): """ Verifies a user's email address. 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="Verification ID is invalid") elif not verification["pending"]: raise HTTPException(status_code=400, detail="Email is already verified") 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: await EmailVerification.update({EmailVerification.pending: False}).where( EmailVerification.user == user.public_id ) await User.update({User.email_verified: True}).where( User.public_id == user.public_id ) return APIResponse(status_code=200, msg="Verification successful") @router.get( "/user/changeEmail/{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 change_email( request: Request, verification_id: str, user: UserModel = Depends(UNVERIFIED_MANAGER), ): """ Modifies a user's email address. 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.CHANGE_EMAIL ) await User.update( { User.email_address: verification["data"].decode(), User.email_verified: False, } ).where(User.public_id == user.public_id) return APIResponse(status_code=200, msg="Email updated") @router.put( "user/resendMail", tags=["Users"], status_code=200, responses={ 200: {"model": APIResponse}, 400: {"model": BadRequest}, 422: {"model": UnprocessableEntity}, 500: {"model": InternalServerError}, }, ) @LIMITER.limit("6/minute") async def resend_email(request: Request, user: UserModel = Depends(UNVERIFIED_MANAGER)): """ Resends the verification email to the user if the previous has expired. Endpoint is limited to 6 hits per minute """ if user.email_verified: raise HTTPException(status_code=400, detail="Email is already verified") email_template = SMTP_TEMPLATES_DIRECTORY / f"{user.locale}.json" try: email_template.resolve(strict=True) except FileNotFoundError: email_template = SMTP_TEMPLATES_DIRECTORY / "en_US.json" with email_template.open() as f: email_message = json.load(f)["signup"] verification_id = uuid.uuid4() if await send_email( SMTP_HOST, SMTP_PORT, email_message["content"].format( first_name=user.first_name, last_name=user.last_name, username=user.username, email=user.email_address, link=f"http{'s' if HAS_HTTPS else ''}://{HOST}" f"{'' if PORT == 443 and HAS_HTTPS or PORT == 80 else f':{PORT}'}/user/verifyEmail/{verification_id}", platformName=PLATFORM_NAME, ), SMTP_TIMEOUT, SMTP_FROM_USER, user.email_address, email_message["subject"].format( first_name=user.first_name, last_name=user.last_name, username=user.username, email=user.email_address, platformName=PLATFORM_NAME, ), SMTP_USER, SMTP_PASSWORD, use_tls=SMTP_USE_TLS, ): await EmailVerification.update( { EmailVerification.id: verification_id, EmailVerification.creation_date: datetime.now(), } ) return APIResponse(status_code=200, msg="Success") else: raise HTTPException( status_code=500, detail="An error occurred while trying to resend the email," " please try again later", )