Skip to content

Commit 04585ac

Browse files
committed
Initialize chapter 9.
1 parent 40be253 commit 04585ac

20 files changed

+679
-0
lines changed

ch09/planner/auth/__init__.py

Whitespace-only changes.

ch09/planner/auth/authenticate.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from fastapi import Depends, HTTPException, status
2+
from fastapi.security import OAuth2PasswordBearer
3+
4+
from auth.jwt_handler import verify_access_token
5+
6+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/user/signin")
7+
8+
9+
async def authenticate(token: str = Depends(oauth2_scheme)) -> str:
10+
if not token:
11+
raise HTTPException(
12+
status_code=status.HTTP_403_FORBIDDEN,
13+
detail="Sign in for access"
14+
)
15+
16+
decoded_token = await verify_access_token(token)
17+
return decoded_token["user"]

ch09/planner/auth/hash_password.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from passlib.context import CryptContext
2+
3+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4+
5+
6+
class HashPassword:
7+
def create_hash(self, password: str) -> str:
8+
return pwd_context.hash(password)
9+
10+
def verify_hash(self, plain_password: str, hashed_password: str) -> bool:
11+
return pwd_context.verify(plain_password, hashed_password)

ch09/planner/auth/jwt_handler.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import time
2+
from datetime import datetime
3+
4+
from fastapi import HTTPException, status
5+
from jose import jwt, JWTError
6+
7+
from database.database import Settings
8+
from models.users import User
9+
10+
settings = Settings()
11+
12+
13+
def create_access_token(user: str) -> str:
14+
payload = {
15+
"user": user,
16+
"expires": time.time() + 3600
17+
}
18+
19+
token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
20+
return token
21+
22+
23+
async def verify_access_token(token: str) -> dict:
24+
try:
25+
data = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
26+
27+
expire = data.get("expires")
28+
29+
if expire is None:
30+
raise HTTPException(
31+
status_code=status.HTTP_400_BAD_REQUEST,
32+
detail="No access token supplied"
33+
)
34+
if datetime.utcnow() > datetime.utcfromtimestamp(expire):
35+
raise HTTPException(
36+
status_code=status.HTTP_403_FORBIDDEN,
37+
detail="Token expired!"
38+
)
39+
user_exist = await User.find_one(User.email == data["user"])
40+
if not user_exist:
41+
raise HTTPException(
42+
status_code=status.HTTP_400_BAD_REQUEST,
43+
detail="Invalid token"
44+
)
45+
46+
return data
47+
48+
except JWTError:
49+
raise HTTPException(
50+
status_code=status.HTTP_400_BAD_REQUEST,
51+
detail="Invalid token"
52+
)

ch09/planner/database/__init__.py

Whitespace-only changes.

ch09/planner/database/database.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from beanie import init_beanie, PydanticObjectId
2+
from motor.motor_asyncio import AsyncIOMotorClient
3+
from typing import Optional, Any, List
4+
from pydantic import BaseSettings, BaseModel
5+
6+
from models.users import User
7+
from models.events import Event
8+
9+
10+
class Settings(BaseSettings):
11+
DATABASE_URL: Optional[str] = None
12+
SECRET_KEY: Optional[str] = "default"
13+
14+
async def initialize_database(self):
15+
client = AsyncIOMotorClient(self.DATABASE_URL)
16+
await init_beanie(database=client.get_default_database(),
17+
document_models=[Event, User])
18+
19+
class Config:
20+
env_file = ".env"
21+
22+
23+
class Database:
24+
def __init__(self, model):
25+
self.model = model
26+
27+
async def save(self, document):
28+
await document.create()
29+
return
30+
31+
async def get(self, id: PydanticObjectId) -> bool:
32+
doc = await self.model.get(id)
33+
if doc:
34+
return doc
35+
return False
36+
37+
async def get_all(self) -> List[Any]:
38+
docs = await self.model.find_all().to_list()
39+
return docs
40+
41+
async def update(self, id: PydanticObjectId, body: BaseModel) -> Any:
42+
doc_id = id
43+
des_body = body.dict()
44+
45+
des_body = {k: v for k, v in des_body.items() if v is not None}
46+
update_query = {"$set": {
47+
field: value for field, value in des_body.items()
48+
}}
49+
50+
doc = await self.get(doc_id)
51+
if not doc:
52+
return False
53+
await doc.update(update_query)
54+
return doc
55+
56+
async def delete(self, id: PydanticObjectId) -> bool:
57+
doc = await self.get(id)
58+
if not doc:
59+
return False
60+
await doc.delete()
61+
return True

ch09/planner/main.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from fastapi import FastAPI
2+
from fastapi.responses import RedirectResponse
3+
from database.database import Settings
4+
5+
from routes.users import user_router
6+
from routes.events import event_router
7+
8+
import uvicorn
9+
10+
app = FastAPI()
11+
12+
settings = Settings()
13+
14+
# Register routes
15+
16+
app.include_router(user_router, prefix="/user")
17+
app.include_router(event_router, prefix="/event")
18+
19+
20+
@app.on_event("startup")
21+
async def init_db():
22+
await settings.initialize_database()
23+
24+
25+
@app.get("/")
26+
async def home():
27+
return RedirectResponse(url="/event/")
28+
29+
if __name__ == '__main__':
30+
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)

ch09/planner/models/__init__.py

Whitespace-only changes.

ch09/planner/models/events.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from beanie import Document
2+
from typing import Optional, List
3+
from pydantic import BaseModel
4+
5+
6+
class Event(Document):
7+
creator: Optional[str]
8+
title: str
9+
image: str
10+
description: str
11+
tags: List[str]
12+
location: str
13+
14+
class Config:
15+
schema_extra = {
16+
"example": {
17+
"title": "FastAPI Book Launch",
18+
"image": "https://linktomyimage.com/image.png",
19+
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
20+
"tags": ["python", "fastapi", "book", "launch"],
21+
"location": "Google Meet"
22+
}
23+
}
24+
25+
class Settings:
26+
name = "events"
27+
28+
29+
class EventUpdate(BaseModel):
30+
title: Optional[str]
31+
image: Optional[str]
32+
description: Optional[str]
33+
tags: Optional[List[str]]
34+
location: Optional[str]
35+
36+
class Config:
37+
schema_extra = {
38+
"example": {
39+
"title": "FastAPI BookLaunch",
40+
"image": "https://linktomyimage.com/image.png",
41+
"description": "We will be discussing the contents of the FastAPI book in this event.Ensure to come with your own copy to win gifts!",
42+
"tags": ["python", "fastapi", "book", "launch"],
43+
"location": "Google Meet"
44+
}
45+
}

ch09/planner/models/users.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from beanie import Document
2+
3+
from pydantic import BaseModel, EmailStr
4+
5+
6+
class User(Document):
7+
email: EmailStr
8+
password: str
9+
10+
class Settings:
11+
name = "users"
12+
13+
class Config:
14+
schema_extra = {
15+
"example": {
16+
"email": "fastapi@packt.com",
17+
"password": "strong!!!"
18+
}
19+
}
20+
21+
22+
class TokenResponse(BaseModel):
23+
access_token: str
24+
token_type: str

0 commit comments

Comments
 (0)