PySimpleSocial/src/endpoints/email.py

211 lines
6.4 KiB
Python

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",
)