User email confirmation

Sending emails with background tasks

Want more?

This lesson for enrolled students only. Join the course to unlock it!

You can see the code changes implemented in this lecture below.

If you have purchased the course in a different platform, you still have access to the code changes per lecture here on Teclado. The lecture video and lecture notes remain locked.
Join course for $30

Modified files

storeapi/tasks.py
--- 
+++ 
@@ -11,7 +11,7 @@
     pass


-async def send_simple_message(to: str, subject: str, body: str):
+async def send_simple_email(to: str, subject: str, body: str):
     logger.debug(f"Sending email to '{to[:3]}' with subject '{subject[:20]}'")
     async with httpx.AsyncClient() as client:
         try:
@@ -37,7 +37,7 @@


 async def send_user_registration_email(email: str, confirmation_url: str):
-    return await send_simple_message(
+    return await send_simple_email(
         email,
         "Successfully signed up",
         (
storeapi/security.py
--- 
+++ 
@@ -16,7 +16,7 @@
 pwd_context = CryptContext(schemes=["bcrypt"])


-def create_unauthorized_exception(detail: str) -> HTTPException:
+def create_credentials_exception(detail: str) -> HTTPException:
     return HTTPException(
         status_code=status.HTTP_401_UNAUTHORIZED,
         detail=detail,
@@ -56,19 +56,19 @@
     token: str, type: Literal["access", "confirmation"]
 ) -> str:
     try:
-        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+        payload = jwt.decode(token, key=SECRET_KEY, algorithms=[ALGORITHM])
     except ExpiredSignatureError as e:
-        raise create_unauthorized_exception("Token has expired") from e
+        raise create_credentials_exception("Token has expired") from e
     except JWTError as e:
-        raise create_unauthorized_exception("Invalid token") from e
+        raise create_credentials_exception("Invalid token") from e

     email = payload.get("sub")
     if email is None:
-        raise create_unauthorized_exception("Token is missing 'sub' field")
+        raise create_credentials_exception("Token is missing 'sub' field")

     token_type = payload.get("type")
     if token_type is None or token_type != type:
-        raise create_unauthorized_exception(
+        raise create_credentials_exception(
             f"Token has incorrect type, expected '{type}'"
         )

@@ -95,11 +95,11 @@
     logger.debug("Authenticating user", extra={"email": email})
     user = await get_user(email)
     if not user:
-        raise create_unauthorized_exception("Invalid email or password")
+        raise create_credentials_exception("Invalid email or password")
     if not verify_password(password, user.password):
-        raise create_unauthorized_exception("Invalid email or password")
+        raise create_credentials_exception("Invalid email or password")
     if not user.confirmed:
-        raise create_unauthorized_exception("User has not confirmed email")
+        raise create_credentials_exception("User has not confirmed email")
     return user


@@ -107,5 +107,5 @@
     email = get_subject_for_token_type(token, "access")
     user = await get_user(email=email)
     if user is None:
-        raise create_unauthorized_exception("Could not find user for this token")
+        raise create_credentials_exception("Could not find user for this token")
     return user
storeapi/routers/user.py
--- 
+++ 
@@ -1,6 +1,6 @@
 import logging

-from fastapi import APIRouter, HTTPException, Request, status
+from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
 from storeapi import tasks
 from storeapi.database import database, user_table
 from storeapi.models.user import UserIn
@@ -18,7 +18,7 @@


 @router.post("/register", status_code=201)
-async def register(user: UserIn, request: Request):
+async def register(user: UserIn, background_tasks: BackgroundTasks, request: Request):
     if await get_user(user.email):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
@@ -30,7 +30,11 @@
     logger.debug(query)

     await database.execute(query)
-    await tasks.send_user_registration_email(
+
+    logger.debug("Submitting background task to send email")
+
+    background_tasks.add_task(
+        tasks.send_user_registration_email,
         user.email,
         confirmation_url=request.url_for(
             "confirm_email", token=create_confirmation_token(user.email)
storeapi/tests/test_security.py
--- 
+++ 
@@ -131,4 +131,4 @@
     token = security.create_confirmation_token(registered_user["email"])

     with pytest.raises(security.HTTPException):
-        await security.get_current_user(token)
+        await security.get_current_user(token)
storeapi/tests/test_tasks.py
--- 
+++ 
@@ -1,19 +1,20 @@
 import httpx
 import pytest
-from storeapi.tasks import APIResponseError, send_simple_message
+
+from storeapi.tasks import APIResponseError, send_simple_email


 @pytest.mark.anyio
-async def test_send_simple_message(mock_httpx_client):
-    await send_simple_message("test@example.net", "Test Subject", "Test Body")
+async def test_send_simple_email(mock_httpx_client):
+    await send_simple_email("test@example.net", "Test Subject", "Test Body")
     mock_httpx_client.post.assert_called()


-@pytest.mark.asyncio
-async def test_send_simple_message_api_error(mock_httpx_client):
+@pytest.mark.anyio
+async def test_send_simple_email_api_error(mock_httpx_client):
     mock_httpx_client.post.return_value = httpx.Response(
         status_code=500, content="", request=httpx.Request("POST", "//")
     )

-    with pytest.raises(APIResponseError, match="API request failed with status code"):
-        await send_simple_message("test@example.com", "Test Subject", "Test Body")
+    with pytest.raises(APIResponseError):
+        await send_simple_email("test@example.net", "Test Subject", "Test Body")
storeapi/tests/routers/test_user.py
--- 
+++ 
@@ -1,6 +1,6 @@
 import pytest
+from fastapi import BackgroundTasks
 from httpx import AsyncClient
-from storeapi import tasks


 async def register_user(async_client: AsyncClient, email: str, password: str):
@@ -29,7 +29,7 @@

 @pytest.mark.anyio
 async def test_confirm_user(async_client: AsyncClient, mocker):
-    spy = mocker.spy(tasks, "send_user_registration_email")
+    spy = mocker.spy(BackgroundTasks, "add_task")
     await register_user(async_client, "test@example.net", "1234")

     confirmation_url = str(spy.call_args[1]["confirmation_url"])
@@ -48,7 +48,7 @@
 @pytest.mark.anyio
 async def test_confirm_user_expired_token(async_client: AsyncClient, mocker):
     mocker.patch("storeapi.security.confirm_token_expire_minutes", return_value=-1)
-    spy = mocker.spy(tasks, "send_user_registration_email")
+    spy = mocker.spy(BackgroundTasks, "add_task")
     await register_user(async_client, "test@example.net", "1234")

     confirmation_url = str(spy.call_args[1]["confirmation_url"])