Skip to content

Commit

Permalink
串接大平台1 (#93)
Browse files Browse the repository at this point in the history
* Add lobby start game(/games) api endpoint

* Add fastapi depends to verify jwt token

* Create heath api

* Add async

* 1. Update JWT package
2. Add create jwt function to fix jwt_token expire problem

* Update config.py code style
  • Loading branch information
s9891326 authored Sep 19, 2023
1 parent f156077 commit d04e426
Show file tree
Hide file tree
Showing 16 changed files with 809 additions and 599 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

### Practice Stack

- 三層式架構 (MVC)
- 三層式架構 (MVC)=> clean architecture(CA)
- 用 event storming,找出遊戲的功能與流程
- 用 example mapping,確定需求的具體內容
- Test-Driven Development: ATDD
Expand Down
24 changes: 17 additions & 7 deletions love_letter/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import os

REPOSITORY_IMPL = os.environ.get("repository_impl")
DB_HOST = os.environ.get("DB_HOST", "127.0.0.1")
DB_PORT = os.environ.get("DB_PORT", 27017)
DB_USER = os.environ.get("DB_USER")
DB_PASSWORD = os.environ.get("DB_PASSWORD")
DB_NAME = os.environ.get("DB_NAME", "love_letter")
DB_COLLECTION = os.environ.get("DB_COLLECTION", "love_letter")

class Configuration:
REPOSITORY_IMPL = os.environ.get("repository_impl")
DB_HOST = os.environ.get("DB_HOST", "127.0.0.1")
DB_PORT = os.environ.get("DB_PORT", 27017)
DB_USER = os.environ.get("DB_USER")
DB_PASSWORD = os.environ.get("DB_PASSWORD")
DB_NAME = os.environ.get("DB_NAME", "love_letter")
DB_COLLECTION = os.environ.get("DB_COLLECTION", "love_letter")
FRONTEND_HOST = os.environ.get("FRONTEND_HOST", "http://127.0.0.1")
LOBBY_ISSUER = os.environ.get(
"LOBBY_ISSUER", "https://dev-1l0ixjw8yohsluoi.us.auth0.com/"
)
LOBBY_AUDIENCE = os.environ.get("LOBBY_ISSUER", "https://api.gaas.waterballsa.tw")


config = Configuration()
3 changes: 2 additions & 1 deletion love_letter/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ class Seen:


class Player:
def __init__(self, name: str):
def __init__(self, name: str, user_id: Union[str] = None):
self.id = user_id
self.name = name
self.cards: List[Card] = []
self.am_i_out: bool = False
Expand Down
5 changes: 5 additions & 0 deletions love_letter/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ class CardPlayedEvent(DomainEvent):
@dataclass
class GetStatusEvent(DomainEvent):
game: "Game"


@dataclass
class GameEvent(DomainEvent):
url: str
2 changes: 1 addition & 1 deletion love_letter/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pymongo import MongoClient
from pymongo.collection import Collection

from love_letter import config
from love_letter.config import config
from love_letter.models import Game
from love_letter.repository.data import GameData

Expand Down
3 changes: 2 additions & 1 deletion love_letter/repository/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ def to_dict(player: "Player", last_round: "Round") -> Dict:
for x in player.cards
],
score=player.tokens_of_affection,
id=player.id,
)

@staticmethod
def to_domain(player_dict: Dict) -> "Player":
player = Player(player_dict["name"])
player = Player(player_dict["name"], player_dict["id"])
player.am_i_out = player_dict["out"]
player.cards = [CardData.to_domain(c) for c in player_dict["cards"]]
player.seen_cards = [SeenData.to_domain(s) for s in player_dict["seen_cards"]]
Expand Down
14 changes: 14 additions & 0 deletions love_letter/usecase/lobby_start_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from love_letter.config import config
from love_letter.models import Game, Player
from love_letter.models.event import GameEvent
from love_letter.usecase.common import Presenter, game_repository


class LobbyStartGame:
def execute(self, input: "LobbyPlayers", presenter: Presenter):
game = Game()
for player in input.players:
game.join(Player(player.nickname, player.id))
game.start()
game_repository.save_or_update(game)
presenter.present([GameEvent(url=f"{config.FRONTEND_HOST}/games/{game.id}")])
19 changes: 17 additions & 2 deletions love_letter/web/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from typing import Union

