Skip to content

Commit 2f24d47

Browse files
committed
add basic auth, add bidding endpoints
1 parent 3f4fe5c commit 2f24d47

33 files changed

+499
-145
lines changed

poetry.lock

+29-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ python = "^3.10.0"
99
pytest = "^6.2.4"
1010
pydantic = "^1.8.2"
1111
black = "^21.5b1"
12-
fastapi = "^0.67.0"
12+
fastapi = "^0.95.2"
1313
uvicorn = "^0.14.0"
1414
starlette-context = "^0.3.3"
1515
SQLAlchemy = "^1.4.22"
@@ -27,6 +27,7 @@ pre-commit = "^2.20.0"
2727
click = "8.0.4"
2828
httpx = "^0.23.1"
2929
requests = "^2.28.1"
30+
bcrypt = "^4.0.1"
3031

3132
[tool.poetry.dev-dependencies]
3233
poethepoet = "^0.10.0"

src/api/dependencies.py

+56-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,59 @@
1-
from pydantic import BaseModel
1+
from collections.abc import Callable
22

3-
from seedwork.domain.value_objects import UUID
3+
from fastapi import Depends, HTTPException, Request
4+
from fastapi.security import OAuth2PasswordBearer
45

6+
from config.container import Container, inject
7+
from modules.iam.domain.entities import User
8+
from seedwork.application import Application
59

6-
class CurrentUser(BaseModel):
7-
id: UUID
8-
username = "fake_current_user"
9-
10-
is_admin = True
10+
from .shared import dependency
11+
12+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
13+
14+
15+
def allow_role(role: str):
16+
def check(user: User):
17+
if user.role != role:
18+
raise HTTPException(status_code=403, detail="Forbidden")
19+
return True
20+
21+
return check
22+
23+
24+
def allow_authenticated():
25+
def check(user: User):
26+
if not user.is_authenticated():
27+
raise HTTPException(status_code=403, detail="Forbidden")
28+
return True
29+
30+
return check
31+
32+
33+
def allow_anonymous():
34+
return lambda user: True
35+
36+
37+
def create_app(check: callable) -> Callable[..., Application]:
38+
@inject
39+
async def create(
40+
request: Request, app: Application = dependency(Container.application)
41+
):
42+
try:
43+
access_token = await oauth2_scheme(request=request)
44+
current_user = app.iam_service.find_user_by_access_token(access_token)
45+
except HTTPException as e:
46+
current_user = User.Anonymous()
47+
48+
print("current user", current_user)
49+
check(current_user)
50+
app.current_user = current_user
51+
return app
52+
53+
return create
54+
55+
56+
def get_current_active_user(
57+
app: Application = Depends(create_app(allow_authenticated())),
58+
):
59+
return app.current_user

src/api/main.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import api.routers.catalog
77
from api.models.catalog import CurrentUser
8-
from api.routers import bidding, catalog, iam
8+
from api.routers import bidding, catalog, diagnostics, iam
99
from config.api_config import ApiConfig
1010
from config.container import Container
1111
from seedwork.domain.exceptions import DomainException, EntityNotFoundException
@@ -18,12 +18,21 @@
1818
# dependency injection container
1919
container = Container()
2020
container.config.from_pydantic(ApiConfig())
21-
container.wire(modules=[api.routers.catalog, api.routers.bidding, api.routers.iam])
21+
container.wire(
22+
modules=[
23+
api.dependencies,
24+
api.routers.catalog,
25+
api.routers.bidding,
26+
api.routers.iam,
27+
api.routers.diagnostics,
28+
]
29+
)
2230

2331
app = FastAPI(debug=container.config.DEBUG)
2432
app.include_router(catalog.router)
2533
app.include_router(bidding.router)
2634
app.include_router(iam.router)
35+
app.include_router(diagnostics.router)
2736
app.container = container
2837

2938
logger.info("using db engine %s" % str(container.engine()))

src/api/models/bidding.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ class BidReadModel(BaseModel):
1111
bidder_username: str
1212

1313

