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"}