Skip to content

Commit 380ead4

Browse files
committed
add authentication to catalog api endpoints
1 parent 3e43095 commit 380ead4

File tree

12 files changed

+139
-78
lines changed

12 files changed

+139
-78
lines changed

src/api/dependencies.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import Annotated
22

3-
from fastapi import Depends, HTTPException, Request
3+
from fastapi import Depends, Request
44
from fastapi.security import OAuth2PasswordBearer
55

66
from modules.iam.application.services import IamService
7-
from modules.iam.domain.entities import AnonymousUser
7+
from modules.iam.domain.entities import User
88
from seedwork.application import Application, TransactionContext
99

1010
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@@ -16,19 +16,17 @@ async def get_application(request: Request) -> Application:
1616

1717

1818
async def get_transaction_context(
19-
request: Request, app: Annotated[Application, Depends(get_application)]
19+
app: Annotated[Application, Depends(get_application)],
2020
) -> TransactionContext:
2121
"""Creates a new transaction context for each request"""
2222

2323
with app.transaction_context() as ctx:
24-
try:
25-
access_token = await oauth2_scheme(request=request)
26-
current_user = ctx.get_service(IamService).find_user_by_access_token(
27-
access_token
28-
)
29-
except HTTPException as e:
30-
current_user = AnonymousUser()
24+
yield ctx
3125

32-
ctx.dependency_provider["current_user"] = current_user
3326

34-
yield ctx
27+
async def get_authenticated_user(
28+
access_token: Annotated[str, Depends(oauth2_scheme)],
29+
ctx: Annotated[TransactionContext, Depends(get_transaction_context)],
30+
) -> User:
31+
current_user = ctx.get_service(IamService).find_user_by_access_token(access_token)
32+
return current_user

src/api/main.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import FastAPI, Request
44
from fastapi.responses import JSONResponse
55

6+
from api.dependencies import oauth2_scheme # noqa
67
from api.routers import bidding, catalog, diagnostics, iam
78
from config.api_config import ApiConfig
89
from config.container import TopLevelContainer
@@ -17,17 +18,37 @@
1718
container.config.from_pydantic(ApiConfig())
1819

1920
app = FastAPI(debug=container.config.DEBUG)
21+
2022
app.include_router(catalog.router)
2123
app.include_router(bidding.router)
2224
app.include_router(iam.router)
2325
app.include_router(diagnostics.router)
2426
app.container = container
2527

26-
# logger.info("using db engine %s" % str(container.engine()))
28+
logger.info("using db engine %s" % str(container.db_engine()))
29+
30+
try:
31+
import uuid
32+
33+
from modules.iam.application.services import IamService
34+
35+
with app.container.application().transaction_context() as ctx:
36+
iam_service = ctx.get_service(IamService)
37+
iam_service.create_user(
38+
user_id=uuid.UUID(int=1),
39+
40+
password="password",
41+
access_token="token",
42+
)
43+
except ValueError as e:
44+
...
2745

2846