14-
class BiddingReadModel(BaseModel):
14+
class BiddingResponse(BaseModel):
1515
listing_id: UUID
1616
auction_status: str = "active" # active, ended
1717
auction_end_date: datetime
1818
bids: list[BidReadModel]
19+
20+
21+
class PlaceBidRequest(BaseModel):
22+
bidder_id: UUID
23+
amount: float

src/api/routers/bidding.py

+64-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from fastapi import APIRouter
22

3-
from api.models.bidding import BiddingReadModel
3+
from api.models.bidding import BiddingResponse, PlaceBidRequest
44
from api.shared import dependency
55
from config.container import Container, inject
6+
from modules.bidding.application.command import PlaceBidCommand, RetractBidCommand
67
from modules.bidding.application.query.get_bidding_details import GetBiddingDetails
78
from seedwork.application import Application
89

@@ -13,7 +14,7 @@
1314
"""
1415

1516

16-
@router.get("/bidding/{listing_id}", tags=["bidding"], response_model=BiddingReadModel)
17+
@router.get("/bidding/{listing_id}", tags=["bidding"], response_model=BiddingResponse)
1718
@inject
1819
async def get_bidding_details_of_listing(
1920
listing_id,
@@ -25,7 +26,67 @@ async def get_bidding_details_of_listing(
2526
query = GetBiddingDetails(listing_id=listing_id)
2627
query_result = app.execute_query(query)
2728
payload = query_result.payload
28-
return BiddingReadModel(
29+
return BiddingResponse(
30+
listing_id=str(payload.id),
31+
auction_end_date=payload.ends_at,
32+
bids=payload.bids,
33+
)
34+
35+
36+
@router.post(
37+
"/bidding/{listing_id}/place_bid", tags=["bidding"], response_model=BiddingResponse
38+
)
39+
@inject
40+
async def place_bid(
41+
listing_id,
42+
request_body: PlaceBidRequest,
43+
app: Application = dependency(Container.application),
44+
):
45+
"""
46+
Places a bid on a listing
47+
"""
48+
# TODO: get bidder from current user
49+
50+
command = PlaceBidCommand(
51+
listing_id=listing_id,
52+
bidder_id=request_body.bidder_id,
53+
amount=request_body.amount,
54+
)
55+
app.execute_command(command)
56+
57+
query = GetBiddingDetails(listing_id=listing_id)
58+
query_result = app.execute_query(query)
59+
payload = query_result.payload
60+
return BiddingResponse(
61+
listing_id=str(payload.id),
62+
auction_end_date=payload.ends_at,
63+
bids=payload.bids,
64+
)
65+
66+
67+
@router.post(
68+
"/bidding/{listing_id}/retract_bid",
69+
tags=["bidding"],
70+
response_model=BiddingResponse,
71+
)
72+
@inject
73+
async def retract_bid(
74+
listing_id,
75+
app: Application = dependency(Container.application),
76+
):
77+
"""
78+
Retracts a bid from a listing
79+
"""
80+
command = RetractBidCommand(
81+
listing_id=listing_id,
82+
bidder_id="",
83+
)
84+
app.execute_command(command)
85+
86+
query = GetBiddingDetails(listing_id=listing_id)
87+
query_result = app.execute_query(query)
88+
payload = query_result.payload
89+
return BiddingResponse(
2990
listing_id=str(payload.id),
3091
auction_end_date=payload.ends_at,
3192
bids=payload.bids,

src/api/routers/diagnostics.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends
4+
5+
from api.dependencies import allow_anonymous, create_app
6+
from seedwork.application import Application
7+
8+
router = APIRouter()
9+
10+
11+
from .iam import UserResponse
12+
13+
app_for_anonymous = create_app(allow_anonymous())
14+
15+
16+
@router.get("/debug", tags=["diagnostics"])
17+
async def debug(app: Annotated[Application, Depends(app_for_anonymous)]):
18+
return dict(
19+
app_id=id(app),
20+
user=UserResponse(
21+
id=str(app.current_user.id), username=app.current_user.username
22+
),
23+
name=app.name,
24+
version=app.version,
25+
)

0 commit comments

Comments
 (0)