import uvicorn
from fastapi import FastAPI
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware

from love_letter.models import GuessCard, ToSomeoneCard
from love_letter.usecase.create_game import CreateGame
from love_letter.usecase.get_status import GetStatus
from love_letter.usecase.join_game import JoinGame
from love_letter.usecase.lobby_start_game import LobbyStartGame
from love_letter.usecase.play_card import PlayCard
from love_letter.usecase.start_game import StartGame
from love_letter.web.dto import GameStatus
from love_letter.web.auth import JWTBearer
from love_letter.web.dto import GameStatus, LobbyPlayers

# isort: off
from love_letter.web.presenter import (
Expand All @@ -20,6 +22,7 @@
build_player_view,
PlayCardPresenter,
GetStatusPresenter,
LobbyStartGamePresenter,
)

# isort: on
Expand Down Expand Up @@ -89,6 +92,18 @@ async def get_status(game_id: str, player_id: str):
return build_player_view(game, player_id)


@app.post("/games", dependencies=[Depends(JWTBearer())])
async def lobby_start_game(players: LobbyPlayers):
presenter = LobbyStartGamePresenter.presenter()
LobbyStartGame().execute(players, presenter)
return presenter.as_view_model()


@app.get("/heath")
async def heath():
return {"success": True}


def run():
uvicorn.run("love_letter.web.app:app", host="0.0.0.0", port=8080, reload=True)

Expand Down
65 changes: 65 additions & 0 deletions love_letter/web/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import datetime

import jwt
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from starlette.requests import Request

from love_letter.config import config


class JWTBearer(HTTPBearer):
def __init__(self, auto_error: bool = True):
super().__init__(auto_error=auto_error)

async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super().__call__(request)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(
status_code=403, detail="Invalid authentication scheme."
)
if not self.verify_jwt(credentials.credentials):
raise HTTPException(
status_code=403, detail="Invalid token or expired token."
)
return credentials.credentials
else:
raise HTTPException(status_code=403, detail="Invalid authorization code.")

@staticmethod
def verify_jwt(token: str) -> bool:
"""
Verify jwt token iss and aud are as expected, and not expired
:param token:
:return:
"""
try:
jwt.decode(
token,
options={
"verify_signature": False,
"verify_iss": True,
"verify_aud": True,
"verify_exp": True,
},
audience=config.LOBBY_AUDIENCE,
issuer=config.LOBBY_ISSUER,
)
return True
except Exception as e:
print(e)
return False

@staticmethod
def create_jwt():
now = datetime.datetime.now()
token = jwt.encode(
payload={
"aud": config.LOBBY_AUDIENCE,
"iss": config.LOBBY_ISSUER,
"exp": now + datetime.timedelta(hours=1),
},
key="",
)
return f"Bearer {token}"
10 changes: 10 additions & 0 deletions love_letter/web/dto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class RoundModel(BaseModel):
class NamedPlayer(BaseModel):
name: str | None
score: int
id: str | None


class GameStatus(BaseModel):
Expand All @@ -40,3 +41,12 @@ class GameStatus(BaseModel):
events: List[Dict]
rounds: List[RoundModel]
final_winner: str | None


class LobbyPlayer(BaseModel):
id: str
nickname: str


class LobbyPlayers(BaseModel):
players: List[LobbyPlayer]
19 changes: 18 additions & 1 deletion love_letter/web/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from love_letter.models import Game, PlayerJoinedEvent

# isort: off
from love_letter.models.event import CardPlayedEvent, GetStatusEvent, StartGameEvent
from love_letter.models.event import (
CardPlayedEvent,
GetStatusEvent,
StartGameEvent,
GameEvent,
)

# isort: on
from love_letter.repository.data import GameData
Expand Down Expand Up @@ -141,3 +146,15 @@ def as_view_model(self) -> Game:
@classmethod
def presenter(cls) -> "GetStatusPresenter":
return GetStatusPresenter()


class LobbyStartGamePresenter(Presenter):
def as_view_model(self):
for event in self.events:
if isinstance(event, GameEvent):
return event
raise BaseException("Game is unavailable.")

@classmethod
def presenter(cls) -> "LobbyStartGamePresenter":
return LobbyStartGamePresenter()
Loading

0 comments on commit d04e426

Please sign in to comment.