🎨 Moved routes into separated files (!!!)
continuous-integration/drone/push Build is passing Details

master
Alix JEUDI--LEMOINE 3 months ago
parent cd926b6487
commit adc09564f4

@ -1,32 +1,12 @@
import bson
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from datetime import datetime, timedelta
from joserfc import jwt
from joserfc.errors import JoseError
from joserfc.jwk import OctKey
from bson.objectid import ObjectId
from app.utils import get_password_hash, verify_password
# Best workaround found for _id typed as ObjectId (creating Exception bcause JSON doesn't support custom types countrary to BSON, used by Mongo)
# also allows to create DTOs at the time, but not at it's best (project structure is chaotic FTM :s)
import app.serializers as serializers # Import all serializers (detailed in __init__.py)
# Import all routers
from app.routes.auth import auth_router
from app.routes.friends import friends_router
from app.routes.users import users_router
from app.routes.pins import pins_router
# Import models
from app.models import User, Friend, Token, TokenData, HTTPError
# Import all DTOs (detailed in __init__.py)
from app.dto import FriendAddDTO, UserDTO, UserRegisterDTO, PinDTO
# Contains all constants
import app.config as config
import pymongo
# Database setup
client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD)
db = client[config.MONGODB_DATABASE]
# FastAPI app instance
app = FastAPI(
@ -50,274 +30,8 @@ app.add_middleware(
allow_headers=["*"],
)
# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=config.TOKEN_URL)
# Collections
users_collection = db["users"]
pins_collection = db["pins"]
friends_collection = db["friends"]
# Token management
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.now() + expires_delta
to_encode.update({"exp": expire})
header = {"alg": config.ALGORITHM}
encoded_jwt = jwt.encode(header, to_encode, OctKey.import_key(config.SECRET_KEY))
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, OctKey.import_key(config.SECRET_KEY))
username: str = payload.claims["sub"]
expire_date = payload.claims["exp"]
if username is None or int(datetime.now().timestamp()) > expire_date:
raise credentials_exception
token_data = TokenData(username=username)
except JoseError:
raise credentials_exception
user = users_collection.find_one({"username": token_data.username})
if user is None:
raise credentials_exception
return serializers.user_serialize(user)
# Exceptions
def friend_not_found():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Friend not found"
)
def objectid_misformatted():
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="The ObjectID is misformatted"
)
# Routes
@app.post(
path="/register",
response_model=Token,
responses={409: {"model": HTTPError}}
)
async def register(user: UserRegisterDTO):
user_exists = users_collection.find_one({"username": user.username})
if user_exists:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already used"
)
hashed_password = get_password_hash(user.password)
user_id = users_collection.insert_one({"username": user.username, "password": hashed_password})
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
return {"access_token": access_token, "token_type": "bearer", "user_id": str(user_id.inserted_id)}
@app.post(
path="/login",
response_model=Token,
responses={401: {"model": HTTPError}}
)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users_collection.find_one({"username": form_data.username})
if not user or not verify_password(form_data.password, user["password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": form_data.username}, expires_delta=access_token_expires)
return {"access_token": access_token, "token_type": "bearer", "user_id": str(user["_id"])}
@app.get(
path="/logout",
responses={401: {"model": HTTPError}}
)
async def logout(current_user: User = Depends(get_current_user)):
return {"message": "Logged out"}
@app.get(
path="/pin/{id}",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def get_pin(id: str, current_user: User = Depends(get_current_user)):
try:
pin = pins_collection.find_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if pin is None:
raise HTTPException(status_code=404, detail="Pin not found")
return serializers.pin_serialize(pin)
@app.patch(
path="/pin/{id}",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def update_pin(id: str, pin: PinDTO, current_user: User = Depends(get_current_user)):
try:
result = pins_collection.update_one({"_id": ObjectId(id)}, {"$set": pin.model_dump()})
except bson.errors.InvalidId:
objectid_misformatted()
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Pin not found")
return {"message": "Pin updated"}
@app.post(
path="/pin/add",
responses={401: {"model": HTTPError}}
)
async def add_pin(pin: PinDTO, current_user: User = Depends(get_current_user)):
pin.user_id = current_user.uid
pin_id = pins_collection.insert_one(pin.model_dump()).inserted_id
return {"id": str(pin_id)}
@app.get(
path="/pins",
responses={401: {"model": HTTPError}}
)
async def list_pins(current_user: User = Depends(get_current_user)):
pins = serializers.pins_serialize(pins_collection.find().to_list(), current_user.uid)
return pins
@app.get(
path="/friend/{id}",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def get_friend(id: str, current_user: User = Depends(get_current_user)):
try:
friend = friends_collection.find_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if friend is None: friend_not_found()
return serializers.friend_serialize(friend,False if friend['user_id']==current_user.uid else True)
@app.post(
path="/friend/add",
responses={401: {"model": HTTPError}, 409: {"model": HTTPError}}
)
async def add_friend(friend_to_add: FriendAddDTO, current_user: User = Depends(get_current_user)):
# TODO: test if exists
friend: Friend = friend_to_add.model_dump()
if(current_user.uid == friend["friend_user_id"]):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Cannot add yourself as a friend"
)
friend["user_id"] = current_user.uid
friend["status"] = "pending"
friend_id = friends_collection.insert_one(friend).inserted_id
return {"id": str(friend_id)}
@app.delete(
path="/friend/{id}/delete",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def delete_friend(id: str, current_user: User = Depends(get_current_user)):
try:
result = friends_collection.delete_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if result.deleted_count == 0: friend_not_found()
return {"message": "Friend deleted"}
@app.patch(
path="/friend/{id}/accept",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def accept_friend(id: str, current_user: User = Depends(get_current_user)):
try:
check_friend = friends_collection.find_one({"_id": ObjectId(id)})
if check_friend is None: friend_not_found()
if check_friend["status"] != "pending":
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Friend request already accepted"
)
friends_collection.update_one({"_id": ObjectId(id)}, {"$set": {"status": "accepted"}})
except bson.errors.InvalidId:
objectid_misformatted()
return {"message": "Friend request accepted"}
@app.post(
path="/friend/{id}/deny",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def deny_friend(id: str, current_user: User = Depends(get_current_user)):
try:
result = friends_collection.update_one({"_id": ObjectId(id)}, {"$set": {"status": "denied"}})
except bson.errors.InvalidId:
objectid_misformatted()
if result.matched_count == 0: friend_not_found()
return {"message": "Friend request denied"}
@app.get(
path="/friends",
response_model=list[Friend],
responses={401: {"model": HTTPError}}
)
async def list_friends(current_user: User = Depends(get_current_user)):
return serializers.friends_serialize(friends_collection.find({"user_id": current_user.uid}).to_list(), friends_collection.find({"friend_user_id": current_user.uid}).to_list())
@app.get(
path="/users",
responses={401: {"model": HTTPError}, 422: {"model": HTTPError}},
response_model=list[UserDTO]
)
async def search_users(name: str, current_user: User = Depends(get_current_user)):
try:
users = serializers.users_serialize(users_collection.find({"username": {"$regex": name, "$options": "i"}}).to_list())
except pymongo.errors.OperationFailure:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Regex may be wrongly formatted")
return users
@app.get(
path="/user/{id}",
responses={401: {"model": HTTPError}, 422: {"model": HTTPError}, 404: {"model": HTTPError}},
response_model=UserDTO
)
async def get_user(id: str, current_user: User = Depends(get_current_user)):
try:
user = users_collection.find_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return serializers.user_serialize(user)
# Inclure les routeurs
app.include_router(auth_router)
app.include_router(friends_router)
app.include_router(users_router)
app.include_router(pins_router)

@ -0,0 +1,69 @@
from datetime import timedelta
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter, Depends, HTTPException, status
import pymongo
import app.config as config
from app.models import User, Token, HTTPError
from app.dto import UserRegisterDTO
from app.serializers import user_serialize
from app.utils import get_current_user, create_access_token, get_password_hash, verify_password
# Database setup
client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD)
db = client[config.MONGODB_DATABASE]
users_collection = db["users"]
auth_router = APIRouter(
tags=["Auth"]
)
@auth_router.post(
path="/register",
response_model=Token,
responses={409: {"model": HTTPError}}
)
async def register(user: UserRegisterDTO):
user_exists = users_collection.find_one({"username": user.username})
if user_exists:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already used"
)
hashed_password = get_password_hash(user.password)
user_id = users_collection.insert_one({"username": user.username, "password": hashed_password})
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
return {"access_token": access_token, "token_type": "bearer", "user_id": str(user_id.inserted_id)}
@auth_router.post(
path="/login",
response_model=Token,
responses={401: {"model": HTTPError}}
)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users_collection.find_one({"username": form_data.username})
if not user or not verify_password(form_data.password, user["password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": form_data.username}, expires_delta=access_token_expires)
return {"access_token": access_token, "token_type": "bearer", "user_id": str(user["_id"])}
@auth_router.get(
path="/logout",
responses={401: {"model": HTTPError}}
)
async def logout(current_user: User = Depends(get_current_user)):
return {"message": "Logged out"}

@ -0,0 +1,116 @@
from bson import ObjectId
import bson
from fastapi import APIRouter, HTTPException, status
from fastapi.params import Depends
import pymongo
from app.dto import FriendAddDTO
from app.models import HTTPError, User, Friend
from app.utils import friend_not_found, get_current_user, objectid_misformatted
import app.config as config
# Best workaround found for _id typed as ObjectId (creating Exception bcause JSON doesn't support custom types countrary to BSON, used by Mongo)
# also allows to create DTOs at the time, but not at it's best (project structure is chaotic FTM :s)
import app.serializers as serializers # Import all serializers (detailed in __init__.py)
# Database setup
client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD)
db = client[config.MONGODB_DATABASE]
friends_collection = db["friends"]
friends_router = APIRouter(
prefix="/friend",
tags=["Friends"]
)
@friends_router.get(
path="/{id}",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def get_friend(id: str, current_user: User = Depends(get_current_user)):
try:
friend = friends_collection.find_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if friend is None: friend_not_found()
return serializers.friend_serialize(friend,False if friend['user_id']==current_user.uid else True)
@friends_router.post(
path="/add",
responses={401: {"model": HTTPError}, 409: {"model": HTTPError}}
)
async def add_friend(friend_to_add: FriendAddDTO, current_user: User = Depends(get_current_user)):
# TODO: test if exists
friend: Friend = friend_to_add.model_dump()
if(current_user.uid == friend["friend_user_id"]):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Cannot add yourself as a friend"
)
friend["user_id"] = current_user.uid
friend["status"] = "pending"
friend_id = friends_collection.insert_one(friend).inserted_id
return {"id": str(friend_id)}
@friends_router.delete(
path="/{id}/delete",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def delete_friend(id: str, current_user: User = Depends(get_current_user)):
try:
result = friends_collection.delete_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if result.deleted_count == 0: friend_not_found()
return {"message": "Friend deleted"}
@friends_router.patch(
path="/{id}/accept",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def accept_friend(id: str, current_user: User = Depends(get_current_user)):
try:
check_friend = friends_collection.find_one({"_id": ObjectId(id)})
if check_friend is None: friend_not_found()
if check_friend["status"] != "pending":
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Friend request already accepted"
)
friends_collection.update_one({"_id": ObjectId(id)}, {"$set": {"status": "accepted"}})
except bson.errors.InvalidId:
objectid_misformatted()
return {"message": "Friend request accepted"}
@friends_router.post(
path="/{id}/deny",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def deny_friend(id: str, current_user: User = Depends(get_current_user)):
try:
result = friends_collection.update_one({"_id": ObjectId(id)}, {"$set": {"status": "denied"}})
except bson.errors.InvalidId:
objectid_misformatted()
if result.matched_count == 0: friend_not_found()
return {"message": "Friend request denied"}
@friends_router.get(
path="s",
response_model=list[Friend],
responses={401: {"model": HTTPError}}
)
async def list_friends(current_user: User = Depends(get_current_user)):
return serializers.friends_serialize(friends_collection.find({"user_id": current_user.uid}).to_list(), friends_collection.find({"friend_user_id": current_user.uid}).to_list())

@ -0,0 +1,73 @@
from bson import ObjectId
import bson
from fastapi import APIRouter, HTTPException
from fastapi.params import Depends
import pymongo
from app.dto import PinDTO
from app.models import HTTPError
from app.models.user import User
from app.utils import get_current_user, objectid_misformatted
import app.config as config
# Best workaround found for _id typed as ObjectId (creating Exception bcause JSON doesn't support custom types countrary to BSON, used by Mongo)
# also allows to create DTOs at the time, but not at it's best (project structure is chaotic FTM :s)
import app.serializers as serializers # Import all serializers (detailed in __init__.py)
# Database setup
client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD)
db = client[config.MONGODB_DATABASE]
pins_collection = db["pins"]
pins_router = APIRouter(
prefix="/pin",
tags=["Pins"]
)
@pins_router.get(
path="/{id}",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def get_pin(id: str, current_user: User = Depends(get_current_user)):
try:
pin = pins_collection.find_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if pin is None:
raise HTTPException(status_code=404, detail="Pin not found")
return serializers.pin_serialize(pin)
@pins_router.patch(
path="/{id}",
responses={401: {"model": HTTPError}, 404: {"model": HTTPError}, 422: {"model": HTTPError}}
)
async def update_pin(id: str, pin: PinDTO, current_user: User = Depends(get_current_user)):
try:
result = pins_collection.update_one({"_id": ObjectId(id)}, {"$set": pin.model_dump()})
except bson.errors.InvalidId:
objectid_misformatted()
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Pin not found")
return {"message": "Pin updated"}
@pins_router.post(
path="/add",
responses={401: {"model": HTTPError}}
)
async def add_pin(pin: PinDTO, current_user: User = Depends(get_current_user)):
pin.user_id = current_user.uid
pin_id = pins_collection.insert_one(pin.model_dump()).inserted_id
return {"id": str(pin_id)}
@pins_router.get(
path="s",
responses={401: {"model": HTTPError}}
)
async def list_pins(current_user: User = Depends(get_current_user)):
pins = serializers.pins_serialize(pins_collection.find().to_list(), current_user.uid)
return pins

@ -0,0 +1,56 @@
import bson
from bson import ObjectId
from fastapi import APIRouter, HTTPException, status
from fastapi.params import Depends
import pymongo
from app.dto import UserDTO
from app.models import HTTPError, User
from app.utils import get_current_user, objectid_misformatted
import app.config as config
# Database setup
client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD)
db = client[config.MONGODB_DATABASE]
users_collection = db["users"]
# Best workaround found for _id typed as ObjectId (creating Exception bcause JSON doesn't support custom types countrary to BSON, used by Mongo)
# also allows to create DTOs at the time, but not at it's best (project structure is chaotic FTM :s)
import app.serializers as serializers # Import all serializers (detailed in __init__.py)
users_router = APIRouter(
prefix="/user",
tags=["Users"]
)
@users_router.get(
path="s",
responses={401: {"model": HTTPError}, 422: {"model": HTTPError}},
response_model=list[UserDTO]
)
async def search_users(name: str, current_user: User = Depends(get_current_user)):
try:
users = serializers.users_serialize(users_collection.find({"username": {"$regex": name, "$options": "i"}}).to_list())
except pymongo.errors.OperationFailure:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Regex may be wrongly formatted")
return users
@users_router.get(
path="/{id}",
responses={401: {"model": HTTPError}, 422: {"model": HTTPError}, 404: {"model": HTTPError}},
response_model=UserDTO
)
async def get_user(id: str, current_user: User = Depends(get_current_user)):
try:
user = users_collection.find_one({"_id": ObjectId(id)})
except bson.errors.InvalidId:
objectid_misformatted()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return serializers.user_serialize(user)

@ -1,7 +1,74 @@
import hashlib
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from joserfc import jwt
from joserfc.errors import JoseError
from joserfc.jwk import OctKey
from datetime import datetime, timedelta
import pymongo
from app.models import User
import app.config as config
from app.models.token_data import TokenData
from app.serializers import user_serialize
# Database setup
client = pymongo.MongoClient(config.MONGODB_URL, username=config.MONGODB_USERNAME, password=config.MONGODB_PASSWORD)
db = client[config.MONGODB_DATABASE]
users_collection = db["users"]
def verify_password(plain_password, hashed_password):
return hashlib.sha256(plain_password.encode()).hexdigest() == hashed_password
def get_password_hash(password):
return hashlib.sha256(password.encode()).hexdigest()
return hashlib.sha256(password.encode()).hexdigest()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=config.TOKEN_URL)
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, OctKey.import_key(config.SECRET_KEY))
username: str = payload.claims["sub"]
expire_date = payload.claims["exp"]
if username is None or int(datetime.now().timestamp()) > expire_date:
raise credentials_exception
token_data = TokenData(username=username)
except JoseError:
raise credentials_exception
user = users_collection.find_one({"username": token_data.username})
if user is None:
raise credentials_exception
return user_serialize(user)
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.now() + expires_delta
to_encode.update({"exp": expire})
header = {"alg": config.ALGORITHM}
encoded_jwt = jwt.encode(header, to_encode, OctKey.import_key(config.SECRET_KEY))
return encoded_jwt
# Exceptions
def friend_not_found():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Friend not found"
)
def objectid_misformatted():
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="The ObjectID is misformatted"
)
Loading…
Cancel
Save