From d197c6a475cdba6a5272499565d82b4f6919400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Vit=C3=B3ria=20Silva?= Date: Mon, 8 Jan 2024 00:13:21 +0000 Subject: [PATCH] Backend optimizations - Added constants file - Added api version to metadata on api responses - Fixed bug that some routes raised an exception when date and datetime columns where set in the database and not properly parsed before JSON Response --- backend/constants.py | 6 ++ backend/controllers/activityController.py | 77 +++++++++--------- backend/controllers/followerController.py | 65 +++++++++------- backend/controllers/gearController.py | 29 +++++-- backend/controllers/sessionController.py | 8 +- backend/controllers/userController.py | 95 +++++++++++++++++------ 6 files changed, 184 insertions(+), 96 deletions(-) create mode 100644 backend/constants.py diff --git a/backend/constants.py b/backend/constants.py new file mode 100644 index 00000000..13c84d4a --- /dev/null +++ b/backend/constants.py @@ -0,0 +1,6 @@ +# Constant related to version +API_VERSION="v0.1.2" + +# Constants related to user access types +ADMIN_ACCESS = 2 +REGULAR_ACCESS = 1 \ No newline at end of file diff --git a/backend/controllers/activityController.py b/backend/controllers/activityController.py index 09b27acf..38319329 100644 --- a/backend/controllers/activityController.py +++ b/backend/controllers/activityController.py @@ -59,6 +59,7 @@ from dependencies import get_db_session, create_error_response, get_current_user from sqlalchemy.orm import Session from fastapi.responses import JSONResponse +from constants import API_VERSION # Define the API router router = APIRouter() @@ -220,7 +221,7 @@ async def read_activities_all( ] # Include metadata in the response - metadata = {"total_records": len(activity_records)} + metadata = {"total_records": len(activity_records), "api_version": API_VERSION} # Return the queried values using JSONResponse return JSONResponse( @@ -272,7 +273,7 @@ async def read_activities_useractivities( ] # Include metadata in the response - metadata = {"total_records": len(activity_records)} + metadata = {"total_records": len(activity_records), "api_version": API_VERSION} # Return the queried values using JSONResponse return JSONResponse( @@ -347,7 +348,12 @@ async def read_activities_useractivities_thisweek_number( ] # Include metadata in the response - metadata = {"total_records": len(activity_records)} + metadata = { + "total_records": len(activity_records), + "user_id": user_id, + "week_number": week_number, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( @@ -416,15 +422,13 @@ async def read_activities_useractivities_thisweek_distances( distances = calculate_activity_distances(activity_records) # Return the queried values using JSONResponse - #return JSONResponse(content=distances) - + # return JSONResponse(content=distances) + # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": distances} - ) + return JSONResponse(content={"metadata": metadata, "content": distances}) except JWTError: # Return an error response if the user is not authenticated return create_error_response("UNAUTHORIZED", "Unauthorized", 401) @@ -487,15 +491,13 @@ async def read_activities_useractivities_thismonth_distances( distances = calculate_activity_distances(activity_records) # Return the queried values using JSONResponse - #return JSONResponse(content=distances) - + # return JSONResponse(content=distances) + # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": distances} - ) + return JSONResponse(content={"metadata": metadata, "content": distances}) except JWTError: # Return an error response if the user is not authenticated return create_error_response("UNAUTHORIZED", "Unauthorized", 401) @@ -554,12 +556,10 @@ async def read_activities_useractivities_thismonth_number( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_count} - ) + return JSONResponse(content={"metadata": metadata, "content": activity_count}) except JWTError: # Return an error response if the user is not authenticated @@ -575,9 +575,9 @@ async def read_activities_useractivities_thismonth_number( ) -@router.get("/activities/gear/{gearID}", response_model=List[dict]) +@router.get("/activities/gear/{gear_id}", response_model=List[dict]) async def read_activities_gearactivities( - gearID=int, + gear_id=int, user_id: int = Depends(get_current_user), db_session: Session = Depends(get_db_session), ): @@ -600,7 +600,7 @@ async def read_activities_gearactivities( # Query the activities records using SQLAlchemy activity_records = ( db_session.query(Activity) - .filter(Activity.user_id == user_id, Activity.gear_id == gearID) + .filter(Activity.user_id == user_id, Activity.gear_id == gear_id) .order_by(desc(Activity.start_time)) .all() ) @@ -611,7 +611,11 @@ async def read_activities_gearactivities( ] # Include metadata in the response - metadata = {"total_records": len(activity_records)} + metadata = { + "total_records": len(activity_records), + "gear_id": gear_id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( @@ -655,12 +659,10 @@ async def read_activities_all_number( activity_count = db_session.query(func.count(Activity.id)).scalar() # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_count} - ) + return JSONResponse(content={"metadata": metadata, "content": activity_count}) except JWTError: # Return an error response if the user is not authenticated @@ -701,12 +703,10 @@ async def read_activities_useractivities_number( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_count} - ) + return JSONResponse(content={"metadata": metadata, "content": activity_count}) except JWTError: # Return an error response if the user is not authenticated @@ -756,12 +756,10 @@ async def read_activities_followed_useractivities_number( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_count} - ) + return JSONResponse(content={"metadata": metadata, "content": activity_count}) except JWTError: # Return an error response if the user is not authenticated @@ -826,6 +824,7 @@ async def read_activities_all_pagination( "total_records": len(activity_records), "page_number": pageNumber, "num_records": numRecords, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -891,6 +890,7 @@ async def read_activities_useractivities_pagination( "total_records": len(activity_records), "page_number": pageNumber, "num_records": numRecords, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -966,6 +966,7 @@ async def read_activities_followed_user_activities_pagination( "total_records": len(activity_records), "page_number": pageNumber, "num_records": numRecords, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -1032,7 +1033,11 @@ async def read_activities_activityFromId( ] # Include metadata in the response - metadata = {"total_records": len(activity_records)} + metadata = { + "total_records": len(activity_records), + "id": id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( diff --git a/backend/controllers/followerController.py b/backend/controllers/followerController.py index 07983173..9e29056b 100644 --- a/backend/controllers/followerController.py +++ b/backend/controllers/followerController.py @@ -46,6 +46,7 @@ ) from dependencies import get_db_session, create_error_response from sqlalchemy.orm import Session +from constants import API_VERSION # Define the API router router = APIRouter() @@ -97,7 +98,12 @@ async def read_followers_user_specific_user( if follower: # Include metadata in the response - metadata = {"total_records": 1} + metadata = { + "total_records": 1, + "user_id": user_id, + "target_user_id": target_user_id, + "api_version": API_VERSION, + } # User follows target_user_id or vice versa response_data = { @@ -112,14 +118,18 @@ async def read_followers_user_specific_user( ) # Users are not following each other - return create_error_response("NOT_FOUND", "Users are not following each other.", 404) + return create_error_response( + "NOT_FOUND", "Users are not following each other.", 404 + ) except JWTError: # Return an error response if the user is not authenticated return create_error_response("UNAUTHORIZED", "Unauthorized", 401) except Exception as err: # Log the error and return an error response - logger.error(f"Error in read_followers_user_specific_user: {err}", exc_info=True) + logger.error( + f"Error in read_followers_user_specific_user: {err}", exc_info=True + ) return create_error_response( "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 ) @@ -156,12 +166,10 @@ async def get_user_follower_count_all( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": follower_count} - ) + return JSONResponse(content={"metadata": metadata, "content": follower_count}) except JWTError: # Return an error response if the user is not authenticated @@ -174,7 +182,6 @@ async def get_user_follower_count_all( ) - @router.get("/followers/user/{user_id}/followers/count") async def get_user_follower_count( user_id: int, @@ -208,12 +215,10 @@ async def get_user_follower_count( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": follower_count} - ) + return JSONResponse(content={"metadata": metadata, "content": follower_count}) except JWTError: # Return an error response if the user is not authenticated @@ -263,12 +268,14 @@ async def get_user_follower_all( ] # Include metadata in the response - metadata = {"total_records": len(followers_list)} + metadata = { + "total_records": len(followers_list), + "user_id": user_id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": followers_list} - ) + return JSONResponse(content={"metadata": metadata, "content": followers_list}) except JWTError: # Return an error response if the user is not authenticated @@ -312,12 +319,10 @@ async def get_user_following_count_all( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": following_count} - ) + return JSONResponse(content={"metadata": metadata, "content": following_count}) except JWTError: # Return an error response if the user is not authenticated @@ -363,12 +368,10 @@ async def get_user_following_count( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "user_id": user_id, "api_version": API_VERSION} # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": following_count} - ) + return JSONResponse(content={"metadata": metadata, "content": following_count}) except JWTError: # Return an error response if the user is not authenticated @@ -421,12 +424,14 @@ async def get_user_following_all( ] # Include metadata in the response - metadata = {"total_records": len(following_list)} + metadata = { + "total_records": len(following_list), + "user_id": user_id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": following_list} - ) + return JSONResponse(content={"metadata": metadata, "content": following_list}) except JWTError: # Return an error response if the user is not authenticated @@ -543,7 +548,9 @@ async def create_follow( if existing_follow: # Follow relationship already exists - return create_error_response("BAD_REQUEST", "Follow relationship already exists.", 400) + return create_error_response( + "BAD_REQUEST", "Follow relationship already exists.", 400 + ) # Create a new follow relationship new_follow = Follower( diff --git a/backend/controllers/gearController.py b/backend/controllers/gearController.py index 4a5c4928..2068990b 100644 --- a/backend/controllers/gearController.py +++ b/backend/controllers/gearController.py @@ -47,6 +47,7 @@ from pydantic import BaseModel from dependencies import get_db_session, create_error_response, get_current_user from fastapi.responses import JSONResponse +from constants import API_VERSION # Define the API router router = APIRouter() @@ -162,7 +163,7 @@ async def read_gear_all( gear_records_dict = [gear_record_to_dict(record) for record in gear_records] # Include metadata in the response - metadata = {"total_records": len(gear_records)} + metadata = {"total_records": len(gear_records), "api_version": API_VERSION} # Return the queried values using JSONResponse return JSONResponse( @@ -211,7 +212,11 @@ async def read_gear_all_by_type( # Validate the gear type (example validation) if not (1 <= gear_type <= 3): # Return an error response if the gear type in invalid - return create_error_response("UNPROCESSABLE COMTENT", "Invalid gear type. Must be between 1 and 3", 422) + return create_error_response( + "UNPROCESSABLE COMTENT", + "Invalid gear type. Must be between 1 and 3", + 422, + ) # Query the gear records using SQLAlchemy and filter by gear type and user ID gear_records = ( @@ -228,6 +233,7 @@ async def read_gear_all_by_type( metadata = { "total_records": len(gear_records), "gear_type": gear_type, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -278,7 +284,7 @@ async def read_gear_number( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "api_version": API_VERSION} # Return the queried values using JSONResponse return JSONResponse(content={"metadata": metadata, "content": gear_count}) @@ -297,7 +303,7 @@ async def read_gear_number( @router.get( "/gear/all/pagenumber/{pageNumber}/numRecords/{numRecords}", response_model=List[dict], - #tags=["Pagination"], + # tags=["Pagination"], ) async def read_gear_all_pagination( pageNumber: int, @@ -344,6 +350,7 @@ async def read_gear_all_pagination( "total_records": len(gear_records), "page_number": pageNumber, "num_records": numRecords, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -407,6 +414,7 @@ async def read_gear_nickname( metadata = { "total_records": len(gear_records), "nickname": nickname, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -464,6 +472,7 @@ async def read_gear_id( metadata = { "total_records": len(gear_records), "id": id, + "api_version": API_VERSION, } # Return the queried values using JSONResponse @@ -594,7 +603,11 @@ async def edit_gear( ) else: # Return an error response if the gear record does not belong to the user - return create_error_response("NOT_FOUND", f"Gear does not belong to user {user_id}. Will not delete", 404) + return create_error_response( + "NOT_FOUND", + f"Gear does not belong to user {user_id}. Will not delete", + 404, + ) else: # Return an error response if the gear record is not found return create_error_response("NOT_FOUND", "Gear not found", 404) @@ -657,7 +670,11 @@ async def delete_gear( ) else: # Return an error response if the gear record does not belong to the user - return create_error_response("NOT_FOUND", f"Gear does not belong to user {user_id}. Will not delete", 404) + return create_error_response( + "NOT_FOUND", + f"Gear does not belong to user {user_id}. Will not delete", + 404, + ) else: # Return an error response if the gear record is not found return create_error_response("NOT_FOUND", "Gear not found", 404) diff --git a/backend/controllers/sessionController.py b/backend/controllers/sessionController.py index 74c93f4f..32fe9e1c 100644 --- a/backend/controllers/sessionController.py +++ b/backend/controllers/sessionController.py @@ -65,6 +65,8 @@ # Custom dependencies for dependency injection in FastAPI from dependencies import get_db_session, create_error_response +from constants import ADMIN_ACCESS + # Define the API router router = APIRouter() @@ -434,7 +436,7 @@ def get_user_data(db_session: Session, token: str = Depends(oauth2_scheme)): "username": user.username, "email": user.email, "city": user.city, - "birthdate": user.birthdate, + "birthdate": user.birthdate.strftime('%Y-%m-%d') if user.birthdate else None, "preferred_language": user.preferred_language, "gender": user.gender, "access_type": user.access_type, @@ -487,6 +489,8 @@ def validate_token(db_session: Session, token: str): ) if not access_token or datetime.utcnow() > datetime.fromtimestamp(exp): + logger.info("Token expired, will force remove_expired_tokens to run") + remove_expired_tokens(db_session=Session) raise JWTError("Token expired") else: return {"message": "Token is valid"} @@ -517,7 +521,7 @@ def validate_admin_access(token: str): """ try: user_access_type = get_access_type_from_token(token) - if user_access_type != 2: + if user_access_type != ADMIN_ACCESS: return create_error_response("UNAUTHORIZED", "Unauthorized", 401) except JWTError: raise JWTError("Invalid token") diff --git a/backend/controllers/userController.py b/backend/controllers/userController.py index 0546ffa5..70ebcaea 100644 --- a/backend/controllers/userController.py +++ b/backend/controllers/userController.py @@ -53,6 +53,7 @@ ) from urllib.parse import unquote from dependencies import get_db_session, create_error_response +from constants import API_VERSION # Define the API router router = APIRouter() @@ -94,19 +95,22 @@ class UserBase(BaseModel): photo_path_aux: Optional[str] is_active: int + class UserCreateRequest(UserBase): """ Pydantic model for creating user records. Inherits from UserBase, which defines the base attributes for user. - This class extends the UserBase Pydantic model and is designed for creating + This class extends the UserBase Pydantic model and is designed for creating new user records. Includes an additional attribute 'password' to idefine user password. """ + password: str + class UserEditRequest(UserBase): """ Pydantic model for editing user records. @@ -116,8 +120,10 @@ class UserEditRequest(UserBase): This class extends the UserBase Pydantic model and is specifically tailored for editing existing user records. """ + pass + class UserResponse(UserBase): """ Pydantic model for representing user responses. @@ -135,6 +141,7 @@ class UserResponse(UserBase): id: int is_strava_linked: Optional[int] + # Define a function to convert User SQLAlchemy objects to dictionaries def user_record_to_dict(record: User) -> dict: """ @@ -154,7 +161,9 @@ def user_record_to_dict(record: User) -> dict: "username": record.username, "email": record.email, "city": record.city, - "birthdate": record.birthdate, + "birthdate": record.birthdate.strftime("%Y-%m-%d") + if record.birthdate + else None, "preferred_language": record.preferred_language, "gender": record.gender, "access_type": record.access_type, @@ -164,7 +173,11 @@ def user_record_to_dict(record: User) -> dict: "strava_state": record.strava_state, "strava_token": record.strava_token, "strava_refresh_token": record.strava_refresh_token, - "strava_token_expires_at": record.strava_token_expires_at, + "strava_token_expires_at": record.strava_token_expires_at.strftime( + "%Y-%m-%dT%H:%M:%S" + ) + if record.strava_token_expires_at + else None, } @@ -201,7 +214,7 @@ async def read_users_all( user_records_dict = [user_record_to_dict(record) for record in user_records] # Include metadata in the response - metadata = {"total_records": len(user_records)} + metadata = {"total_records": len(user_records), "api_version": API_VERSION} # Return the queried values using JSONResponse return JSONResponse( @@ -249,7 +262,7 @@ async def read_users_number( user_count = db_session.query(User).count() # Include metadata in the response - metadata = {"total_records": 1} + metadata = {"total_records": 1, "api_version": API_VERSION} # Return the queried values using JSONResponse return JSONResponse(content={"metadata": metadata, "content": user_count}) @@ -312,7 +325,12 @@ async def read_users_all_pagination( user_records_dict = [user_record_to_dict(record) for record in user_records] # Include metadata in the response - metadata = {"total_records": len(user_records)} + metadata = { + "total_records": len(user_records), + "pageNumber": pageNumber, + "numRecords": numRecords, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( @@ -373,7 +391,11 @@ async def read_users_username( user_records_dict = [user_record_to_dict(record) for record in user_records] # Include metadata in the response - metadata = {"total_records": len(user_records)} + metadata = { + "total_records": len(user_records), + "username": username, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( @@ -424,7 +446,11 @@ async def read_users_id( user_records_dict = [user_record_to_dict(record) for record in user_records] # Include metadata in the response - metadata = {"total_records": len(user_records)} + metadata = { + "total_records": len(user_records), + "user_id": user_id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( @@ -479,12 +505,14 @@ async def read_users_username_id( ) # Include metadata in the response - metadata = {"total_records": 1} + metadata = { + "total_records": 1, + "username": username, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": {"id": user_id}} - ) + return JSONResponse(content={"metadata": metadata, "content": {"id": user_id}}) except JWTError: # Return an error response if the user is not authenticated @@ -531,15 +559,24 @@ async def read_users_id_photo_path( if user: # Include metadata in the response - metadata = {"total_records": 1} + metadata = { + "total_records": 1, + "user_id": user_id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( - content={"metadata": metadata, "content": {"photo_path": user.photo_path}} + content={ + "metadata": metadata, + "content": {"photo_path": user.photo_path}, + } ) else: # Handle the case where the user was not found or doesn't have a photo path - return create_error_response("NOT_FOUND", "User not found or no photo path available", 404) + return create_error_response( + "NOT_FOUND", "User not found or no photo path available", 404 + ) except JWTError: # Return an error response if the user is not authenticated @@ -586,15 +623,24 @@ async def read_users_id_photo_path_aux( if user: # Include metadata in the response - metadata = {"total_records": 1} + metadata = { + "total_records": 1, + "user_id": user_id, + "api_version": API_VERSION, + } # Return the queried values using JSONResponse return JSONResponse( - content={"metadata": metadata, "content": {"photo_path_aux": user.photo_path_aux}} + content={ + "metadata": metadata, + "content": {"photo_path_aux": user.photo_path_aux}, + } ) else: # Handle the case where the user was not found or doesn't have a photo path aux - return create_error_response("NOT_FOUND", "User not found or no photo path aux available", 404) + return create_error_response( + "NOT_FOUND", "User not found or no photo path aux available", 404 + ) except JWTError: # Return an error response if the user is not authenticated @@ -742,7 +788,7 @@ async def edit_user( return JSONResponse( content={"message": "User edited successfully"}, status_code=200 ) - + except JWTError: # Return an error response if the user is not authenticated return create_error_response("UNAUTHORIZED", "Unauthorized", 401) @@ -801,9 +847,10 @@ async def delete_user_photo( # Return a JSONResponse indicating the success of the user edit return JSONResponse( - content={"message": f"Photo for user {user_id} has been deleted"}, status_code=200 + content={"message": f"Photo for user {user_id} has been deleted"}, + status_code=200, ) - + except JWTError: # Return an error response if the user is not authenticated return create_error_response("UNAUTHORIZED", "Unauthorized", 401) @@ -856,7 +903,9 @@ async def delete_user( count_gear = db_session.query(Gear).filter(Gear.user_id == user_id).count() if count_gear > 0: # Return an error response if the user has gear created - return create_error_response("CONFLIT", "Cannot delete user due to existing dependencies", 409) + return create_error_response( + "CONFLIT", "Cannot delete user due to existing dependencies", 409 + ) # Delete the user from the database db_session.delete(user) db_session.commit() @@ -865,7 +914,7 @@ async def delete_user( return JSONResponse( content={"message": f"User {user_id} has been deleted"}, status_code=200 ) - + except JWTError: # Return an error response if the user is not authenticated return create_error_response("UNAUTHORIZED", "Unauthorized", 401)