PySimpleSocial/src/endpoints/email.py

211 lines
6.4 KiB
Python
Raw Normal View History

2023-03-13 15:59:03 +01:00
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",
)