2947
@app.exception_handler(DomainException)
3048
async def unicorn_exception_handler(request: Request, exc: DomainException):
49+
if container.config.DEBUG:
50+
raise exc
51+
3152
return JSONResponse(
3253
status_code=500,
3354
content={"message": f"Oops! {exc} did something. There goes a rainbow..."},
@@ -45,37 +66,17 @@ async def unicorn_exception_handler(request: Request, exc: EntityNotFoundExcepti
4566

4667

4768
@app.middleware("http")
48-
async def add_request_context(request: Request, call_next):
69+
async def add_process_time(request: Request, call_next):
4970
start_time = time.time()
50-
# request_context.begin_request(current_user=CurrentUser.fake_user())
5171
try:
5272
response = await call_next(request)
5373
process_time = time.time() - start_time
5474
response.headers["X-Process-Time"] = str(process_time)
5575
return response
5676
finally:
5777
pass
58-
# request_context.end_request()
5978

6079

6180
@app.get("/")
6281
async def root():
6382
return {"info": "Online auctions API. See /docs for documentation"}
64-
65-
66-
@app.get("/test")
67-
async def test():
68-
import asyncio
69-
70-
logger.debug("test1")
71-
await asyncio.sleep(3)
72-
logger.debug("test2")
73-
await asyncio.sleep(3)
74-
logger.debug("test3")
75-
await asyncio.sleep(3)
76-
logger.debug("test4")
77-
service = container.dummy_service()
78-
return {
79-
"service response": service.serve(),
80-
"correlation_id": request_context.correlation_id,
81-
}

src/api/routers/catalog.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import uuid
12
from typing import Annotated
23
from uuid import UUID
34

45
from fastapi import APIRouter, Depends
56

6-
from api.dependencies import Application, get_application
7+
from api.dependencies import Application, User, get_application, get_authenticated_user
78
from api.models.catalog import ListingIndexModel, ListingReadModel, ListingWriteModel
89
from config.container import inject
910
from modules.catalog.application.command import (
@@ -13,7 +14,6 @@
1314
)
1415
from modules.catalog.application.query.get_all_listings import GetAllListings
1516
from modules.catalog.application.query.get_listing_details import GetListingDetails
16-
from seedwork.application import Application
1717
from seedwork.domain.value_objects import Money
1818

1919
"""
@@ -54,19 +54,21 @@ async def get_listing_details(
5454
async def create_listing(
5555
request_body: ListingWriteModel,
5656
app: Annotated[Application, Depends(get_application)],
57+
current_user: Annotated[User, Depends(get_authenticated_user)],
5758
):
5859
"""
5960
Creates a new listing.
6061
"""
6162
command = CreateListingDraftCommand(
63+
listing_id=uuid.uuid4(),
6264
title=request_body.title,
6365
description=request_body.description,
6466
ask_price=Money(request_body.ask_price_amount, request_body.ask_price_currency),
65-
seller_id=request_context.current_user.id,
67+
seller_id=current_user.id,
6668
)
67-
command_result = app.execute_command(command)
69+
app.execute_command(command)
6870

69-
query = GetListingDetails(listing_id=command_result.payload)
71+
query = GetListingDetails(listing_id=command.listing_id)
7072
query_result = app.execute_query(query)
7173
return dict(query_result.payload)
7274

@@ -76,13 +78,16 @@ async def create_listing(
7678
)
7779
@inject
7880
async def delete_listing(
79-
listing_id, app: Annotated[Application, Depends(get_application)]
81+
listing_id,
82+
app: Annotated[Application, Depends(get_application)],
83+
current_user: Annotated[User, Depends(get_authenticated_user)],
8084
):
8185
"""
8286
Delete listing
8387
"""
8488
command = DeleteListingDraftCommand(
8589
listing_id=listing_id,
90+
seller_id=current_user.id,
8691
)
8792
app.execute_command(command)
8893

src/api/routers/diagnostics.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@
22

33
from fastapi import APIRouter, Depends
44

5-
from api.dependencies import get_transaction_context
6-
from seedwork.application import TransactionContext
5+
from api.dependencies import (
6+
TransactionContext,
7+
User,
8+
get_authenticated_user,
9+
get_transaction_context,
10+
)
711

812
from .iam import UserResponse
913

1014
router = APIRouter()
1115

1216

1317
@router.get("/debug", tags=["diagnostics"])
14-
async def debug(ctx: Annotated[TransactionContext, Depends(get_transaction_context)]):
18+
async def debug(
19+
ctx: Annotated[TransactionContext, Depends(get_transaction_context)],
20+
current_user: Annotated[User, Depends(get_authenticated_user)],
21+
):
1522
return dict(
1623
app_id=id(ctx.app),
17-
user=UserResponse(
18-
id=str(ctx.current_user.id), username=ctx.current_user.username
19-
),
2024
name=ctx.app.name,
2125
version=ctx.app.version,
26+
user=UserResponse(
27+
id=str(current_user.id),
28+
username=current_user.username,
29+
access_token=current_user.access_token,
30+
),
2231
)

src/api/routers/iam.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@
44
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
55
from pydantic import BaseModel
66

7-
from api.dependencies import (
8-
Application,
9-
TransactionContext,
10-
get_application,
11-
get_transaction_context,
12-
)
7+
from api.dependencies import TransactionContext, get_transaction_context
138
from config.container import inject
149
from modules.iam.application.exceptions import InvalidCredentialsException
1510
from modules.iam.application.services import IamService
@@ -21,19 +16,25 @@
2116
class UserResponse(BaseModel):
2217
id: str
2318
username: str
19+
access_token: str
2420

2521

26-
@router.get("/token", tags=["iam"])
27-
async def get_token(app: Annotated[Application, Depends(get_application)]):
28-
return app.current_user.access_token
22+
class LoginResponse(BaseModel):
23+
access_token: str
24+
token_type: str
25+
26+
27+
# @router.get("/token", tags=["iam"])
28+
# async def get_token(ctx: Annotated[TransactionContext, Depends(get_transaction_context_for_public_route)]):
29+
# return ctx.current_user.access_token
2930

3031

3132
@router.post("/token", tags=["iam"])
3233
@inject
3334
async def login(
3435
ctx: Annotated[TransactionContext, Depends(get_transaction_context)],
3536
form_data: OAuth2PasswordRequestForm = Depends(),
36-
):
37+
) -> LoginResponse:
3738
try:
3839
iam_service = ctx.get_service(IamService)
3940
user = iam_service.authenticate_with_name_and_password(
@@ -46,11 +47,14 @@ async def login(
4647
detail="Incorrect username or password",
4748
)
4849

49-
return {"access_token": user.access_token, "token_type": "bearer"}
50+
return LoginResponse(access_token=user.access_token, token_type="bearer")
5051

5152

5253
@router.get("/users/me", tags=["iam"])
5354
async def get_users_me(
54-
app: Annotated[Application, Depends(get_application)],
55-
):
56-
return app.current_user
55+
ctx: Annotated[TransactionContext, Depends(get_transaction_context)],
56+
) -> UserResponse:
57+
user = ctx.current_user
58+
return UserResponse(
59+
id=str(user.id), username=user.username, access_token=user.access_token
60+
)

src/api/tests/test_catalog.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,32 +78,35 @@ def test_catalog_list_with_two_items(app, api_client):
7878
assert len(response_data) == 2
7979

8080

81-
def test_catalog_create_draft_fails_due_to_incomplete_data(api, api_client):
82-
response = api_client.post("/catalog")
81+
def test_catalog_create_draft_fails_due_to_incomplete_data(
82+
api, authenticated_api_client
83+
):
84+
response = authenticated_api_client.post("/catalog")
8385
assert response.status_code == 422
8486

8587

8688
@pytest.mark.integration
87-
def test_catalog_delete_draft(app, api_client):
89+
def test_catalog_delete_draft(app, authenticated_api_client):
90+
current_user = authenticated_api_client.current_user
8891
app.execute_command(
8992
CreateListingDraftCommand(
9093
listing_id=UUID(int=1),
9194
title="Listing to be deleted",
9295
description="...",
9396
ask_price=Money(10),
94-
seller_id=UUID("00000000000000000000000000000002"),
97+
seller_id=current_user.id,
9598
)
9699
)
97100

98-
response = api_client.delete(f"/catalog/{str(UUID(int=1))}")
101+
response = authenticated_api_client.delete(f"/catalog/{str(UUID(int=1))}")
99102

100103
assert response.status_code == 204
101104

102105

103106
@pytest.mark.integration
104-
def test_catalog_delete_non_existing_draft_returns_404(api_client):
107+
def test_catalog_delete_non_existing_draft_returns_404(authenticated_api_client):
105108
listing_id = UUID("00000000000000000000000000000001")
106-
response = api_client.delete(f"/catalog/{listing_id}")
109+
response = authenticated_api_client.delete(f"/catalog/{listing_id}")
107110
assert response.status_code == 404
108111

109112

src/api/tests/test_diagnostics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33

44
@pytest.mark.integration
5-
def test_debug_endpoint(api_client):
6-
response = api_client.get("/debug")
5+
def test_debug_endpoint(authenticated_api_client):
6+
response = authenticated_api_client.get("/debug")
77
assert response.status_code == 200

src/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import uuid
2+
13
import pytest
24
from fastapi.testclient import TestClient
35
from sqlalchemy import create_engine
46
from sqlalchemy.orm import Session
57

68
from api.main import app as fastapi_instance
79
from config.api_config import ApiConfig
10+
from modules.iam.application.services import IamService
811
from seedwork.infrastructure.database import Base
912

1013

@@ -36,6 +39,24 @@ def api_client(api, app):
3639
return client
3740

3841

42+
@pytest.fixture
43+
def authenticated_api_client(api, app):
44+
access_token = uuid.uuid4()
45+
with app.transaction_context() as ctx:
46+
iam: IamService = ctx[IamService]
47+
current_user = iam.create_user(
48+
uuid.UUID(int=1),
49+
50+
password="password",
51+
access_token=str(access_token),
52+
is_superuser=False,
53+
)
54+
headers = {"Authorization": f"bearer {access_token}"}
55+
client = TestClient(api, headers=headers)
56+
client.current_user = current_user
57+
return client
58+
59+
3960
@pytest.fixture
4061
def app(api, db_session):
4162
app = api.container.application()

0 commit comments

Comments
 (0)