diff --git a/app/main.py b/app/main.py index c22ce66..ab7f5d0 100644 --- a/app/main.py +++ b/app/main.py @@ -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) \ No newline at end of file +# Inclure les routeurs +app.include_router(auth_router) +app.include_router(friends_router) +app.include_router(users_router) +app.include_router(pins_router) \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/auth.py b/app/routes/auth.py new file mode 100644 index 0000000..c3af68e --- /dev/null +++ b/app/routes/auth.py @@ -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"} diff --git a/app/routes/friends.py b/app/routes/friends.py new file mode 100644 index 0000000..a95b427 --- /dev/null +++ b/app/routes/friends.py @@ -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()) diff --git a/app/routes/pins.py b/app/routes/pins.py new file mode 100644 index 0000000..38c85eb --- /dev/null +++ b/app/routes/pins.py @@ -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 \ No newline at end of file diff --git a/app/routes/users.py b/app/routes/users.py new file mode 100644 index 0000000..308d9ab --- /dev/null +++ b/app/routes/users.py @@ -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) \ No newline at end of file diff --git a/app/utils.py b/app/utils.py index 90221dc..349e7ec 100644 --- a/app/utils.py +++ b/app/utils.py @@ -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() \ No newline at end of file + 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" + ) \ No newline at end of file