User email confirmation

Adding a user confirmation endpoint

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/database.py
--- 
+++ 
@@ -19,6 +19,7 @@
     sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
     sqlalchemy.Column("email", sqlalchemy.String, unique=True),
     sqlalchemy.Column("password", sqlalchemy.String),
+    sqlalchemy.Column("confirmed", sqlalchemy.Boolean, default=False)
 )
storeapi/routers/user.py
--- 
+++ 
@@ -1,13 +1,14 @@
 import logging

-from fastapi import APIRouter, HTTPException, status
-
+from fastapi import APIRouter, HTTPException, Request, status
 from storeapi.database import database, user_table
 from storeapi.models.user import UserIn
 from storeapi.security import (
     authenticate_user,
     create_access_token,
+    create_confirmation_token,
     get_password_hash,
+    get_subject_for_token_type,
     get_user,
 )

@@ -16,7 +17,7 @@


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

     await database.execute(query)
-    return {"detail": "User created."}
+    return {
+        "detail": "User created. Please confirm your email.",
+        "confirmation_url": request.url_for(
+            "confirm_email", token=create_confirmation_token(user.email)
+        ),
+    }


 @router.post("/token")
@@ -36,3 +42,16 @@
     user = await authenticate_user(user.email, user.password)
     access_token = create_access_token(user.email)
     return {"access_token": access_token, "token_type": "bearer"}
+
+
+@router.get("/confirm/{token}")
+async def confirm_email(token: str):
+    email = get_subject_for_token_type(token, "confirmation")
+    query = (
+        user_table.update().where(user_table.c.email == email).values(confirmed=True)
+    )
+
+    logger.debug(query)
+
+    await database.execute(query)
+    return {"detail": "User confirmed"}
storeapi/tests/test_security.py
--- 
+++ 
@@ -1,5 +1,3 @@
-import time
-
 import pytest
 from jose import jwt
 from storeapi import security
storeapi/tests/routers/test_user.py
--- 
+++ 
@@ -1,4 +1,5 @@
 import pytest
+from fastapi import Request
 from httpx import AsyncClient


@@ -27,6 +28,40 @@


 @pytest.mark.anyio
+async def test_confirm_user(async_client: AsyncClient, mocker):
+    spy = mocker.spy(Request, "url_for")
+    await register_user(async_client, "test@example.net", "1234")
+
+    # We only call Request.url_for once, so this is OK.
+    # This is not a scalable solution.
+    # A better solution will be discussed in the next couple lectures.
+    confirmation_url = str(spy.spy_return)
+    response = await async_client.get(confirmation_url)
+
+    assert response.status_code == 200
+    assert "User confirmed" in response.json()["detail"]
+
+
+@pytest.mark.anyio
+async def test_confirm_user_invalid_token(async_client: AsyncClient):
+    response = await async_client.get("/confirm/invalid_token")
+    assert response.status_code == 401
+
+
+@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(Request, "url_for")
+    await register_user(async_client, "test@example.net", "1234")
+
+    confirmation_url = str(spy.spy_return)
+    response = await async_client.get(confirmation_url)
+
+    assert response.status_code == 401
+    assert "Token has expired" in response.json()["detail"]
+
+
+@pytest.mark.anyio
 async def test_login_user_not_exists(async_client: AsyncClient):
     response = await async_client.post(
         "/token", json={"email": "test@example.net", "password": "1234"}