From 5e9773962c512321a834e66109441df8f80229ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Vit=C3=B3ria=20Silva?= Date: Mon, 5 Feb 2024 18:11:38 +0000 Subject: [PATCH 01/10] Backend revamp - [backend] Session functions started - [backend] User functions finished - [backend] Activities functions started --- backend/.env | 3 +- backend/__init__.py | 1 - backend/constants.py | 15 +- backend/controllers/__init__.py | 1 - backend/controllers/activityController.py | 1385 ----------------- .../controllers/activity_streamsController.py | 212 --- backend/controllers/followerController.py | 642 -------- backend/controllers/gearController.py | 691 -------- backend/controllers/sessionController.py | 664 -------- backend/controllers/stravaController.py | 685 -------- backend/controllers/userController.py | 927 ----------- backend/crud/access_tokens.py | 133 ++ backend/crud/activities.py | 151 ++ backend/crud/user_integrations.py | 64 + backend/crud/users.py | 406 +++++ backend/database.py | 26 + backend/db/__init__.py | 1 - backend/db/createdb.sql | 163 -- backend/dependencies.py | 78 +- backend/logs/logging_config.ini | 52 - backend/main.py | 249 +-- backend/{db/db.py => models.py} | 199 +-- backend/{logs => routers}/__init__.py | 0 backend/routers/activities.py | 343 ++++ backend/routers/session.py | 114 ++ backend/routers/users.py | 317 ++++ backend/schemas/__init__.py | 0 backend/schemas/access_tokens.py | 201 +++ backend/schemas/activities.py | 48 + backend/schemas/user_integrations.py | 14 + backend/schemas/users.py | 32 + frontend/activities/activity.php | 8 +- frontend/gear/gear.php | 12 +- .../api_logo_cptblWith_strava_stack_light.svg | 1 + .../strava/btn_strava_connectwith_orange.png | Bin 5768 -> 3509 bytes frontend/inc/Template-Bottom.php | 29 +- frontend/inc/Template-Top.php | 3 - frontend/inc/func/activities-funcs.php | 421 +---- .../inc/func/activities-streams-funcs.php | 16 + frontend/inc/func/followers-funcs.php | 22 +- frontend/inc/func/main-api-funcs.php | 55 +- frontend/inc/func/session-funcs.php | 8 +- frontend/inc/func/strava-funcs.php | 27 +- frontend/inc/func/users-funcs.php | 60 +- frontend/index.php | 393 +++-- frontend/lang/gear/gear/en.php | 1 + frontend/lang/login/en.php | 5 +- frontend/lang/settings/en.php | 231 +-- .../settings/inc/integration-settings/en.php | 17 + .../lang/settings/inc/profile-settings/en.php | 50 + .../settings/inc/security-settings/en.php | 24 + .../lang/settings/inc/users-settings/en.php | 86 + frontend/login.php | 104 +- .../settings/inc/integration-settings.php | 97 ++ frontend/settings/inc/profile-settings.php | 294 ++++ frontend/settings/inc/security-settings.php | 102 ++ frontend/settings/inc/users-settings.php | 655 ++++++++ frontend/settings/settings.php | 928 +---------- frontend/users/user.php | 2 +- requirements.txt | 7 +- 60 files changed, 3788 insertions(+), 7687 deletions(-) delete mode 100644 backend/controllers/__init__.py delete mode 100644 backend/controllers/activityController.py delete mode 100644 backend/controllers/activity_streamsController.py delete mode 100644 backend/controllers/followerController.py delete mode 100644 backend/controllers/gearController.py delete mode 100644 backend/controllers/sessionController.py delete mode 100644 backend/controllers/stravaController.py delete mode 100644 backend/controllers/userController.py create mode 100644 backend/crud/access_tokens.py create mode 100644 backend/crud/activities.py create mode 100644 backend/crud/user_integrations.py create mode 100644 backend/crud/users.py create mode 100644 backend/database.py delete mode 100644 backend/db/__init__.py delete mode 100644 backend/db/createdb.sql delete mode 100644 backend/logs/logging_config.ini rename backend/{db/db.py => models.py} (66%) rename backend/{logs => routers}/__init__.py (100%) create mode 100644 backend/routers/activities.py create mode 100644 backend/routers/session.py create mode 100644 backend/routers/users.py create mode 100644 backend/schemas/__init__.py create mode 100644 backend/schemas/access_tokens.py create mode 100644 backend/schemas/activities.py create mode 100644 backend/schemas/user_integrations.py create mode 100644 backend/schemas/users.py create mode 100644 frontend/img/strava/api_logo_cptblWith_strava_stack_light.svg create mode 100644 frontend/lang/settings/inc/integration-settings/en.php create mode 100644 frontend/lang/settings/inc/profile-settings/en.php create mode 100644 frontend/lang/settings/inc/security-settings/en.php create mode 100644 frontend/lang/settings/inc/users-settings/en.php create mode 100644 frontend/settings/inc/integration-settings.php create mode 100644 frontend/settings/inc/profile-settings.php create mode 100644 frontend/settings/inc/security-settings.php create mode 100644 frontend/settings/inc/users-settings.php diff --git a/backend/.env b/backend/.env index 3a20764c..55f810b7 100644 --- a/backend/.env +++ b/backend/.env @@ -15,4 +15,5 @@ JAEGER_PROTOCOL=http JAEGER_HOST=jaeger JAGGER_PORT=4317 STRAVA_DAYS_ACTIVITIES_ONLINK=30 -API_ENDPOINT=changeme \ No newline at end of file +API_ENDPOINT=changeme +GEOCODES_MAPS_API=changeme \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py index 8b137891..e69de29b 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -1 +0,0 @@ - diff --git a/backend/constants.py b/backend/constants.py index 171e4dd3..6022ac8f 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -1,6 +1,17 @@ +import os + # Constant related to version -API_VERSION="v0.1.3" +API_VERSION="v0.1.4" + +# JWT Token constants +JWT_ALGORITHM = os.environ.get("ALGORITHM") +JWT_EXPIRATION_IN_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES")) +JWT_SECRET_KEY = os.environ.get("SECRET_KEY") # Constants related to user access types ADMIN_ACCESS = 2 -REGULAR_ACCESS = 1 \ No newline at end of file +REGULAR_ACCESS = 1 + +# Constants related to user active status +USER_ACTIVE = 1 +USER_NOT_ACTIVE = 2 \ No newline at end of file diff --git a/backend/controllers/__init__.py b/backend/controllers/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/backend/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/controllers/activityController.py b/backend/controllers/activityController.py deleted file mode 100644 index 0a9a83b5..00000000 --- a/backend/controllers/activityController.py +++ /dev/null @@ -1,1385 +0,0 @@ -""" -API Router for managing user activity information. - -This module defines FastAPI routes for performing CRUD operations on user activity records. -It includes endpoints for retrieving, creating, updating, and deleting activity records. -The routes handle user authentication, database interactions using SQLAlchemy, -and provide JSON responses with appropriate metadata. - -Endpoints: -- GET /activities/all: Retrieve all user activities. -- GET /activities/useractivities: Retrieve activities for the authenticated user. -- GET /activities/useractivities/{user_id}/week/{week_number}: Retrieve activities for a user in a specific week. -- GET /activities/useractivities/{user_id}/thisweek/distances: Retrieve distances for a user's activities in the current week. -- GET /activities/useractivities/{user_id}/thismonth/distances: Retrieve distances for a user's activities in the current month. -- GET /activities/useractivities/{user_id}/thismonth/number: Retrieve the count of activities for a user in the current month. -- GET /activities/gear/{gearID}: Retrieve activities associated with a specific gear for the authenticated user. -- GET /activities/all/number: Retrieve the total count of all activities. -- GET /activities/useractivities/number: Retrieve the total count of activities for the authenticated user. -- GET /activities/followeduseractivities/number: Retrieve the count of activities for followed users. -- GET /activities/all/pagenumber/{pageNumber}/numRecords/{numRecords}: Retrieve paginated activities for all users. -- GET /activities/useractivities/pagenumber/{pageNumber}/numRecords/{numRecords}: Retrieve paginated activities for the authenticated user. -- GET /activities/followeduseractivities/pagenumber/{pageNumber}/numRecords/{numRecords}: Retrieve paginated activities for followed users. -- GET /activities/{id}: Retrieve details of a specific activity. -- PUT /activities/{activity_id}/addgear/{gear_id}: Associate a gear with a specific activity. -- POST /activities/create: Create a new activity. -- PUT /activities/{activity_id}/deletegear: Disassociate a gear from a specific activity. -- DELETE /activities/{activity_id}/delete: Delete a specific activity. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. -- get_current_user: Dependency function to get the current authenticated user. - -Models: -- CreateActivityRequest: Pydantic model for creating activity records. - -Functions: -- activity_record_to_dict: Convert Activity SQLAlchemy objects to dictionaries. -- calculate_activity_distances: Calculate distances for different activity types. - -Logger: -- Logger named "myLogger" for logging errors and exceptions. - -""" -from operator import and_, or_ -import logging -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordBearer -from typing import List, Optional -from sqlalchemy import func, desc -from sqlalchemy.orm import joinedload -from db.db import Activity, Follower -from jose import JWTError -from pydantic import BaseModel -from datetime import datetime, timedelta -from . import sessionController -import calendar -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() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class ActivityBase(BaseModel): - """ - Pydantic model for representing activity attributes. - - Attributes: - - distance (int): The distance covered in the activity. - - name (str): The name of the activity. - - activity_type (str): The type of activity (e.g., running, cycling). - - start_time (str): The start time of the activity in ISO 8601 format. - - end_time (str): The end time of the activity in ISO 8601 format. - - city (Optional[str]): The city where the activity took place (optional). - - town (Optional[str]): The town where the activity took place (optional). - - country (Optional[str]): The country where the activity took place (optional). - - elevation_gain (int): The elevation gain during the activity. - - elevation_loss (int): The elevation loss during the activity. - - pace (float): The pace of the activity. - - average_speed (float): The average speed during the activity. - - average_power (int): The average power during the activity. - - strava_activity_id (Optional[int]): The ID of the activity on Strava (optional). - """ - - distance: int - name: str - activity_type: str - start_time: str - end_time: str - city: Optional[str] - town: Optional[str] - country: Optional[str] - elevation_gain: int - elevation_loss: int - pace: float - average_speed: float - average_power: int - strava_gear_id: Optional[int] - strava_activity_id: Optional[int] - - -class ActivityCreateRequest(ActivityBase): - """ - Pydantic model for creating activity records. - - Inherits from ActivityBase, which defines the base attributes for activity. - - This class extends the ActivityBase Pydantic model and is specifically tailored for - creating new activity records. - """ - - pass - - -# Define a function to convert Activity SQLAlchemy objects to dictionaries -def activity_record_to_dict(record: Activity) -> dict: - """ - Converts an Activity SQLAlchemy object to a dictionary. - - Parameters: - - record (Activity): The SQLAlchemy object representing an activity record. - - Returns: - dict: A dictionary representation of the Activity object. - - This function is used to convert an SQLAlchemy Activity object into a dictionary format for easier serialization and response handling. - """ - return { - "id": record.id, - "user_id": record.user_id, - "name": record.name, - "distance": record.distance, - "activity_type": record.activity_type, - "start_time": record.start_time.strftime("%Y-%m-%dT%H:%M:%S"), - "end_time": record.end_time.strftime("%Y-%m-%dT%H:%M:%S"), - "city": record.city, - "town": record.town, - "country": record.country, - "created_at": record.created_at.strftime("%Y-%m-%dT%H:%M:%S"), - "elevation_gain": record.elevation_gain, - "elevation_loss": record.elevation_loss, - "pace": str(record.pace), - "average_speed": str(record.average_speed), - "average_power": record.average_power, - "visibility": record.visibility, - "gear_id": record.gear_id, - "strava_activity_id": record.strava_activity_id, - } - - -def calculate_activity_distances(activity_records: List[Activity]) -> dict: - """ - Calculates the total distances for different activity types. - - Parameters: - - activity_records (List[Activity]): A list of SQLAlchemy Activity objects representing different activities. - - Returns: - dict: A dictionary containing the total distances for different activity types. - The keys include 'run,' 'bike,' and 'swim,' each representing the summed distance for running, biking, and swimming activities, respectively. - - This function iterates through the given list of activity records and categorizes the distances based on the activity type. - The result is a dictionary with the summed distances for each activity type. - """ - - run = bike = swim = 0 - - for activity in activity_records: - if activity.activity_type in [1, 2, 3]: - run += activity.distance - elif activity.activity_type in [4, 5, 6, 7, 8]: - bike += activity.distance - elif activity.activity_type == 9: - swim += activity.distance - - return {"run": run, "bike": bike, "swim": swim} - - -@router.get("/activities/all", response_model=List[dict]) -async def read_activities_all( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Retrieve all activity records. - - Parameters: - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and activity records. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Query the activities records using SQLAlchemy and order by start time - activity_records = ( - db_session.query(Activity).order_by(desc(Activity.start_time)).all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = {"total_records": len(activity_records), "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/useractivities", response_model=List[dict]) -async def read_activities_useractivities( - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve all activities for a specific user. - - Parameters: - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user-specific activity records. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Query the activities records using SQLAlchemy - activity_records = ( - db_session.query(Activity) - .filter(Activity.user_id == user_id) - .order_by(desc(Activity.start_time)) - .all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = {"total_records": len(activity_records), "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_useractivities: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/useractivities/{user_id}/week/{week_number}") -async def read_activities_useractivities_thisweek_number( - user_id: int, - week_number: int, - logged_user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve activities for a specific user during a specified week. - - Parameters: - - user_id (int): The ID of the user for whom activities are being queried. - - week_number (int): The number of weeks in the past or future to retrieve activities for. - - logged_user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user-specific activity records for the specified week. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Calculate the start of the requested week - today = datetime.utcnow().date() - start_of_week = today - timedelta(days=(today.weekday() + 7 * week_number)) - end_of_week = start_of_week + timedelta(days=7) - - # Query the count of activities records for the requested week - if logged_user_id == user_id: - activity_records = ( - db_session.query(Activity) - .filter( - Activity.user_id == user_id, - func.date(Activity.start_time) >= start_of_week, - func.date(Activity.start_time) <= end_of_week, - ) - .order_by(desc(Activity.start_time)) - ).all() - else: - activity_records = ( - db_session.query(Activity) - .filter( - and_(Activity.user_id == user_id, Activity.visibility.in_([0, 1])), - func.date(Activity.start_time) >= start_of_week, - func.date(Activity.start_time) <= end_of_week, - ) - .order_by(desc(Activity.start_time)) - ).all() - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - 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( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_useractivities_thisweek_number: {err}", - exc_info=True, - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/useractivities/{user_id}/thisweek/distances") -async def read_activities_useractivities_thisweek_distances( - user_id=int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve distances covered in activities by a specific user during the current week. - - Parameters: - - user_id (int): The ID of the user for whom distances are being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing distances covered in activities for the specified user during the current week. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Calculate the start of the current week - today = datetime.utcnow().date() - start_of_week = today - timedelta( - days=today.weekday() - ) # Monday is the first day of the week, which is denoted by 0 - end_of_week = start_of_week + timedelta(days=7) - - # Query the activities records for the current week - activity_records = ( - db_session.query(Activity) - .filter( - Activity.user_id == user_id, - func.date(Activity.start_time) >= start_of_week, - func.date(Activity.start_time) < end_of_week, - ) - .order_by(desc(Activity.start_time)) - .all() - ) - - # Use the helper function to calculate distances - distances = calculate_activity_distances(activity_records) - - # Return the queried values using JSONResponse - # return JSONResponse(content=distances) - - # Include metadata in the response - 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}) - 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_activities_useractivities_thisweek_distances: {err}", - exc_info=True, - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/useractivities/{user_id}/thismonth/distances") -async def read_activities_useractivities_thismonth_distances( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve distances covered in activities by a specific user during the current month. - - Parameters: - - user_id (int): The ID of the user for whom distances are being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing distances covered in activities for the specified user during the current month. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Calculate the start of the current month - today = datetime.utcnow().date() - start_of_month = today.replace(day=1) - end_of_month = start_of_month.replace( - day=calendar.monthrange(today.year, today.month)[1] - ) - - # Query the activities records for the current month - activity_records = ( - db_session.query(Activity) - .filter( - Activity.user_id == user_id, - func.date(Activity.start_time) >= start_of_month, - func.date(Activity.start_time) <= end_of_month, - ) - .order_by(desc(Activity.start_time)) - .all() - ) - - # Use the helper function to calculate distances - distances = calculate_activity_distances(activity_records) - - # Return the queried values using JSONResponse - # return JSONResponse(content=distances) - - # Include metadata in the response - 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}) - 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_activities_useractivities_thismonth_distances: {err}", - exc_info=True, - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/useractivities/{user_id}/thismonth/number") -async def read_activities_useractivities_thismonth_number( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the number of activities for a specific user during the current month. - - Parameters: - - user_id (int): The ID of the user for whom the activity count is being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the number of activities for the specified user during the current month. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Calculate the start of the current month - today = datetime.utcnow().date() - start_of_month = today.replace(day=1) - end_of_month = start_of_month.replace( - day=calendar.monthrange(today.year, today.month)[1] - ) - - # Query the count of activities records for the current month - activity_count = ( - db_session.query(func.count(Activity.id)) - .filter( - Activity.user_id == user_id, - func.date(Activity.start_time) >= start_of_month, - func.date(Activity.start_time) <= end_of_month, - ) - .scalar() - ) - - # Include metadata in the response - 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}) - - 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_activities_useractivities_thismonth_number: {err}", - exc_info=True, - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/gear/{gear_id}", response_model=List[dict]) -async def read_activities_gearactivities( - gear_id=int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve activities associated with a specific gear. - - Parameters: - - gearID (int): The ID of the gear for which activities are being queried. - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and gear-specific activity records. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Query the activities records using SQLAlchemy - activity_records = ( - db_session.query(Activity) - .filter(Activity.user_id == user_id, Activity.gear_id == gear_id) - .order_by(desc(Activity.start_time)) - .all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = { - "total_records": len(activity_records), - "gear_id": gear_id, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_gearactivities: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/all/number") -async def read_activities_all_number( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Retrieve the total number of activities. - - Parameters: - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the total number of activities. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Query the number of activities records for the user using SQLAlchemy - activity_count = db_session.query(func.count(Activity.id)).scalar() - - # Include metadata in the response - metadata = {"total_records": 1, "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse(content={"metadata": metadata, "content": activity_count}) - - 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_activities_all_number: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/useractivities/number") -async def read_activities_useractivities_number( - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the number of activities for a specific user. - - Parameters: - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the number of activities for the specified user. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Query the number of activities records for the user using SQLAlchemy - activity_count = ( - db_session.query(func.count(Activity.id)) - .filter(Activity.user_id == user_id) - .scalar() - ) - - # Include metadata in the response - metadata = {"total_records": 1, "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse(content={"metadata": metadata, "content": activity_count}) - - 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_activities_useractivities_number: {err}", exc_info=True - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/activities/followeduseractivities/number") -async def read_activities_followed_useractivities_number( - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the number of activities for users followed by the authenticated user. - - Parameters: - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the number of activities for users followed by the authenticated user. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Query the number of activities records for followed users using SQLAlchemy - activity_count = ( - db_session.query(func.count(Activity.id)) - .join(Follower, Follower.following_id == Activity.user_id) - .filter( - and_( - Follower.follower_id == user_id, - Follower.is_accepted == True, - ), - Activity.visibility.in_([0, 1]), - ) - .scalar() - ) - - # Include metadata in the response - metadata = {"total_records": 1, "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse(content={"metadata": metadata, "content": activity_count}) - - 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_activities_followed_useractivities_number: {err}", - exc_info=True, - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get( - "/activities/all/pagenumber/{pageNumber}/numRecords/{numRecords}", - response_model=List[dict], -) -async def read_activities_all_pagination( - pageNumber: int, - numRecords: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve paginated activity records. - - Parameters: - - pageNumber (int): The page number to retrieve. - - numRecords (int): The number of records to retrieve per page. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and paginated activity records. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Use SQLAlchemy to query the gear records with pagination - activity_records = ( - db_session.query(Activity) - .order_by(desc(Activity.start_time)) - .offset((pageNumber - 1) * numRecords) - .limit(numRecords) - .all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = { - "total_records": len(activity_records), - "page_number": pageNumber, - "num_records": numRecords, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_all_pagination: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get( - "/activities/useractivities/pagenumber/{pageNumber}/numRecords/{numRecords}", - response_model=List[dict], -) -async def read_activities_useractivities_pagination( - pageNumber: int, - numRecords: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve paginated activity records for a specific user. - - Parameters: - - pageNumber (int): The page number to retrieve. - - numRecords (int): The number of records to retrieve per page. - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and paginated activity records for the specified user. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Use SQLAlchemy to query the gear records with pagination - activity_records = ( - db_session.query(Activity) - .filter(Activity.user_id == user_id) - .order_by(desc(Activity.start_time)) - .offset((pageNumber - 1) * numRecords) - .limit(numRecords) - .all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = { - "total_records": len(activity_records), - "page_number": pageNumber, - "num_records": numRecords, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_useractivities_pagination: {err}", exc_info=True - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get( - "/activities/followeduseractivities/pagenumber/{pageNumber}/numRecords/{numRecords}", - response_model=List[dict], -) -async def read_activities_followed_user_activities_pagination( - pageNumber: int, - numRecords: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve paginated activity records for users followed by the authenticated user. - - Parameters: - - pageNumber (int): The page number to retrieve. - - numRecords (int): The number of records to retrieve per page. - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and paginated activity records for users followed by the authenticated user. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Use SQLAlchemy to query activities of followed users with pagination - activity_records = ( - db_session.query(Activity) - .join(Follower, Follower.following_id == Activity.user_id) - .filter( - and_( - Follower.follower_id == user_id, - Follower.is_accepted == True, - ), - Activity.visibility.in_([0, 1]), - ) - .order_by(desc(Activity.start_time)) - .offset((pageNumber - 1) * numRecords) - .limit(numRecords) - .options(joinedload(Activity.user)) - .all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = { - "total_records": len(activity_records), - "page_number": pageNumber, - "num_records": numRecords, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_followed_user_activities_pagination: {err}", - exc_info=True, - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Get gear from id -@router.get("/activities/{id}", response_model=List[dict]) -async def read_activities_activityFromId( - id: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve activity records by ID. - - Parameters: - - id (int): The ID of the activity to retrieve. - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and activity records for the specified ID. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Use SQLAlchemy to query the gear record by ID - activity_record = ( - db_session.query(Activity) - .filter( - or_(Activity.user_id == user_id, Activity.visibility.in_([0, 1])), - Activity.id == id, - ) - .all() - ) - - # Convert the SQLAlchemy result to a list of dictionaries - if activity_record: - activity_records = activity_record - else: - activity_records = [] - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_records_dict = [ - activity_record_to_dict(record) for record in activity_records - ] - - # Include metadata in the response - metadata = { - "total_records": len(activity_records), - "id": id, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_records_dict} - ) - - 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_activities_activityFromId: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.put("/activities/{activity_id}/addgear/{gear_id}") -async def activity_add_gear( - activity_id: int, - gear_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Add gear to a specific activity. - - Parameters: - - activity_id (int): The ID of the activity to which gear is being added. - - gear_id (int): The ID of the gear to add to the activity. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success or failure of adding gear to the activity. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Query the database to find the activity by its ID - activity_record = db_session.query(Activity).get(activity_id) - - # Check if the activity with the given ID exists - if activity_record: - # Set the activity's gear ID to the given gear ID - activity_record.gear_id = gear_id - - # Commit the transaction - db_session.commit() - - # Return a success response - return JSONResponse( - content={"message": "Gear added to activity successfully"}, - status_code=200, - ) - else: - # Return a 404 response if the activity with the given ID does not exist - return create_error_response("NOT_FOUND", "Activity not found", 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in activity_add_gear: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.post("/activities/create") -async def create_activity( - activity_data: ActivityCreateRequest, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Create a new activity. - - Parameters: - - activity_data (ActivityCreateRequest): Data for creating a new activity. - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success or failure of creating the activity. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Convert the 'starttime' string to a datetime - starttime = parse_timestamp(activity_data.start_time) - # Convert the 'endtime' string to a datetime - endtime = parse_timestamp(activity_data.end_time) - - auxType = 10 # Default value - type_mapping = { - "Run": 1, - "running": 1, - "trail running": 2, - "TrailRun": 2, - "VirtualRun": 3, - "cycling": 4, - "Ride": 4, - "GravelRide": 5, - "EBikeRide": 6, - "VirtualRide": 7, - "virtual_ride": 7, - "swimming": 8, - "open_water_swimming": 8, - "Walk": 9, - } - # "AlpineSki", - # "BackcountrySki", - # "Badminton", - # "Canoeing", - # "Crossfit", - # "EBikeRide", - # "Elliptical", - # "EMountainBikeRide", - # "Golf", - # "GravelRide", - # "Handcycle", - # "HighIntensityIntervalTraining", - # "Hike", - # "IceSkate", - # "InlineSkate", - # "Kayaking", - # "Kitesurf", - # "MountainBikeRide", - # "NordicSki", - # "Pickleball", - # "Pilates", - # "Racquetball", - # "Ride", - # "RockClimbing", - # "RollerSki", - # "Rowing", - # "Run", - # "Sail", - # "Skateboard", - # "Snowboard", - # "Snowshoe", - # "Soccer", - # "Squash", - # "StairStepper", - # "StandUpPaddling", - # "Surfing", - # "Swim", - # "TableTennis", - # "Tennis", - # "TrailRun", - # "Velomobile", - # "VirtualRide", - # "VirtualRow", - # "VirtualRun", - # "Walk", - # "WeightTraining", - # "Wheelchair", - # "Windsurf", - # "Workout", - # "Yoga" - auxType = type_mapping.get(activity_data.activity_type, 10) - - # Create a new Activity record - activity = Activity( - user_id=user_id, - name=activity_data.name, - distance=activity_data.distance, - activity_type=auxType, - start_time=starttime, - end_time=endtime, - city=activity_data.city, - town=activity_data.town, - country=activity_data.country, - created_at=func.now(), # Use func.now() to set 'created_at' to the current timestamp - elevation_gain=activity_data.elevation_gain, - elevation_loss=activity_data.elevation_loss, - pace=activity_data.pace, - average_speed=activity_data.average_speed, - average_power=activity_data.average_power, - strava_gear_id=activity_data.strava_gear_id, - strava_activity_id=activity_data.strava_activity_id, - ) - - # Store the Activity record in the database - db_session.add(activity) - db_session.commit() - db_session.refresh(activity) - - # Return a JSONResponse indicating the success of the activity creation - return JSONResponse( - content={"message": "Activity created successfully", "activity_id": activity.id}, - status_code=201, - ) - - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in create_activity: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def parse_timestamp(timestamp_string): - """ - Parse a timestamp string into a datetime object. - - Parameters: - - timestamp_string (str): The timestamp string to be parsed. - - Returns: - - datetime: The parsed datetime object. - """ - try: - # Try to parse with milliseconds - return datetime.strptime(timestamp_string, "%Y-%m-%dT%H:%M:%S.%fZ") - except ValueError: - # If milliseconds are not present, use a default value of 0 - return datetime.strptime(timestamp_string, "%Y-%m-%dT%H:%M:%SZ") - - -# Define an HTTP PUT route to delete an activity gear -@router.put("/activities/{activity_id}/deletegear") -async def delete_activity_gear( - activity_id: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Delete gear associated with a specific activity. - - Parameters: - - activity_id (int): The ID of the activity from which gear is being deleted. - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success or failure of deleting gear from the activity. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Query the database to find the user by their ID - activity = ( - db_session.query(Activity) - .filter(Activity.id == activity_id, Activity.user_id == user_id) - .first() - ) - - # Check if the user with the given ID exists - if not activity: - # Return a 404 response if the user with the given ID does not exist - return create_error_response("NOT_FOUND", "Activity not found", 404) - - # Set the user's photo paths to None to delete the photo - activity.gear_id = None - - # Commit the changes to the database - db_session.commit() - - # Return a success response - return JSONResponse( - content={"message": "Gear deleted from activity successfully"}, - status_code=200, - ) - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in delete_activity_gear: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.delete("/activities/{activity_id}/delete") -async def delete_activity( - activity_id: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Delete a specific activity. - - Parameters: - - activity_id (int): The ID of the activity to be deleted. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success or failure of deleting the activity. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Use SQLAlchemy to query and delete the gear record - activity_record = ( - db_session.query(Activity) - .filter(Activity.id == activity_id, Activity.user_id == user_id) - .first() - ) - - if activity_record: - # Delete the gear record - db_session.delete(activity_record) - - # Commit the transaction - db_session.commit() - - # Return a success response - return JSONResponse( - content={"message": f"Activity {activity_id} has been deleted"}, - status_code=200, - ) - else: - # Return a 404 response if the gear with the given ID does not exist - return create_error_response("NOT_FOUND", "Activity not found", 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in delete_activity: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) diff --git a/backend/controllers/activity_streamsController.py b/backend/controllers/activity_streamsController.py deleted file mode 100644 index 7fcde7de..00000000 --- a/backend/controllers/activity_streamsController.py +++ /dev/null @@ -1,212 +0,0 @@ -""" -API Router for managing activity stream information. - -This module defines FastAPI routes for performing CRUD operations on activity stream records. -It includes endpoints for retrieving and creating activity stream records. -The routes handle user authentication, database interactions using SQLAlchemy, -and provide JSON responses with appropriate metadata. - -Endpoints: -- GET /activities/streams/activity_id/{activity_id}/all: Retrieve all activity streams for a specific activity. -- POST /activities/streams/create: Create a new activity stream record. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. - -Models: -- ActivityStreamBase: Pydantic model for representing activity stream attributes. -- ActivityStreamCreateRequest: Pydantic model for creating activity stream records. - -Functions: -- activity_streams_records_to_dict: Convert ActivityStreams SQLAlchemy objects to dictionaries. - -Logger: -- Logger named "myLogger" for logging errors and exceptions. - -Routes: -- read_activities_streams_for_activity_all: Retrieve all activity streams for a specific activity. -- create_activity_stream: Create a new activity stream record. -""" -import logging -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordBearer -from typing import List, Optional -from db.db import ActivityStreams -from jose import JWTError -from pydantic import BaseModel -from . import sessionController -from dependencies import get_db_session, create_error_response -from sqlalchemy.orm import Session -from fastapi.responses import JSONResponse -from constants import API_VERSION - -# Define the API router -router = APIRouter() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class ActivityStreamBase(BaseModel): - """ - Pydantic model for representing activity attributes. - - Attributes: - - activity_id (int): The activity this activity stream belongs. - - stream_type (str): The stream type. - - stream_waypoints (List[dict]): List of waypoints for the activity stream, typically contains datetime and specify stream like HR, Power, etc. - - strava_activity_stream_id (Optional[int]): The ID of the activity stream on Strava (optional). - """ - activity_id: int - stream_type: str - stream_waypoints: List[dict] - strava_activity_stream_id: Optional[int] - - -class ActivityStreamCreateRequest(ActivityStreamBase): - """ - Pydantic model for creating activity stream records. - - Inherits from ActivityStreamBase, which defines the base attributes for activity stream. - - This class extends the ActivityStreamBase Pydantic model and is specifically tailored for - creating new activity stream records. - """ - pass - - -# Define a function to convert Activity SQLAlchemy objects to dictionaries -def activity_streams_records_to_dict(record: ActivityStreams) -> dict: - """ - Converts an ActivityStreams SQLAlchemy object to a dictionary. - - Parameters: - - record (ActivityStreams): The SQLAlchemy object representing an activity stream record. - - Returns: - dict: A dictionary representation of the ActivityStreams object. - - This function is used to convert an SQLAlchemy ActivityStreams object into a dictionary format for easier serialization and response handling. - """ - return { - "id": record.id, - "activity_id": record.activity_id, - "stream_type": record.stream_type, - "stream_waypoints": record.stream_waypoints, - "strava_activity_stream_id": record.strava_activity_stream_id, - } - - -@router.get("/activities/streams/activity_id/{activity_id}/all", response_model=List[dict]) -async def read_activities_streams_for_activity_all( - activity_id=int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve all activity streams for a specific activity. - - Parameters: - - activity_id (int): The ID of the activity for which to retrieve streams. - - token (str): OAuth2 bearer token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and activity stream records. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Validate the token - sessionController.validate_token(db_session, token) - - # Query the activities streams records using SQLAlchemy - activity_streams_records = ( - db_session.query(ActivityStreams) - .filter(ActivityStreams.activity_id == activity_id) - .all() - ) - - # Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries - activity_streams_records_dict = [ - activity_streams_records_to_dict(record) - for record in activity_streams_records - ] - - # Include metadata in the response - metadata = { - "total_records": len(activity_streams_records), - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": activity_streams_records_dict} - ) - - 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_activities_streams_for_activity_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - -@router.post("/activities/streams/create") -async def create_activity_stream( - activity_stream_data: ActivityStreamCreateRequest, - db_session: Session = Depends(get_db_session), -): - """ - Create a new activity stream record. - - Parameters: - - activity_stream_data (ActivityStreamCreateRequest): Pydantic model representing the data for creating a new activity stream. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of the activity stream creation. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Create a new Activity record - activity_stream = ActivityStreams( - activity_id=activity_stream_data.activity_id, - stream_type=activity_stream_data.stream_type, - stream_waypoints=activity_stream_data.stream_waypoints, - strava_activity_stream_id=activity_stream_data.strava_activity_stream_id, - ) - - # Store the Activity record in the database - db_session.add(activity_stream) - db_session.commit() - db_session.refresh(activity_stream) - - # Return a JSONResponse indicating the success of the activity creation - return JSONResponse( - content={"message": "Activity stream created successfully"}, - status_code=201, - ) - - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in create_activity_stream: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) \ No newline at end of file diff --git a/backend/controllers/followerController.py b/backend/controllers/followerController.py deleted file mode 100644 index 9e29056b..00000000 --- a/backend/controllers/followerController.py +++ /dev/null @@ -1,642 +0,0 @@ -""" -API Router for managing user followers information. - -This module defines FastAPI routes for handling user follower relationships. -It includes endpoints for retrieving follower information, creating, accepting, -and deleting follower relationships. The routes handle user authentication, -database interactions using SQLAlchemy, and provide JSON responses with appropriate metadata. - -Endpoints: -- GET /followers/user/{user_id}/targetUser/{target_user_id}: Retrieve specific follower relationship details. -- GET /followers/user/{user_id}/followers/count/all: Retrieve the total number of followers for a user. -- GET /followers/user/{user_id}/followers/count: Retrieve the count of accepted followers for a user. -- GET /followers/user/{user_id}/followers/all: Retrieve all followers for a user. -- GET /followers/user/{user_id}/following/count/all: Retrieve the total number of users a user is following. -- GET /followers/user/{user_id}/following/count: Retrieve the count of accepted users a user is following. -- GET /followers/user/{user_id}/following/all: Retrieve all users a user is following. -- PUT /followers/accept/user/{user_id}/targetUser/{target_user_id}: Accept a follow request. -- POST /followers/create/user/{user_id}/targetUser/{target_user_id}: Create a new follower relationship. -- DELETE /followers/delete/user/{user_id}/targetUser/{target_user_id}: Delete a follower relationship. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. - -Models: -- Follower: SQLAlchemy model for the follower relationship. -- JSONResponse: FastAPI response model for JSON content. - -Functions: -- validate_token: Function to validate user access tokens. -- create_error_response: Function to create a standardized error response. - -Logger: -- Logger named "myLogger" for logging errors and exceptions. - -""" -import logging -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordBearer -from . import sessionController -from jose import JWTError -from fastapi.responses import JSONResponse -from db.db import ( - Follower, -) -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() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -# Define an HTTP GET route to retrieve the number of users -@router.get("/followers/user/{user_id}/targetUser/{target_user_id}") -async def read_followers_user_specific_user( - user_id: int, - target_user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve specific follower relationship details. - - Parameters: - - user_id (int): The ID of the user whose followers are being queried. - - target_user_id (int): The ID of the target user in the relationship. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and follower relationship details. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query the specific follower record - follower = ( - db_session.query(Follower) - .filter( - (Follower.follower_id == user_id) - & (Follower.following_id == target_user_id) - ) - .first() - ) - - if follower: - # Include metadata in the response - 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 = { - "follower_id": user_id, - "following_id": target_user_id, - "is_accepted": follower.is_accepted, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": response_data} - ) - - # Users are not following each other - 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 - ) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/followers/user/{user_id}/followers/count/all") -async def get_user_follower_count_all( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the total number of followers for a user. - - Parameters: - - user_id (int): The ID of the user whose follower count is being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the total follower count. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query the specific follower record and retrieve count - follower_count = ( - db_session.query(Follower).filter(Follower.follower_id == user_id).count() - ) - - # Include metadata in the response - 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}) - - 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 get_user_follower_count_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/followers/user/{user_id}/followers/count") -async def get_user_follower_count( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the count of accepted followers for a user. - - Parameters: - - user_id (int): The ID of the user whose accepted follower count is being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the count of accepted followers. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query for the count of entries where user_id is equal to Follower.follower_id - follower_count = ( - db_session.query(Follower) - .filter((Follower.follower_id == user_id) & (Follower.is_accepted == True)) - .count() - ) - - # Include metadata in the response - 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}) - - 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 get_user_follower_count: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/followers/user/{user_id}/followers/all") -async def get_user_follower_all( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve all followers for a user. - - Parameters: - - user_id (int): The ID of the user whose followers are being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and a list of follower details. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query for the entries where both conditions are met - followers = ( - db_session.query(Follower).filter(Follower.following_id == user_id).all() - ) - - # Convert the query result to a list of dictionaries - followers_list = [ - {"follower_id": follower.follower_id, "is_accepted": follower.is_accepted} - for follower in followers - ] - - # Include metadata in the response - 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}) - - 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 get_user_follower_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/followers/user/{user_id}/following/count/all") -async def get_user_following_count_all( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the total number of users a user is following. - - Parameters: - - user_id (int): The ID of the user whose following count is being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the total following count. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query the specific follower record and retrieve count - following_count = ( - db_session.query(Follower).filter(Follower.following_id == user_id).count() - ) - - # Include metadata in the response - 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}) - - 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 get_user_following_count_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/followers/user/{user_id}/following/count") -async def get_user_following_count( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the count of accepted users a user is following. - - Parameters: - - user_id (int): The ID of the user whose accepted following count is being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the count of accepted following relationships. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query for the count of entries where user_id is equal to Follower.follower_id - following_count = ( - db_session.query(Follower) - .filter((Follower.following_id == user_id) & (Follower.is_accepted == True)) - .count() - ) - - # Include metadata in the response - 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}) - - 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 get_user_following_count: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/followers/user/{user_id}/following/all") -async def get_user_following_all( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve all users a user is following. - - Parameters: - - user_id (int): The ID of the user whose following relationships are being queried. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and a list of following details. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query for the entries where both conditions are met - followings = ( - db_session.query(Follower).filter(Follower.follower_id == user_id).all() - ) - - # Convert the query result to a list of dictionaries - following_list = [ - { - "following_id": following.following_id, - "is_accepted": following.is_accepted, - } - for following in followings - ] - - # Include metadata in the response - 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}) - - 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 get_user_following_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.put("/followers/accept/user/{user_id}/targetUser/{target_user_id}") -async def accept_follow( - user_id: int, - target_user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Accept a follow request. - - Parameters: - - user_id (int): The ID of the user who will accept the follow request. - - target_user_id (int): The ID of the user who sent the follow request. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing details of the accepted follower relationship. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Check if the follow relationship exists and is not accepted yet - follow_request = ( - db_session.query(Follower) - .filter( - (Follower.follower_id == target_user_id) - & (Follower.following_id == user_id) - & (Follower.is_accepted == False) - ) - .first() - ) - - if follow_request: - # Accept the follow request by changing the "is_accepted" column to True - follow_request.is_accepted = True - db_session.commit() - - # Return success response - response_data = { - "follower_id": target_user_id, - "following_id": user_id, - "is_accepted": True, - } - return JSONResponse(content=response_data, status_code=200) - else: - # Follow request does not exist or has already been accepted - return create_error_response("BAD_REQUEST", "Invalid follow request.", 400) - - 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 accept_follow: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.post("/followers/create/user/{user_id}/targetUser/{target_user_id}") -async def create_follow( - user_id: int, - target_user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Create a new follower relationship. - - Parameters: - - user_id (int): The ID of the user who will follow another user. - - target_user_id (int): The ID of the user to be followed. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing details of the newly created follower relationship. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Check if the follow relationship already exists - existing_follow = ( - db_session.query(Follower) - .filter( - (Follower.follower_id == user_id) - & (Follower.following_id == target_user_id) - ) - .first() - ) - - if existing_follow: - # Follow relationship already exists - return create_error_response( - "BAD_REQUEST", "Follow relationship already exists.", 400 - ) - - # Create a new follow relationship - new_follow = Follower( - follower_id=user_id, following_id=target_user_id, is_accepted=False - ) - - # Add the new follow relationship to the database - db_session.add(new_follow) - db_session.commit() - - # Return success response - response_data = { - "follower_id": user_id, - "following_id": target_user_id, - "is_accepted": False, - } - return JSONResponse(content=response_data, status_code=201) - - 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 create_follow: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.delete("/followers/delete/user/{user_id}/targetUser/{target_user_id}") -async def delete_follow( - user_id: int, - target_user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Delete a follower relationship. - - Parameters: - - user_id (int): The ID of the user who will unfollow another user. - - target_user_id (int): The ID of the user to be unfollowed. - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of the unfollow operation. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Query and delete the specific follower record - follower = ( - db_session.query(Follower) - .filter( - (Follower.follower_id == user_id) - & (Follower.following_id == target_user_id) - ) - .first() - ) - - if follower: - # Delete the follower record - db_session.delete(follower) - db_session.commit() - - # Respond with a success message - return JSONResponse( - content={"detail": "Follower record deleted successfully."}, - status_code=200, - ) - - # Follower record not found - return create_error_response("NOT_FOUND", "Follower record not found.", 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 delete_follow: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) diff --git a/backend/controllers/gearController.py b/backend/controllers/gearController.py deleted file mode 100644 index 2068990b..00000000 --- a/backend/controllers/gearController.py +++ /dev/null @@ -1,691 +0,0 @@ -""" -API Router for managing user gear information. - -This module defines FastAPI routes for performing CRUD operations on user gear records. -It includes endpoints for retrieving, creating, updating, and deleting gear records. -The routes handle user authentication, database interactions using SQLAlchemy, -and provide JSON responses with appropriate metadata. - -Endpoints: -- GET /gear/all: Retrieve all user gear records. -- GET /gear/all/{gear_type}: Retrieve user gear records filtered by gear type. -- GET /gear/number: Retrieve the total number of user gear records. -- GET /gear/all/pagenumber/{pageNumber}/numRecords/{numRecords}: Retrieve user gear records with pagination. -- GET /gear/{nickname}/gearfromnickname: Retrieve user gear records by nickname. -- GET /gear/{id}/gearfromid: Retrieve user gear records by ID. -- POST /gear/create: Create a new user gear record. -- PUT /gear/{gear_id}/edit: Edit an existing user gear record. -- DELETE /gear/{gear_id}/delete: Delete an existing user gear record. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. -- get_current_user: Dependency function to get the current authenticated user. - -Models: -- GearBase: Base Pydantic model for gear attributes. -- GearCreateRequest: Pydantic model for creating gear records. -- GearEditRequest: Pydantic model for editing gear records. - -Functions: -- gear_record_to_dict: Convert Gear SQLAlchemy objects to dictionaries. - -Logger: -- Logger named "myLogger" for logging errors and exceptions. - -""" -import logging -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordBearer -from typing import List, Optional -from sqlalchemy import func -from sqlalchemy.orm import Session -from db.db import Gear -from jose import JWTError -from urllib.parse import unquote -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() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class GearBase(BaseModel): - """ - Base Pydantic model for representing gear attributes. - - Attributes: - - brand (str, optional): The brand of the gear. - - model (str, optional): The model of the gear. - - nickname (str): The nickname of the gear. - - gear_type (int): The type of gear. - - date (str): The creation date of the gear. - """ - - brand: Optional[str] - model: Optional[str] - nickname: str - gear_type: int - date: str - - -class GearCreateRequest(GearBase): - """ - Pydantic model for creating gear records. - - Inherits from GearBase, which defines the base attributes for gear. - - This class extends the GearBase Pydantic model and is specifically tailored for - creating new gear records. - """ - - pass - - -class GearEditRequest(GearBase): - """ - Pydantic model for editing gear records. - - Inherits from GearBase, which defines the base attributes for gear. - - This class extends the GearBase Pydantic model and is designed for editing existing - gear records. Includes an additional attribute 'is_active' - to indicate whether the gear is active or not. - - """ - - is_active: int - - -# Define a function to convert Gear SQLAlchemy objects to dictionaries -def gear_record_to_dict(record: Gear) -> dict: - """ - Convert Gear SQLAlchemy objects to dictionaries. - - Parameters: - - record (Gear): The Gear SQLAlchemy object to convert. - - Returns: - - dict: A dictionary representation of the Gear object. - - This function is used to convert an SQLAlchemy Gear object into a dictionary format for easier serialization and response handling. - """ - return { - "id": record.id, - "brand": record.brand, - "model": record.model, - "nickname": record.nickname, - "gear_type": record.gear_type, - "user_id": record.user_id, - "created_at": record.created_at.strftime("%Y-%m-%dT%H:%M:%S"), - "is_active": record.is_active, - } - - -@router.get("/gear/all", response_model=List[dict]) -async def read_gear_all( - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve all user gear records. - - Parameters: - - user_id (int): The ID of the authenticated user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user gear records. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - """ - try: - # Query the gear records using SQLAlchemy - gear_records = ( - db_session.query(Gear) - .filter(Gear.user_id == user_id) - .order_by(Gear.nickname) - .all() - ) - - # Use the gear_record_to_dict function to convert SQLAlchemy objects to dictionaries - gear_records_dict = [gear_record_to_dict(record) for record in gear_records] - - # Include metadata in the response - metadata = {"total_records": len(gear_records), "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": gear_records_dict} - ) - - 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_gear_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/gear/all/{gear_type}", response_model=List[dict]) -async def read_gear_all_by_type( - gear_type: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user gear records filtered by gear type. - - Parameters: - - gear_type (int): The type of gear to filter by. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response containing metadata and user gear records filtered by gear type. - - Raises: - - ValueError: If the gear type is invalid. - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function queries user gear records from the database based on the specified gear type. - It filters records by both gear type and user ID, includes metadata in the response, - and returns a JSONResponse with the filtered gear records. - - """ - try: - # 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, - ) - - # Query the gear records using SQLAlchemy and filter by gear type and user ID - gear_records = ( - db_session.query(Gear) - .filter(Gear.gear_type == gear_type, Gear.user_id == user_id) - .order_by(Gear.nickname) - .all() - ) - - # Use the gear_record_to_dict function to convert SQLAlchemy objects to dictionaries - gear_records_dict = [gear_record_to_dict(record) for record in gear_records] - - # Include metadata in the response - metadata = { - "total_records": len(gear_records), - "gear_type": gear_type, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": gear_records_dict} - ) - - 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_gear_all_by_type: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/gear/number") -async def read_gear_number( - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve the total number of user gear records. - - Parameters: - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response containing metadata and the total number of user gear records. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function queries the total number of user gear records from the database, - includes metadata in the response, and returns a JSONResponse with the total gear count. - - """ - try: - # Query the number of gear records for the user using SQLAlchemy, filter by user ID and return the count - gear_count = ( - db_session.query(func.count(Gear.id)) - .filter(Gear.user_id == user_id) - .scalar() - ) - - # Include metadata in the response - metadata = {"total_records": 1, "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse(content={"metadata": metadata, "content": gear_count}) - - 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_gear_number: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get( - "/gear/all/pagenumber/{pageNumber}/numRecords/{numRecords}", - response_model=List[dict], - # tags=["Pagination"], -) -async def read_gear_all_pagination( - pageNumber: int, - numRecords: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user gear records with pagination. - - Parameters: - - pageNumber (int): The page number to retrieve. - - numRecords (int): The number of records to display per page. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response containing metadata and user gear records for the specified page. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function queries user gear records from the database with pagination, - includes metadata in the response, and returns a JSONResponse with the gear records for the specified page. - - """ - try: - # Use SQLAlchemy to query the gear records with pagination, filter by user ID, order by nickname ascending and return the records - gear_records = ( - db_session.query(Gear) - .filter(Gear.user_id == user_id) - .order_by(Gear.nickname.asc()) - .offset((pageNumber - 1) * numRecords) - .limit(numRecords) - .all() - ) - - # Use the gear_record_to_dict function to convert SQLAlchemy objects to dictionaries - gear_records_dict = [gear_record_to_dict(record) for record in gear_records] - - # Include metadata in the response - metadata = { - "total_records": len(gear_records), - "page_number": pageNumber, - "num_records": numRecords, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": gear_records_dict} - ) - - 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_gear_all_pagination: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/gear/nickname/{nickname}", response_model=List[dict]) -async def read_gear_nickname( - nickname: str, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user gear records by nickname. - - Parameters: - - nickname (str): The nickname to search for in user gear records. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response containing metadata and user gear records matching the provided nickname. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function queries user gear records from the database by nickname, - includes metadata in the response, and returns a JSONResponse with the matching gear records. - - """ - try: - # Define a search term - partial_nickname = unquote(nickname).replace("+", " ") - - # Use SQLAlchemy to query the gear records by nickname, filter by user ID and nickname, and return the records - gear_records = ( - db_session.query(Gear) - .filter( - Gear.nickname.like(f"%{partial_nickname}%"), Gear.user_id == user_id - ) - .all() - ) - - # Use the gear_record_to_dict function to convert SQLAlchemy objects to dictionaries - gear_records_dict = [gear_record_to_dict(record) for record in gear_records] - - # Include metadata in the response - metadata = { - "total_records": len(gear_records), - "nickname": nickname, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": gear_records_dict} - ) - - 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_gear_nickname: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Get gear from id -@router.get("/gear/id/{id}", response_model=List[dict]) -async def read_gear_id( - id: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user gear records by ID. - - Parameters: - - id (int): The ID of the gear record to retrieve. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response containing metadata and user gear records matching the provided ID. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function queries user gear records from the database by ID, - includes metadata in the response, and returns a JSONResponse with the matching gear records. - - """ - try: - # Use SQLAlchemy to query the gear record by ID and filter by user ID and gear ID - gear_records = ( - db_session.query(Gear).filter(Gear.id == id, Gear.user_id == user_id).all() - ) - - # Use the gear_record_to_dict function to convert SQLAlchemy objects to dictionaries - gear_records_dict = [gear_record_to_dict(record) for record in gear_records] - - # Include metadata in the response - metadata = { - "total_records": len(gear_records), - "id": id, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": gear_records_dict} - ) - - 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_gear_id: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.post("/gear/create") -async def create_gear( - gear: GearCreateRequest, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Create a new user gear record. - - Parameters: - - gear (GearCreateRequest): Pydantic model containing information for creating a new gear record. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response indicating the success of the gear creation. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function uses the provided GearCreateRequest model to create a new gear record in the database, - associates it with the authenticated user, and returns a JSONResponse indicating the success of the creation. - - """ - try: - # Use SQLAlchemy to create a new gear record - gear_record = Gear( - brand=unquote(gear.brand).replace("+", " "), - model=unquote(gear.model).replace("+", " "), - nickname=unquote(gear.nickname).replace("+", " "), - gear_type=gear.gear_type, - user_id=user_id, - created_at=gear.date, - is_active=True, - ) - - # Add the gear record to the database using SQLAlchemy - db_session.add(gear_record) - db_session.commit() - - # Return a JSONResponse indicating the success of the gear creation - return JSONResponse( - content={"message": "Gear created successfully"}, status_code=201 - ) - - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in create_gear: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.put("/gear/{gear_id}/edit") -async def edit_gear( - gear_id: int, - gear: GearEditRequest, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Edit an existing user gear record. - - Parameters: - - gear_id (int): The ID of the gear record to edit. - - gear (GearEditRequest): Pydantic model containing information for editing the gear record. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response indicating the success of the gear edit. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function uses the provided GearEditRequest model to edit an existing gear record in the database, - verifies ownership, and returns a JSONResponse indicating the success of the edit. - - """ - try: - # Use SQLAlchemy to query and update the gear record - gear_record = db_session.query(Gear).filter(Gear.id == gear_id).first() - - # Check if the gear record exists - if gear_record: - # Check if the gear record belongs to the user - if gear_record.user_id == user_id: - # Update the gear record - if gear.brand is not None: - gear_record.brand = unquote(gear.brand).replace("+", " ") - if gear.model is not None: - gear_record.model = unquote(gear.model).replace("+", " ") - gear_record.nickname = unquote(gear.nickname).replace("+", " ") - gear_record.gear_type = gear.gear_type - gear_record.created_at = gear.date - gear_record.is_active = gear.is_active - - # Commit the transaction - db_session.commit() - - # Return a JSONResponse indicating the success of the gear edit - return JSONResponse( - content={"message": "Gear edited successfully"}, status_code=200 - ) - 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, - ) - else: - # Return an error response if the gear record is not found - return create_error_response("NOT_FOUND", "Gear not found", 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in edit_gear: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.delete("/gear/{gear_id}/delete") -async def delete_gear( - gear_id: int, - user_id: int = Depends(get_current_user), - db_session: Session = Depends(get_db_session), -): - """ - Delete an existing user gear record. - - Parameters: - - gear_id (int): The ID of the gear record to delete. - - user_id (int, optional): The ID of the authenticated user (default: extracted from token). - - db_session (Session, optional): SQLAlchemy database session (default: obtained from dependency). - - Returns: - - JSONResponse: JSON response indicating the success of the gear deletion. - - Raises: - - JWTError: If the user is not authenticated. - - Exception: For other unexpected errors. - - This function deletes an existing gear record from the database, - verifies ownership, and returns a JSONResponse indicating the success of the deletion. - - """ - try: - # Use SQLAlchemy to query and delete the gear record - gear_record = db_session.query(Gear).filter(Gear.id == gear_id).first() - - # Check if the gear record exists - if gear_record: - # Check if the gear record belongs to the user - if gear_record.user_id == user_id: - # Delete the gear record - db_session.delete(gear_record) - - # Commit the transaction - db_session.commit() - - # Return a JSONResponse indicating the success of the gear deletion - return JSONResponse( - content={"message": f"Gear {gear_id} has been deleted"}, - status_code=200, - ) - 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, - ) - else: - # Return an error response if the gear record is not found - return create_error_response("NOT_FOUND", "Gear not found", 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in delete_gear: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) diff --git a/backend/controllers/sessionController.py b/backend/controllers/sessionController.py deleted file mode 100644 index 32fe9e1c..00000000 --- a/backend/controllers/sessionController.py +++ /dev/null @@ -1,664 +0,0 @@ -""" -Authentication and User Management Module - -This module defines FastAPI routes and functions for user authentication, access token management, -and CRUD operations on user records. It integrates with a relational database using SQLAlchemy -and provides endpoints for handling user login, token validation, user data retrieval, -and logout functionality. - -Endpoints: -- POST /token: Endpoint for user login to obtain an access token. -- GET /validate_token: Endpoint for validating the integrity and expiration of an access token. -- GET /users/me: Endpoint to retrieve user data based on the provided access token. -- DELETE /logout/{user_id}: Endpoint for user logout, revoking the associated access token. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. - -Models: -- TokenBase: Base Pydantic model for token attributes. -- CreateTokenRequest: Pydantic model for creating token records. - -Functions: -- authenticate_user: Function to authenticate a user and generate an access token. -- create_access_token: Function to create and store a new access token. -- remove_expired_tokens: Function to remove expired access tokens from the database. -- get_user_data: Function to retrieve user data based on the provided access token. -- validate_token: Function to validate the integrity and expiration of an access token. -- validate_admin_access: Function to validate if a user has admin access based on the token. - -Logger: -- Logger named "myLogger" for logging errors and exceptions. -""" -# OS module for interacting with the operating system -import os - -# Logging module for adding log statements to your code -import logging - -# FastAPI framework imports -from fastapi import APIRouter, Depends - -# Datetime module for working with date and time -from datetime import datetime, timedelta - -# JOSE (JavaScript Object Signing and Encryption) library for JWT (JSON Web Tokens) -from jose import jwt, JWTError - -# FastAPI security module for handling OAuth2 password bearer authentication -from fastapi.security import OAuth2PasswordBearer - -# SQLAlchemy module for working with relational databases -from sqlalchemy.orm import Session - -# Importing User and AccessToken models from the 'db' module -from db.db import User, AccessToken - -# Importing UserResponse model from the 'controllers.userController' module -from controllers.userController import UserResponse - -# Pydantic module for data validation and parsing -from pydantic import BaseModel - -# 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() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class TokenBase(BaseModel): - """ - Base Pydantic model for representing token attributes. - - Attributes: - - username (str): The username of the user. - - password (str): The user password in hash format. - - neverExpires (str): True or false value to set the token to expire. - """ - username: str - password: str - neverExpires: bool - -class CreateTokenRequest(TokenBase): - """ - Pydantic model for creating token records. - - Inherits from TokenBase, which defines the base attributes for token. - - This class extends the TokenBase Pydantic model and is specifically tailored for - creating new records. - """ - pass - - -def decode_token(token: str): - """ - Decode a JSON Web Token (JWT) and extract its payload. - - Parameters: - - token (str): The JWT string to be decoded. - - Returns: - - dict: A dictionary containing the decoded payload of the JWT. - - This function decodes a given JWT using the provided secret key and algorithm. It extracts and returns the payload - of the JWT, which typically includes information such as user ID, access type, and expiration time. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - """ - try: - payload = jwt.decode( - token, - os.environ.get("SECRET_KEY"), - algorithms=[os.environ.get("ALGORITHM")], - ) - return payload - except JWTError: - # Return an error response if the user is not authenticated - return ("UNAUTHORIZED", "Unauthorized", 401) - - -def get_user_id_from_token(token: str): - """ - Extract the user ID from a decoded JSON Web Token (JWT) payload. - - Parameters: - - token (str): The decoded JWT string. - - Returns: - - Union[int, Tuple[str, str, int]]: The user ID extracted from the JWT payload, - or a tuple representing an error response if the token is invalid. - - This function retrieves the user ID from the decoded payload of a JWT. It is used for - obtaining the user ID associated with a valid token during user authentication. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the extraction process. - """ - try: - return decode_token(token).get("id") - except JWTError: - # Return an error response if the user is not authenticated - return ("UNAUTHORIZED", "Unauthorized", 401) - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in get_user_id_from_token: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def get_exp_from_token(token: str = Depends(oauth2_scheme)): - """ - Extract the expiration time from a decoded JSON Web Token (JWT) payload. - - Parameters: - - token (str): The decoded JWT string. - - Returns: - - Union[int, Tuple[str, str, int]]: The expiration time (UNIX timestamp) extracted - from the JWT payload, or a tuple representing an error response if the token is invalid. - - This function retrieves the expiration time from the decoded payload of a JWT. - It is used to check the validity and expiration status of an access token. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the extraction process. - """ - try: - return decode_token(token).get("exp") - except JWTError: - # Return an error response if the user is not authenticated - return ("UNAUTHORIZED", "Unauthorized", 401) - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in get_exp_from_token: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def get_access_type_from_token(token: str = Depends(oauth2_scheme)): - """ - Extract the access type from a decoded JSON Web Token (JWT) payload. - - Parameters: - - token (str): The decoded JWT string. - - Returns: - - Union[int, Tuple[str, str, int]]: The access type extracted from the JWT payload, - or a tuple representing an error response if the token is invalid. - - This function retrieves the access type from the decoded payload of a JWT. - It is used to determine the level of access associated with a user's token. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the extraction process. - """ - try: - return decode_token(token).get("access_type") - except JWTError: - # Return an error response if the user is not authenticated - return ("UNAUTHORIZED", "Unauthorized", 401) - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in get_access_type_from_token: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -async def authenticate_user( - username: str, - password: str, - neverExpires: bool, - db_session: Session, -): - """ - Authenticate a user and generate an access token. - - Parameters: - - username (str): The username of the user attempting to authenticate. - - password (str): The password of the user attempting to authenticate. - - neverExpires (bool): Flag indicating whether the access token should never expire. - - db_session (Session): SQLAlchemy database session. - - Returns: - - Union[str, Tuple[str, str, int]]: The generated access token, - or a tuple representing an error response if authentication fails. - - This function verifies the user's credentials, checks for an existing access token, - and generates a new access token if necessary. The token's expiration is determined - based on the 'neverExpires' flag. - - Raises: - - Exception: If an unexpected error occurs during the authentication process. - """ - try: - # Use SQLAlchemy ORM to query the database - user = ( - db_session.query(User) - .filter(User.username == username, User.password == password) - .first() - ) - if not user: - return create_error_response( - "BAD_REQUEST", "Incorrect username or password", 400 - ) - - # Check if there is an existing access token for the user - access_token = ( - db_session.query(AccessToken).filter(AccessToken.user_id == user.id).first() - ) - if access_token: - return access_token.token - - # If there is no existing access token, create a new one - access_token_expires = timedelta( - minutes=int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES")) - ) - access_token = await create_access_token( - data={"id": user.id, "access_type": user.access_type}, - never_expire=neverExpires, - db_session=db_session, - expires_delta=access_token_expires, - ) - - return access_token - - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in authenticate_user: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -async def create_access_token( - data: dict, - never_expire: bool, - db_session: Session, - expires_delta: timedelta = None, -): - """ - Create and store a new access token. - - Parameters: - - data (dict): The payload data to be encoded in the access token. - - never_expire (bool): Flag indicating whether the access token should never expire. - - db_session (Session): SQLAlchemy database session. - - expires_delta (timedelta, optional): Duration until the access token expires. - - Returns: - - Union[str, Tuple[str, str, int]]: The generated access token, - or a tuple representing an error response if token creation fails. - - This function creates a new access token by encoding the provided payload data. - The token is then stored in the database, and the generated token string is returned. - - Raises: - - Exception: If an unexpected error occurs during the token creation process. - """ - try: - to_encode = data.copy() - if never_expire: - expire = datetime.utcnow() + timedelta(days=90) - elif expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode( - to_encode, - os.environ.get("SECRET_KEY"), - algorithm=os.environ.get("ALGORITHM"), - ) - - # Insert the access token into the database using SQLAlchemy - access_token = AccessToken( - token=encoded_jwt, - user_id=data["id"], - created_at=datetime.utcnow(), - expires_at=expire, - ) - - db_session.add(access_token) - db_session.commit() - - return encoded_jwt - except Exception as err: - # Log the error, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in create_access_token: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def remove_expired_tokens(db_session: Session): - """ - Remove expired access tokens from the database. - - Parameters: - - db_session (Session): SQLAlchemy database session. - - Returns: - - Union[None, Tuple[str, str, int]]: None on successful removal, - or a tuple representing an error response if removal fails. - - This function deletes access tokens from the database that have exceeded their expiration time. - It helps maintain the database's integrity by regularly purging expired access tokens. - - Raises: - - Exception: If an unexpected error occurs during the removal process. - """ - try: - # Calculate the expiration time - expiration_time = datetime.utcnow() - timedelta( - minutes=int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES")) - ) - - # Delete expired access tokens using SQLAlchemy ORM - rows_deleted = ( - db_session.query(AccessToken) - .filter(AccessToken.created_at < expiration_time) - .delete() - ) - db_session.commit() - - logger.info(f"{rows_deleted} access tokens deleted from the database") - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in remove_expired_tokens: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def get_user_data(db_session: Session, token: str = Depends(oauth2_scheme)): - """ - Retrieve user data based on the provided access token. - - Parameters: - - db_session (Session): SQLAlchemy database session. - - token (str): The access token for which user data is requested. - - Returns: - - Union[dict, Tuple[str, str, int]]: A dictionary containing user data, - or a tuple representing an error response if retrieval fails. - - This function fetches user data from the database using the provided access token. - It validates the token, retrieves the associated user ID, and returns the user's details - in a dictionary format. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the data retrieval process. - """ - try: - validate_token(db_session=db_session, token=token) - - user_id = get_user_id_from_token(token) - if user_id is None: - return create_error_response("UNAUTHORIZED", "Unauthorized", 401) - - # Retrieve the user details from the database using the user ID - user = db_session.query(User).filter(User.id == user_id).first() - - if not user: - return create_error_response("NOT_FOUND", "User not found", 404) - - if user.strava_token is None: - is_strava_linked = 0 - else: - is_strava_linked = 1 - - # Map the user object to a dictionary that matches the UserResponse model - user_data = { - "id": user.id, - "name": user.name, - "username": user.username, - "email": user.email, - "city": user.city, - "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, - "photo_path": user.photo_path, - "photo_path_aux": user.photo_path_aux, - "is_active": user.is_active, - "is_strava_linked": is_strava_linked, - } - - return user_data - except JWTError: - # Return an error response if the user is not authenticated - return ("UNAUTHORIZED", "Unauthorized", 401) - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in get_user_data: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def validate_token(db_session: Session, token: str): - """ - Validate the integrity and expiration of an access token. - - Parameters: - - db_session (Session): SQLAlchemy database session. - - token (str): The access token to be validated. - - Returns: - - Union[dict, Tuple[str, str, int]]: A dictionary with a success message if the token is valid, - or a tuple representing an error response if validation fails. - - This function checks the integrity and expiration of the provided access token. - It ensures that the token is associated with a valid user in the database and has not expired. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the validation process. - """ - try: - user_id = get_user_id_from_token(token) - - exp = get_exp_from_token(token) - - access_token = ( - db_session.query(AccessToken) - .filter(AccessToken.user_id == user_id, AccessToken.token == token) - .first() - ) - - 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"} - except JWTError as jwt_error: - raise jwt_error - except Exception as err: - logger.error(f"Error in token validation: {err}", exc_info=True) - raise JWTError("Token validation failed") - - -def validate_admin_access(token: str): - """ - Validate if the user associated with the provided token has administrative access. - - Parameters: - - token (str): The access token to be validated. - - Returns: - - Union[None, Tuple[str, str, int]]: None if the user has admin access, - or a tuple representing an error response if validation fails. - - This function checks if the user associated with the provided access token has administrative access. - It verifies the access type stored in the token, allowing or denying access based on the user's privileges. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the validation process. - """ - try: - user_access_type = get_access_type_from_token(token) - if user_access_type != ADMIN_ACCESS: - return create_error_response("UNAUTHORIZED", "Unauthorized", 401) - except JWTError: - raise JWTError("Invalid token") - - -@router.post("/token") -async def login_for_access_token( - token: CreateTokenRequest, db_session: Session = Depends(get_db_session) -): - """ - Endpoint for user login to obtain an access token. - - Parameters: - - token (CreateTokenRequest): The request model containing username, password, and neverExpires flag. - - db_session (Session, optional): SQLAlchemy database session. Obtained through dependency injection. - - Returns: - - Union[dict, Tuple[str, str, int]]: A dictionary containing the access token if login is successful, - or a tuple representing an error response if login fails. - - This endpoint handles user authentication by verifying the provided credentials. - If successful, it generates an access token and returns it to the user. - - Raises: - - Exception: If an unexpected error occurs during the authentication process. - """ - access_token = await authenticate_user( - token.username, token.password, token.neverExpires, db_session - ) - if not access_token: - return create_error_response( - "BAD_REQUEST", "Unable to retrieve access token", 400 - ) - return {"access_token": access_token, "token_type": "bearer"} - - -@router.get("/validate_token") -async def check_validate_token( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Endpoint for validating the integrity and expiration of an access token. - - Parameters: - - token (str): The access token to be validated. - - db_session (Session, optional): SQLAlchemy database session. Obtained through dependency injection. - - Returns: - - Union[dict, Tuple[str, str, int]]: A dictionary with a success message if the token is valid, - or a tuple representing an error response if validation fails. - - This endpoint checks the integrity and expiration of the provided access token. - If the token is valid, it returns a success message; otherwise, it returns an error response. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the validation process. - """ - try: - return validate_token(db_session, token) - except JWTError: - # Return an error response if the user is not authenticated - return ("UNAUTHORIZED", "Unauthorized", 401) - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in get_user_data: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -@router.get("/users/me", response_model=UserResponse) -async def read_users_me( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Endpoint to retrieve user data based on the provided access token. - - Parameters: - - token (str): The access token used to identify the user. - - db_session (Session, optional): SQLAlchemy database session. Obtained through dependency injection. - - Returns: - - Union[dict, Tuple[str, str, int]]: A dictionary containing user data, - or a tuple representing an error response if retrieval fails. - - This endpoint fetches and returns the user's data based on the provided access token. - It validates the token, retrieves the associated user ID, and returns the user's details - in a format consistent with the UserResponse Pydantic model. - - Raises: - - JWTError: If there is an issue with JWT decoding or the token is invalid. - - Exception: If an unexpected error occurs during the data retrieval process. - """ - return get_user_data(db_session, token) - - -@router.delete("/logout/{user_id}") -async def logout( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Endpoint for user logout, revoking the associated access token. - - Parameters: - - user_id (int): The user ID for which logout is requested. - - token (str): The access token used to identify the user. - - db_session (Session, optional): SQLAlchemy database session. Obtained through dependency injection. - - Returns: - - Union[dict, Tuple[str, str, int]]: A dictionary with a success message if logout is successful, - or a tuple representing an error response if logout fails. - - This endpoint revokes the access token associated with the provided user ID, effectively logging the user out. - If the token is found and successfully revoked, it returns a success message; otherwise, it returns an error response. - - Raises: - - Exception: If an unexpected error occurs during the logout process. - """ - try: - access_token = ( - db_session.query(AccessToken) - .filter(AccessToken.user_id == user_id, AccessToken.token == token) - .first() - ) - if access_token: - db_session.delete(access_token) - db_session.commit() - return {"message": "Logged out successfully"} - else: - return create_error_response("NOT_FOUND", "Token not found", 404) - - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in logout: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) diff --git a/backend/controllers/stravaController.py b/backend/controllers/stravaController.py deleted file mode 100644 index 71807009..00000000 --- a/backend/controllers/stravaController.py +++ /dev/null @@ -1,685 +0,0 @@ -""" -API Router for Strava Integration and Activity Processing. - -This module defines FastAPI routes and functions for integrating Strava accounts, -refreshing Strava tokens, and processing Strava activities. It includes endpoints -for handling Strava callback, setting/unsetting unique user states for Strava link logic, -and background tasks for fetching and processing Strava activities. - -Endpoints: -- GET /strava/strava-callback: Handle Strava callback to link user Strava accounts. -- PUT /strava/set-user-unique-state/{state}: Set unique state for user Strava link logic. -- PUT /strava/unset-user-unique-state: Unset unique state for user Strava link logic. - -Functions: -- refresh_strava_token: Refresh Strava tokens for all users in the database. -- get_strava_activities: Fetch and process Strava activities for all users. -- get_user_strava_activities: Fetch and process Strava activities for a specific user. -- process_activity: Process individual Strava activity and store in the database. -- update_strava_user_tokens: Common function to update Strava tokens and perform additional tasks. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. -- BackgroundTasks: FastAPI class for handling background tasks. -- Session: SQLAlchemy session for database interactions. - -Models: -- User: SQLAlchemy model for user records. -- Activity: SQLAlchemy model for Strava activity records. - -Logger: -- Logger named "myLogger" for logging errors, exceptions, and informational events. -""" -from datetime import datetime, timedelta -from db.db import User, Activity -from fastapi import APIRouter, Depends -from fastapi.responses import RedirectResponse -from fastapi.security import OAuth2PasswordBearer -from jose import jwt, JWTError -from stravalib.client import Client -from pint import Quantity -from concurrent.futures import ThreadPoolExecutor -from fastapi import BackgroundTasks -from urllib.parse import urlencode -from . import sessionController -from dependencies import get_db_session, create_error_response -from sqlalchemy.orm import Session -from fastapi.responses import JSONResponse -import logging -import requests -import os - -# Define the API router -router = APIRouter() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -# Strava logic to refresh user Strava account refresh account -def refresh_strava_token(db_session: Session): - """ - Refresh Strava tokens for all users in the database. - - Parameters: - - db_session (Session): SQLAlchemy database session. - - Returns: - - None: The function updates Strava tokens in the database and logs the results. - - Raises: - - Exception: For unexpected errors during token refresh. - """ - # Strava token refresh endpoint - token_url = "https://www.strava.com/oauth/token" - - try: - # Query all users from the database - users = db_session.query(User).all() - - for user in users: - # expires_at = user.strava_token_expires_at - if user.strava_token_expires_at is not None: - refresh_time = user.strava_token_expires_at - timedelta(minutes=60) - - if datetime.utcnow() > refresh_time: - # Parameters for the token refresh request - payload = { - "client_id": os.environ.get("STRAVA_CLIENT_ID"), - "client_secret": os.environ.get("STRAVA_CLIENT_SECRET"), - "refresh_token": user.strava_refresh_token, - "grant_type": "refresh_token", - } - - try: - # Make a POST request to refresh the Strava token - response = requests.post(token_url, data=payload) - response.raise_for_status() # Raise an error for bad responses - - tokens = response.json() - - # Update the user in the database - db_user = ( - db_session.query(User) - .filter(User.id == user.id) - .first() - ) - - if db_user: - db_user.strava_token = tokens["access_token"] - db_user.strava_refresh_token = tokens["refresh_token"] - db_user.strava_token_expires_at = ( - datetime.fromtimestamp(tokens["expires_at"]) - ) - db_session.commit() # Commit the changes to the database - logger.info( - f"Token refreshed successfully for user {user.id}." - ) - else: - logger.error("User not found in the database.") - except requests.exceptions.RequestException as req_err: - logger.error( - f"Error refreshing token for user {user.id}: {req_err}" - ) - else: - logger.info( - f"Token not refreshed for user {user.id}. Will not expire in less than 60min" - ) - else: - logger.info(f"User {user.id} does not have strava linked") - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in refresh_strava_token: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def get_strava_activities(start_date: datetime, db_session: Session): - """ - Fetch and process Strava activities for all users. - - Parameters: - - start_date (datetime): The start date to retrieve activities after. - - db_session (Session): SQLAlchemy database session. - - Returns: - - None: The function fetches Strava activities, processes them, and stores in the database. - - Raises: - - Exception: For unexpected errors during activity fetching and processing. - """ - try: - # Query all users from the database - users = db_session.query(User).all() - - for user in users: - if user.strava_token_expires_at is not None: - - # Create a Strava client with the user's access token - stravaClient = Client(access_token=user.strava_token) - - # Fetch Strava activities after the specified start date - strava_activities = list( - stravaClient.get_activities(after=start_date) - ) - - if strava_activities: - # Initialize an empty list for results - all_results = [] - - # Use ThreadPoolExecutor for parallel processing of activities - with ThreadPoolExecutor() as executor: - results = list( - executor.map( - lambda activity: process_activity( - activity, user.id, stravaClient, db_session - ), - strava_activities, - ) - ) - - # Append non-empty and non-None results to the overall results list - all_results.extend(results) - - # Flatten the list of results - activities_to_insert = [ - activity for sublist in all_results for activity in sublist - ] - - # Bulk insert all activities into the database - db_session.bulk_save_objects(activities_to_insert) - db_session.commit() - - # Log an informational event for tracing - logger.info(f"User {user.id}: {len(strava_activities)} periodic activities processed") - else: - # Log an informational event if no activities were found - logger.info(f"User {user.id}: No new activities found after {start_date}") - else: - # Log an informational event if the user does not have Strava linked - logger.info(f"User {user.id} does not have Strava linked") - except Exception as err: - # Log the error, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in strava_set_user_unique_state: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -def get_user_strava_activities(start_date: datetime, user_id: int, db_session: Session): - """ - Fetch and process Strava activities for a specific user. - - Parameters: - - start_date (datetime): The start date to retrieve activities after. - - user_id (int): The user ID for whom to fetch and process Strava activities. - - db_session (Session): SQLAlchemy database session. - - Returns: - - None: The function fetches Strava activities for the specified user, - processes them, and stores in the database. - - Raises: - - Exception: For unexpected errors during user-specific activity fetching and processing. - """ - # Query user from the database - db_user = db_session.query(User).get(user_id) - - # Check if db returned an user object and variable is set - if db_user: - if db_user.strava_token_expires_at is not None: - # Log an informational event for tracing - logger.info(f"User {db_user.id}: Started initial activities processing") - - # Create a Strava client with the user's access token - stravaClient = Client(access_token=db_user.strava_token) - - # Fetch Strava activities after the specified start date - strava_activities = list(stravaClient.get_activities(after=start_date)) - - if strava_activities: - # Initialize an empty list for results - all_results = [] - - # Use ThreadPoolExecutor for parallel processing of activities - with ThreadPoolExecutor() as executor: - results = list( - executor.map( - lambda activity: process_activity( - activity, db_user.id, stravaClient, db_session - ), - strava_activities, - ) - ) - - # Append non-empty and non-None results to the overall results list - all_results.extend(results) - - # Flatten the list of results - activities_to_insert = [ - activity for sublist in all_results for activity in sublist - ] - - # Bulk insert all activities into the database - db_session.bulk_save_objects(activities_to_insert) - db_session.commit() - - # Log an informational event for tracing - logger.info(f"User {db_user.id}: {len(strava_activities)} initial activities processed") - - else: - # Log an informational event if no activities were found - logger.info(f"User {db_user.id}: No new activities found after {start_date}") - else: - # Log an informational event if the user does not have Strava linked - logger.info(f"User {db_user.id} does not have Strava linked") - - else: - # Log an informational event if the user is not found - logger.info(f"User with ID {user_id} not found.") - - -def process_activity(activity, user_id, stravaClient, db_session: Session): - """ - Process individual Strava activity and store in the database. - - Parameters: - - activity: Strava activity object obtained from the Strava API. - - user_id (int): The user ID associated with the Strava activity. - - stravaClient: Strava client instance for making API requests. - - db_session (Session): SQLAlchemy database session. - - Returns: - - List[Activity]: List of Activity objects constructed from the Strava activity. - - Raises: - - Exception: For unexpected errors during activity processing. - """ - activities_to_insert = [] - - # Check if the activity already exists in the database - activity_record = ( - db_session.query(Activity) - .filter(Activity.strava_activity_id == activity.id) - .first() - ) - - if activity_record: - # Skip existing activities - return activities_to_insert - - # Parse start and end dates - start_date_parsed = activity.start_date - # Ensure activity.elapsed_time is a numerical value - elapsed_time_seconds = ( - activity.elapsed_time.total_seconds() - if isinstance(activity.elapsed_time, timedelta) - else activity.elapsed_time - ) - end_date_parsed = start_date_parsed + timedelta(seconds=elapsed_time_seconds) - - # Initialize location variables - latitude, longitude = 0, 0 - - if hasattr(activity, "start_latlng") and activity.start_latlng is not None: - latitude = activity.start_latlng.lat - longitude = activity.start_latlng.lon - - city, town, country = None, None, None - - # Retrieve location details using reverse geocoding - if latitude != 0 and longitude != 0: - # Encode URL with query parameters to ensure proper encoding and protection against special characters. - url_params = {"lat": latitude, "lon": longitude} - url = f"https://geocode.maps.co/reverse?{urlencode(url_params)}" - # url = f"https://geocode.maps.co/reverse?lat={latitude}&lon={longitude}" - try: - # Make a GET request - response = requests.get(url) - - # Check if the request was successful (status code 200) - if response.status_code == 200: - # Parse the JSON response - data = response.json() - - # Extract the town and country from the address components - city = data.get("address", {}).get("city", None) - town = data.get("address", {}).get("town", None) - country = data.get("address", {}).get("country", None) - else: - logger.error(f"Error location: {response.status_code}") - logger.error(f"Error location: {url}") - except Exception as err: - # Log the error and return an error response - logger.error(f"Error in process_activity: {err}", exc_info=True) - - # List to store constructed waypoints - waypoints = [] - - # Initialize variables for elevation gain and loss - elevation_gain = 0 - elevation_loss = 0 - previous_elevation = None - - # Get streams for the activity - streams = stravaClient.get_activity_streams( - activity.id, - types=[ - "latlng", - "altitude", - "time", - "heartrate", - "cadence", - "watts", - "velocity_smooth", - ], - ) - - # Extract data from streams - latitudes = streams["latlng"].data if "latlng" in streams else [] - longitudes = streams["latlng"].data if "latlng" in streams else [] - elevations = streams["altitude"].data if "altitude" in streams else [] - times = streams["time"].data if "time" in streams else [] - heart_rates = streams["heartrate"].data if "heartrate" in streams else [] - cadences = streams["cadence"].data if "cadence" in streams else [] - powers = streams["watts"].data if "watts" in streams else [] - velocities = ( - streams["velocity_smooth"].data if "velocity_smooth" in streams else [] - ) - - # Iterate through stream data to construct waypoints - for i in range(len(heart_rates)): - waypoint = { - "lat": latitudes[i] if i < len(latitudes) else None, - "lon": longitudes[i] if i < len(longitudes) else None, - "ele": elevations[i] if i < len(elevations) else None, - "time": times[i] if i < len(times) else None, - "hr": heart_rates[i] if i < len(heart_rates) else None, - "cad": cadences[i] if i < len(cadences) else None, - "power": powers[i] if i < len(powers) else None, - "vel": velocities[i] if i < len(velocities) else None, - "pace": 1 / velocities[i] - if i < len(velocities) and velocities[i] != 0 - else None, - } - - # Calculate elevation gain and loss on-the-fly - current_elevation = elevations[i] if i < len(elevations) else None - - if current_elevation is not None: - if previous_elevation is not None: - elevation_change = current_elevation - previous_elevation - - if elevation_change > 0: - elevation_gain += elevation_change - else: - elevation_loss += abs(elevation_change) - - previous_elevation = current_elevation - - # Append the constructed waypoint to the waypoints list - waypoints.append(waypoint) - - # Calculate average speed, pace, and watts - average_speed = 0 - if activity.average_speed is not None: - average_speed = ( - float(activity.average_speed.magnitude) - if isinstance(activity.average_speed, Quantity) - else activity.average_speed - ) - - average_pace = 1 / average_speed if average_speed != 0 else 0 - - average_watts = 0 - if activity.average_watts is not None: - average_watts = activity.average_watts - - # Map activity type to a numerical value - auxType = 10 # Default value - type_mapping = { - "running": 1, - "Run": 1, - "trail running": 2, - "TrailRun": 2, - "VirtualRun": 3, - "cycling": 4, - "Ride": 4, - "GravelRide": 5, - "EBikeRide": 6, - "EMountainBikeRide": 6, - "VirtualRide": 7, - "virtual_ride": 7, - "MountainBikeRide": 8, - "swimming": 9, - "Swim": 9, - "open_water_swimming": 9, - "Workout": 10, - } - auxType = type_mapping.get(activity.sport_type, 10) - - # Create a new Activity record - newActivity = Activity( - user_id=user_id, - name=activity.name, - distance=round(float(activity.distance)) - if isinstance(activity.distance, Quantity) - else round(activity.distance), - activity_type=auxType, - start_time=start_date_parsed, - end_time=end_date_parsed, - city=city, - town=town, - country=country, - created_at=datetime.utcnow(), - waypoints=waypoints, - elevation_gain=elevation_gain, - elevation_loss=elevation_loss, - pace=average_pace, - average_speed=average_speed, - average_power=average_watts, - strava_activity_id=activity.id, - ) - - activities_to_insert.append(newActivity) - - return activities_to_insert - - -# Strava route to link user Strava account -@router.get("/strava/strava-callback") -async def strava_callback( - state: str, - code: str, - background_tasks: BackgroundTasks, - db_session: Session = Depends(get_db_session), -): - """ - Handle Strava callback to link user Strava accounts. - - Parameters: - - state (str): Unique state associated with the user Strava link process. - - code (str): Authorization code received from Strava callback. - - background_tasks (BackgroundTasks): FastAPI class for handling background tasks. - - db_session (Session): SQLAlchemy database session. - - Returns: - - RedirectResponse: Redirects to the main page or specified URL after processing. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors during Strava callback processing. - """ - token_url = "https://www.strava.com/oauth/token" - payload = { - "client_id": os.environ.get("STRAVA_CLIENT_ID"), - "client_secret": os.environ.get("STRAVA_CLIENT_SECRET"), - "code": code, - "grant_type": "authorization_code", - } - try: - response = requests.post(token_url, data=payload) - if response.status_code != 200: - return create_error_response("ERROR", "Error retrieving tokens from Strava.", response.status_code) - - tokens = response.json() - - # Query the activities records using SQLAlchemy - db_user = db_session.query(User).filter(User.strava_state == state).first() - - if db_user: - db_user.strava_token = tokens["access_token"] - db_user.strava_refresh_token = tokens["refresh_token"] - db_user.strava_token_expires_at = datetime.fromtimestamp( - tokens["expires_at"] - ) - db_session.commit() # Commit the changes to the database - - # get_strava_activities((datetime.utcnow() - timedelta(days=90)).strftime("%Y-%m-%dT%H:%M:%SZ")) - background_tasks.add_task( - get_user_strava_activities, - ( - datetime.utcnow() - - timedelta( - days=int(os.environ.get("STRAVA_DAYS_ACTIVITIES_ONLINK")) - ) - ).strftime("%Y-%m-%dT%H:%M:%SZ"), - db_user.id, - db_session, - ) - - # Redirect to the main page or any other desired page after processing - redirect_url = "https://"+os.environ.get("API_ENDPOINT")+"/settings/settings.php?profileSettings=1&stravaLinked=1" # Change this URL to your main page - return RedirectResponse(url=redirect_url) - else: - return create_error_response("NOT_FOUND", "User not found", 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in read_users_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - -# Define an HTTP PUT route set strava unique state for link logic -@router.put("/strava/set-user-unique-state/{state}") -async def strava_set_user_unique_state( - state: str, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Set unique state for user Strava link logic. - - Parameters: - - state (str): Unique state associated with the user Strava link process. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of setting the Strava user state. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors during setting Strava user state. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(token) - - # Get the user ID from the token - user_id = sessionController.get_user_id_from_token(token) - - # Query the database to find the user by their ID - user = db_session.query(User).filter(User.id == user_id).first() - - # Check if the user with the given ID exists - if not user: - return create_error_response("NOT_FOUND", "User not found", 404) - - # Set the user's photo paths to None to delete the photo - user.strava_state = state - - # Commit the changes to the database - db_session.commit() - - # Return a success message - return JSONResponse( - content={"message": f"Strava state for user {user_id} has been updated"}, status_code=200 - ) - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in strava_set_user_unique_state: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP PUT route set strava unique state for link logic -@router.put("/strava/unset-user-unique-state") -async def strava_unset_user_unique_state( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Unset unique state for user Strava link logic. - - Parameters: - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of unsetting the Strava user state. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors during unsetting Strava user state. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(token) - - # Get the user ID from the token - user_id = sessionController.get_user_id_from_token(token) - - # Query the database to find the user by their ID - user = db_session.query(User).filter(User.id == user_id).first() - - # Check if the user with the given ID exists - if not user: - return create_error_response("NOT_FOUND", "User not found", 404) - - # Set the user's photo paths to None to delete the photo - user.strava_state = None - - # Commit the changes to the database - db_session.commit() - - # Return a success message - return JSONResponse( - content={"message": f"Strava state for user {user_id} has been updated"}, status_code=200 - ) - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in strava_set_user_unique_state: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) \ No newline at end of file diff --git a/backend/controllers/userController.py b/backend/controllers/userController.py deleted file mode 100644 index 70ebcaea..00000000 --- a/backend/controllers/userController.py +++ /dev/null @@ -1,927 +0,0 @@ -""" -API Router for managing user information. - -This module defines FastAPI routes for performing CRUD operations on user records. -It includes endpoints for retrieving, creating, updating, and deleting user records. -The routes handle user authentication, database interactions using SQLAlchemy, -and provide JSON responses with appropriate metadata. - -Endpoints: -- GET /users/all: Retrieve all users. -- GET /users/number: Retrieve the total number of users. -- GET /users/all/pagenumber/{pageNumber}/numRecords/{numRecords}: Retrieve users with pagination. -- GET /users/username/{username}: Retrieve users by username. -- GET /users/id/{user_id}: Retrieve users by user ID. -- GET /users/{username}/id: Retrieve user ID by username. -- GET /users/{user_id}/photo_path: Retrieve user photo path by user ID. -- GET /users/{user_id}/photo_path_aux: Retrieve user photo path aux by user ID. -- POST /users/create: Create a new user. -- PUT /users/{user_id}/edit: Edit an existing user. -- PUT /users/{user_id}/delete-photo: Delete a user's photo. -- DELETE /users/{user_id}/delete: Delete a user. - -Dependencies: -- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens. -- get_db_session: Dependency function to get a database session. -- create_error_response: Function to create a standardized error response. - -Models: -- UserBase: Base Pydantic model for user attributes. -- UserCreateRequest: Pydantic model for creating user records. -- UserEditRequest: Pydantic model for editing user records. -- UserResponse: Pydantic model for user responses. - -Functions: -- user_record_to_dict: Convert User SQLAlchemy objects to dictionaries. - -Logger: -- Logger named "myLogger" for logging errors and exceptions. -""" -import logging -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordBearer -from pydantic import BaseModel -from typing import List, Optional -from jose import JWTError -from fastapi.responses import JSONResponse -from datetime import date -from . import sessionController -from sqlalchemy.orm import Session -from db.db import ( - User, - Gear, -) -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() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -class UserBase(BaseModel): - """ - Base Pydantic model for representing user attributes. - - Attributes: - - name (str): The name of the user. - - username (str): The username of the user. - - email (str): The email address of the user. - - preferred_language (str): The preferred language of the user. - - city (str, optional): The city where the user resides. - - birthdate (str, optional): The birthdate of the user. - - gender (int): The gender of the user. - - access_type (int): The access type of the user. - - photo_path (str, optional): The path to the user's main photo. - - photo_path_aux (str, optional): The path to the user's auxiliary photo. - - is_active (int): The status indicating whether the user is active. - """ - - name: str - username: str - email: str - preferred_language: str - city: Optional[str] - birthdate: Optional[str] - gender: int - access_type: int - photo_path: Optional[str] - 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 - new user records. Includes an additional attribute 'password' - to idefine user password. - - """ - - password: str - - -class UserEditRequest(UserBase): - """ - Pydantic model for editing user records. - - Inherits from UserBase, which defines the base attributes for user. - - 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. - - Inherits from UserBase, which defines the base attributes for a user. - - This class extends the UserBase Pydantic model and is designed for representing - user responses. It includes an additional attribute 'id' to represent the user's ID. - - Attributes: - - id (int): The unique identifier for the user. - - is_strava_linked (int, optional): Indicator for whether the user is linked to Strava. - """ - - 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: - """ - Convert User SQLAlchemy objects to dictionaries. - - Parameters: - - record (User): The User SQLAlchemy object to convert. - - Returns: - - dict: A dictionary representation of the User object. - - This function is used to convert an SQLAlchemy User object into a dictionary format for easier serialization and response handling. - """ - return { - "id": record.id, - "name": record.name, - "username": record.username, - "email": record.email, - "city": record.city, - "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, - "photo_path": record.photo_path, - "photo_path_aux": record.photo_path_aux, - "is_active": record.is_active, - "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.strftime( - "%Y-%m-%dT%H:%M:%S" - ) - if record.strava_token_expires_at - else None, - } - - -# Define an HTTP GET route to retrieve all users -@router.get("/users/all", response_model=list[dict]) -async def read_users_all( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Retrieve all user records. - - Parameters: - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user records. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Query all users from the database - user_records = db_session.query(User).all() - - # Use the user_record_to_dict function to convert SQLAlchemy objects to dictionaries - user_records_dict = [user_record_to_dict(record) for record in user_records] - - # Include metadata in the response - metadata = {"total_records": len(user_records), "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": user_records_dict} - ) - - 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_users_all: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve the number of users -@router.get("/users/number") -async def read_users_number( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Retrieve the total number of user records. - - Parameters: - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the total number of user records. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Count the number of users in the database - user_count = db_session.query(User).count() - - # Include metadata in the response - metadata = {"total_records": 1, "api_version": API_VERSION} - - # Return the queried values using JSONResponse - return JSONResponse(content={"metadata": metadata, "content": user_count}) - - 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_users_number: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve user records with pagination -@router.get( - "/users/all/pagenumber/{pageNumber}/numRecords/{numRecords}", - response_model=List[dict], -) -async def read_users_all_pagination( - pageNumber: int, - numRecords: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user records with pagination. - - Parameters: - - pageNumber (int): The page number for pagination. - - numRecords (int): The number of records to retrieve per page. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user records for the specified page. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Use SQLAlchemy to query the user records with pagination - user_records = ( - db_session.query(User) - .order_by(User.name.asc()) - .offset((pageNumber - 1) * numRecords) - .limit(numRecords) - .all() - ) - - # Use the user_record_to_dict function to convert SQLAlchemy objects to dictionaries - user_records_dict = [user_record_to_dict(record) for record in user_records] - - # Include metadata in the response - metadata = { - "total_records": len(user_records), - "pageNumber": pageNumber, - "numRecords": numRecords, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": user_records_dict} - ) - - 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_users_all_pagination: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve user records by username -@router.get("/users/username/{username}", response_model=List[dict]) -async def read_users_username( - username: str, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user records by username. - - Parameters: - - username (str): The username to search for. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user records matching the username. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Define a search term - partial_username = unquote(username).replace("+", " ") - - # Use SQLAlchemy to query the user records by username - user_records = ( - db_session.query(User) - .filter(User.username.like(f"%{partial_username}%")) - .all() - ) - - # Use the user_record_to_dict function to convert SQLAlchemy objects to dictionaries - user_records_dict = [user_record_to_dict(record) for record in user_records] - - # Include metadata in the response - metadata = { - "total_records": len(user_records), - "username": username, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": user_records_dict} - ) - - 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_users_username: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve user records by user ID -@router.get("/users/id/{user_id}", response_model=List[dict]) -async def read_users_id( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user records by user ID. - - Parameters: - - user_id (int): The ID of the user to retrieve. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and user records matching the user ID. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Use SQLAlchemy to query the user records by user ID - user_records = db_session.query(User).filter(User.id == user_id).all() - - # Use the user_record_to_dict function to convert SQLAlchemy objects to dictionaries - user_records_dict = [user_record_to_dict(record) for record in user_records] - - # Include metadata in the response - metadata = { - "total_records": len(user_records), - "user_id": user_id, - "api_version": API_VERSION, - } - - # Return the queried values using JSONResponse - return JSONResponse( - content={"metadata": metadata, "content": user_records_dict} - ) - - 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_users_id: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve user ID by username -@router.get("/users/{username}/id") -async def read_users_username_id( - username: str, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user ID by username. - - Parameters: - - username (str): The username to search for. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the user ID matching the username. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Use SQLAlchemy to query the user ID by username - user_id = ( - db_session.query(User.id) - .filter(User.username == unquote(username).replace("+", " ")) - .first() - ) - - # Include metadata in the response - 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}}) - - 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_users_username_id: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve user photos by user ID -@router.get("/users/{user_id}/photo_path") -async def read_users_id_photo_path( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user photo path by user ID. - - Parameters: - - user_id (int): The ID of the user to retrieve the photo path for. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the user's photo path. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Use SQLAlchemy to query the user's photo path by user ID - user = db_session.query(User.photo_path).filter(User.id == user_id).first() - - if user: - # Include metadata in the response - 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}, - } - ) - 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 - ) - - 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_users_id_photo_path: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP GET route to retrieve user photos aux by user ID -@router.get("/users/{user_id}/photo_path_aux") -async def read_users_id_photo_path_aux( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Retrieve user photo path aux by user ID. - - Parameters: - - user_id (int): The ID of the user to retrieve the auxiliary photo path for. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response containing metadata and the user's auxiliary photo path. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Use SQLAlchemy to query the user's photo path by user ID - user = db_session.query(User.photo_path_aux).filter(User.id == user_id).first() - - if user: - # Include metadata in the response - 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}, - } - ) - 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 - ) - - 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_users_id_photo_path_aux: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP POST route to create a new user -@router.post("/users/create") -async def create_user( - user: UserCreateRequest, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Create a new user. - - Parameters: - - user (UserCreateRequest): Pydantic model containing the user information for creation. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of the user creation. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Create a new User instance using SQLAlchemy's ORM - new_user = User( - name=user.name, - username=user.username, - email=user.email, - password=user.password, - preferred_language=user.preferred_language, - city=user.city, - birthdate=user.birthdate, - gender=user.gender, - access_type=user.access_type, - photo_path=user.photo_path, - photo_path_aux=user.photo_path_aux, - is_active=user.is_active, - ) - - # Add the new user to the database - db_session.add(new_user) - db_session.commit() - - # Return a JSONResponse indicating the success of the user creation - return JSONResponse( - content={"message": "User created successfully"}, status_code=201 - ) - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in create_user: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP PUT route to edit a user's information -@router.put("/users/{user_id}/edit") -async def edit_user( - user_id: int, - user_attributtes: UserEditRequest, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Edit an existing user's information. - - Parameters: - - user_id (int): The ID of the user to edit. - - user_attributes (UserEditRequest): Pydantic model containing the user information for editing. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of the user edit. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Query the database to find the user by their ID - user = db_session.query(User).filter(User.id == user_id).first() - - # Check if the user with the given ID exists - if not user: - # Return an error response if the user record is not found - return create_error_response("NOT_FOUND", "User not found", 404) - - # Update user information if provided in the form data - if user_attributtes.name is not None: - user.name = user_attributtes.name - if user_attributtes.username is not None: - user.username = user_attributtes.username - if user_attributtes.email is not None: - user.email = user_attributtes.email - if user_attributtes.preferred_language is not None: - user.preferred_language = user_attributtes.preferred_language - if user_attributtes.city is not None: - user.city = user_attributtes.city - if user_attributtes.birthdate is not None: - user.birthdate = user_attributtes.birthdate - if user_attributtes.gender is not None: - user.gender = user_attributtes.gender - if user_attributtes.access_type is not None: - user.access_type = user_attributtes.access_type - if user_attributtes.photo_path is not None: - user.photo_path = user_attributtes.photo_path - if user_attributtes.photo_path_aux is not None: - user.photo_path_aux = user_attributtes.photo_path_aux - if user_attributtes.is_active is not None: - user.is_active = user_attributtes.is_active - - # Commit the changes to the database - db_session.commit() - - # Return a JSONResponse indicating the success of the user edit - 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) - except Exception as err: - # Log the error, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in edit_user: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP PUT route to delete a user's photo -@router.put("/users/{user_id}/delete-photo") -async def delete_user_photo( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Delete a user's photo. - - Parameters: - - user_id (int): The ID of the user to delete the photo for. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of the photo deletion. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - # Query the database to find the user by their ID - user = db_session.query(User).filter(User.id == user_id).first() - - # Check if the user with the given ID exists - if not user: - # Return an error response if the user record is not found - return create_error_response("NOT_FOUND", "User not found", 404) - - # Set the user's photo paths to None to delete the photo - user.photo_path = None - user.photo_path_aux = None - - # Commit the changes to the database - db_session.commit() - - # 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, - ) - - 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, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in delete_user_photo: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) - - -# Define an HTTP DELETE route to delete a user -@router.delete("/users/{user_id}/delete") -async def delete_user( - user_id: int, - token: str = Depends(oauth2_scheme), - db_session: Session = Depends(get_db_session), -): - """ - Delete a user. - - Parameters: - - user_id (int): The ID of the user to delete. - - token (str): The access token for user authentication. - - db_session (Session): SQLAlchemy database session. - - Returns: - - JSONResponse: JSON response indicating the success of the user deletion. - - Raises: - - JWTError: If the user's access token is invalid or expired. - - Exception: For other unexpected errors. - """ - try: - # Validate the user's access token using the oauth2_scheme - sessionController.validate_token(db_session, token) - - # Validate that the user has admin access - sessionController.validate_admin_access(token) - - user = db_session.query(User).filter(User.id == user_id).first() - - # Check if the user with the given ID exists - if not user: - # Return an error response if the user record is not found - return create_error_response("NOT_FOUND", "User not found", 404) - - # Check for existing dependencies if needed (e.g., related systems) - 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 - ) - # Delete the user from the database - db_session.delete(user) - db_session.commit() - - # Return a JSONResponse indicating the success of the user edit - 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) - except Exception as err: - # Log the error, rollback the transaction, and return an error response - db_session.rollback() - logger.error(f"Error in delete_user: {err}", exc_info=True) - return create_error_response( - "INTERNAL_SERVER_ERROR", "Internal Server Error", 500 - ) diff --git a/backend/crud/access_tokens.py b/backend/crud/access_tokens.py new file mode 100644 index 00000000..98c8b3e4 --- /dev/null +++ b/backend/crud/access_tokens.py @@ -0,0 +1,133 @@ +import logging + +import models + +from datetime import datetime + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def get_acess_tokens_by_user_id(user_id: int, db: Session): + try: + access_tokens = ( + db.query(models.AccessToken) + .filter(models.AccessToken.user_id == user_id) + .all() + ) + if access_tokens is None: + # If the user was not found, return a 404 Not Found error + return None + + return access_tokens + except Exception as err: + # Log the exception + logger.error(f"Error in get_acess_tokens_by_user_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def create_access_token(token, db: Session): + """Creates a new access token in the database using the provided token.""" + try: + # Create a new access token in the database + db_access_token = models.AccessToken( + token=token.token, + user_id=token.user_id, + created_at=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), + expires_at=token.expires_at, + ) + + # Add the access token to the database and commit the transaction + db.add(db_access_token) + db.commit() + db.refresh(db_access_token) + + # return the access token + return db_access_token + except Exception as err: + # Handle database-related exceptions + db.rollback() # Rollback the transaction to maintain database consistency + + # Log the exception + logger.error(f"Error in create_access_token: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def delete_access_token(token: str, db: Session): + """Deletes an access token from the database using the provided token.""" + try: + # Delete the access token from the database + db_access_token = ( + db.query(models.AccessToken) + .filter(models.AccessToken.token == token) + .delete() + ) + + # Commit the transaction to the database + if db_access_token: + db.delete(db_access_token) + db.commit() + logger.info(f"{db_access_token} access tokens deleted from the database") + return db_access_token + else: + # If the access token was not found, return a 404 Not Found error + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Access token not found", + ) + except Exception as err: + # Handle database-related exceptions + db.rollback() # Rollback the transaction to maintain database consistency + + # Log the exception + logger.error(f"Error in delete_access_token: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def delete_access_tokens(expiration_time: str, db: Session): + """Deletes all access tokens from the database that have expired.""" + try: + # Delete the access tokens from the database + db_access_tokens = ( + db.query(models.AccessToken) + .filter(models.AccessToken.created_at < expiration_time) + .delete() + ) + + # Commit the transaction to the database + if db_access_tokens: + db.commit() + logger.info(f"{db_access_tokens} access tokens deleted from the database") + return db_access_tokens + else: + # If no access tokens were found, log the event and return 0 + logger.info("0 access tokens deleted from the database") + except Exception as err: + # Handle database-related exceptions + db.rollback() # Rollback the transaction to maintain database consistency + + # Log the exception + logger.error(f"Error in delete_access_tokens: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err diff --git a/backend/crud/activities.py b/backend/crud/activities.py new file mode 100644 index 00000000..599676a9 --- /dev/null +++ b/backend/crud/activities.py @@ -0,0 +1,151 @@ +import logging + +from operator import and_, or_ +from fastapi import HTTPException, status +from datetime import datetime +from sqlalchemy import func, desc +from sqlalchemy.orm import Session + +from schemas import activities as activities_schemas +import models + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def get_user_activities( + user_id: int, + db: Session, +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter(models.Activity.user_id == user_id) + .order_by(desc(models.Activity.start_time)) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_activities: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_activities_with_pagination( + user_id: int, db: Session, page_number: int = 1, num_records: int = 5 +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter(models.Activity.user_id == user_id) + .order_by(desc(models.Activity.start_time)) + .offset((page_number - 1) * num_records) + .limit(num_records) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_activities_with_pagination: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_activities_per_timeframe( + user_id: int, + start: datetime, + end: datetime, + db: Session, +): + """Get the activities of a user for a given week""" + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter( + models.Activity.user_id == user_id, + func.date(models.Activity.start_time) >= start, + func.date(models.Activity.start_time) <= end, + ) + .order_by(desc(models.Activity.start_time)) + ).all() + + # Check if there are activities if not return None + if not activities: + return None + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_activities_per_timeframe: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_following_activities_per_timeframe( + user_id: int, + start: datetime, + end: datetime, + db: Session, +): + """Get the activities of the users that the user is following for a given week""" + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter( + and_( + models.Activity.user_id == user_id, + models.Activity.visibility.in_([0, 1]), + ), + func.date(models.Activity.start_time) >= start, + func.date(models.Activity.start_time) <= end, + ) + .order_by(desc(models.Activity.start_time)) + ).all() + + # Check if there are activities if not return None + if not activities: + return None + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error( + f"Error in get_user_following_activities_per_timeframe: {err}", exc_info=True + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err diff --git a/backend/crud/user_integrations.py b/backend/crud/user_integrations.py new file mode 100644 index 00000000..56ac58ab --- /dev/null +++ b/backend/crud/user_integrations.py @@ -0,0 +1,64 @@ +import logging + +from schemas import user_integrations as user_integrations_schema +import models + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def get_user_integrations_by_user_id(user_id: int, db: Session): + """Get a user integrations by user id from the database""" + try: + user_integrations = ( + db.query(models.UserIntegrations) + .filter(models.UserIntegrations.user_id == user_id) + .first() + ) + if user_integrations is None: + # If the user was not found, return a 404 Not Found error + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User integrations not found", + ) + return user_integrations + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_integrations_by_user_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + +def create_user_integrations(user_id: int, db: Session): + """Create a new user integrations in the database""" + try: + # Create a new user integrations + user_integrations = models.UserIntegrations( + user_id = user_id, + strava_sync_gear = False, + ) + + # Add the user integrations to the database + db.add(user_integrations) + db.commit() + db.refresh(user_integrations) + + # Return the user integrations + return user_integrations + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in create_user_integrations: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err \ No newline at end of file diff --git a/backend/crud/users.py b/backend/crud/users.py new file mode 100644 index 00000000..96d4b56f --- /dev/null +++ b/backend/crud/users.py @@ -0,0 +1,406 @@ +import logging + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError +from urllib.parse import unquote + +from schemas import users as users_schema +import models + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def format_user_birthdate(user): + user.birthdate = user.birthdate.strftime("%Y-%m-%d") if user.birthdate else None + return user + + +def authenticate_user(username: str, password: str, db: Session): + """Get the user from the database and verify the password""" + try: + # Get the user from the database + user = ( + db.query(models.User) + .filter( + models.User.username == username and models.User.password == password + ) + .first() + ) + + # Check if the user exists and if the password is correct and if not return None + if not user: + return None + + # Return the user if the password is correct + return user + except Exception as err: + # Log the exception + logger.error(f"Error in authenticate_user: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_users_number(db: Session): + """Get the number of users in the database""" + try: + return db.query(models.User).count() + except Exception as err: + # Log the exception + logger.error(f"Error in get_users_number: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_users_with_pagination(db: Session, page_number: int = 1, num_records: int = 5): + """Get the users from the database with pagination""" + try: + # Get the users from the database + users = ( + db.query(models.User) + .offset((page_number - 1) * num_records) + .limit(num_records) + .all() + ) + + # If the users were not found, return None + if not users: + return None + + # Format the birthdate + for user in users: + user = format_user_birthdate(user) + + # Return the users + return users + except Exception as err: + # Log the exception + logger.error(f"Error in get_users_with_pagination: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_by_username(username: str, db: Session): + """Get the user from the database by username""" + try: + # Define a search term + partial_username = unquote(username).replace("+", " ") + + # Get the user from the database + users = ( + db.query(models.User) + .filter(models.User.username.like(f"%{partial_username}%")) + .all() + ) + + # If the user was not found, return None + if users is None: + return None + + # Format the birthdate + for user in users: + user = format_user_birthdate(user) + + # Return the user + return users + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_by_username: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_by_id(user_id: int, db: Session): + """Get the user from the database by id""" + try: + # Get the user from the database + user = db.query(models.User).filter(models.User.id == user_id).first() + + # If the user was not found, return None + if user is None: + return None + + # Format the birthdate + user = format_user_birthdate(user) + + # Return the user + return user + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_id_by_username(username: str, db: Session): + """Get the user id from the database by username""" + try: + # Get the user from the database + user_id = ( + db.query(models.User.id) + .filter(models.User.username == unquote(username).replace("+", " ")) + .first() + ) + + # If the user was not found, return None + if user_id is None: + return None + + # Return the user id + return user_id + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_id_by_username: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_photo_path_by_id(user_id: int, db: Session): + try: + # Get the user from the database + user_db = ( + db.query(models.User.photo_path).filter(models.User.id == user_id).first() + ) + + # If the user was not found, return None + if user_db is None: + return None + + # Return the user + return user_db.photo_path + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_photo_path_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_photo_path_aux_by_id(user_id: int, db: Session): + try: + # Get the user from the database + user_db = ( + db.query(models.User.photo_path_aux) + .filter(models.User.id == user_id) + .first() + ) + + # If the user was not found, return None + if user_db is None: + return None + + # Return the photo_path_aux value directly + return user_db.photo_path_aux + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_photo_path_aux_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def create_user(user: users_schema.UserCreate, db: Session): + """Create a new user in the database""" + try: + # Create a new user + db_user = models.User( + name=user.name, + username=user.username, + password=user.password, + email=user.email, + city=user.city, + birthdate=user.birthdate, + preferred_language=user.preferred_language, + gender=user.gender, + access_type=user.access_type, + photo_path=user.photo_path, + photo_path_aux=user.photo_path_aux, + is_active=user.is_active, + ) + + # Add the user to the database + db.add(db_user) + db.commit() + db.refresh(db_user) + + # Return the user + return db_user + except IntegrityError as integrity_error: + # Rollback the transaction + db.rollback() + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Duplicate entry error. Check if email and username are unique", + ) from integrity_error + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in create_user: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def edit_user(user: users_schema.User, db: Session): + """Edit the user in the database""" + try: + # Get the user from the database + db_user = db.query(models.User).filter(models.User.id == user.id).first() + + # Update the user + if user.name is not None: + db_user.name = user.name + if user.username is not None: + db_user.username = user.username + if user.email is not None: + db_user.email = user.email + if user.city is not None: + db_user.city = user.city + if user.birthdate is not None: + db_user.birthdate = user.birthdate + if user.preferred_language is not None: + db_user.preferred_language = user.preferred_language + if user.gender is not None: + db_user.gender = user.gender + if user.access_type is not None: + db_user.access_type = user.access_type + if user.photo_path is not None: + db_user.photo_path = user.photo_path + if user.photo_path_aux is not None: + db_user.photo_path_aux = user.photo_path_aux + if user.is_active is not None: + db_user.is_active = user.is_active + + # Commit the transaction + db.commit() + except IntegrityError as integrity_error: + # Rollback the transaction + db.rollback() + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Duplicate entry error. Check if email and username are unique", + ) from integrity_error + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in edit_user: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def edit_user_password(user_id: int, password: str, db: Session): + """Edit the user password in the database""" + try: + # Get the user from the database + db_user = db.query(models.User).filter(models.User.id == user_id).first() + + # Update the user + db_user.password = password + + # Commit the transaction + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in edit_user_password: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def delete_user_photo(user_id: int, db: Session): + """Delete the user photo path in the database""" + try: + # Get the user from the database + db_user = db.query(models.User).filter(models.User.id == user_id).first() + + # Update the user + db_user.photo_path = None + db_user.photo_path_aux = None + + # Commit the transaction + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in delete_user_photo: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def delete_user(user_id: int, db: Session): + """Delete the user in the database""" + try: + # Delete the user + num_deleted = db.query(models.User).filter(models.User.id == user_id).delete() + + # Check if the user was found and deleted + if num_deleted == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found", + ) + + # Commit the transaction + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in delete_user: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 00000000..8331381c --- /dev/null +++ b/backend/database.py @@ -0,0 +1,26 @@ +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.engine.url import URL + +# Define the database connection URL using environment variables +db_url = URL.create( + drivername="mysql", + username=os.environ.get("DB_USER"), + password=os.environ.get("DB_PASSWORD"), + host=os.environ.get("DB_HOST"), + port=os.environ.get("DB_PORT"), + database=os.environ.get("DB_DATABASE"), +) + +# Create the SQLAlchemy engine +engine = create_engine( + db_url, pool_size=10, max_overflow=20, pool_timeout=180, pool_recycle=3600 +) + +# Create a session factory +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Create a base class for declarative models +Base = declarative_base() \ No newline at end of file diff --git a/backend/db/__init__.py b/backend/db/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/backend/db/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/db/createdb.sql b/backend/db/createdb.sql deleted file mode 100644 index a1366532..00000000 --- a/backend/db/createdb.sql +++ /dev/null @@ -1,163 +0,0 @@ -SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; -SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; -SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; - -CREATE SCHEMA IF NOT EXISTS `gearguardian` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ; -USE `gearguardian`; - --- ----------------------------------------------------- --- Table `gearguardian`.`users` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `gearguardian`.`users` ( - `id` INT(10) NOT NULL AUTO_INCREMENT , - `name` VARCHAR(45) NOT NULL COMMENT 'User real name (May include spaces)' , - `username` VARCHAR(45) NOT NULL UNIQUE COMMENT 'User username (letters, numbers and dots allowed)' , - `email` VARCHAR(250) NOT NULL UNIQUE COMMENT 'User email (max 45 characteres)' , - `password` VARCHAR(100) NOT NULL COMMENT 'User password (hash)' , - `city` VARCHAR(45) NULL COMMENT 'User city' , - `birthdate` DATE NULL COMMENT 'User birthdate (data)' , - `preferred_language` VARCHAR(5) NOT NULL COMMENT 'User preferred language (en, pt, others)' , - `gender` INT(1) NOT NULL COMMENT 'User gender (one digit)(1 - male, 2 - female)' , - `access_type` INT(1) NOT NULL COMMENT 'User type (one digit)(1 - regular user, 2 - admin)' , - `photo_path` VARCHAR(250) NULL COMMENT 'User photo path' , - `photo_path_aux` VARCHAR(250) NULL COMMENT 'Auxiliar photo path' , - `is_active` INT(1) NOT NULL COMMENT 'Is user active (2 - not active, 1 - active)' , - `strava_state` VARCHAR(45) NULL , - `strava_token` VARCHAR(250) NULL , - `strava_refresh_token` VARCHAR(250) NULL , - `strava_token_expires_at` DATETIME NULL , - PRIMARY KEY (`id`) ) -ENGINE = InnoDB; - --- ----------------------------------------------------- --- Create default admin user --- ----------------------------------------------------- -INSERT INTO `gearguardian`.`users` (`id`,`name`,`username`,`password`,`email`,`preferred_language`,`gender`,`access_type`,`is_active`) VALUES (1,'Administrator','admin','d31e6a23d06bb2ca18ad612a596be36183b8e302ba3aa583384e305b279ab9e7',"joao.vitoria.silva@pm.me","en",1,2,1); - --- ----------------------------------------------------- --- Table `gearguardian`.`access_tokens` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `gearguardian`.`access_tokens` ( - `id` INT(10) NOT NULL AUTO_INCREMENT , - `token` VARCHAR(256) NOT NULL COMMENT 'User token' , - `user_id` INT(10) NOT NULL COMMENT 'User ID that the token belongs' , - `created_at` DATETIME NOT NULL COMMENT 'Token creation date (date)' , - `expires_at` DATETIME NOT NULL COMMENT 'Token expiration date (date)' , - PRIMARY KEY (`id`) , - INDEX `FK_user_id_idx` (`user_id` ASC) , - CONSTRAINT `FK_token_user` - FOREIGN KEY (`user_id` ) - REFERENCES `gearguardian`.`users` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION) -ENGINE = InnoDB; - --- ----------------------------------------------------- --- Table `gearguardian`.`gear` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `gearguardian`.`gear` ( - `id` INT(10) NOT NULL AUTO_INCREMENT , - `brand` VARCHAR(45) NULL COMMENT 'Gear brand (May include spaces)' , - `model` VARCHAR(45) NULL COMMENT 'Gear model (May include spaces)' , - `nickname` VARCHAR(45) NOT NULL COMMENT 'Gear nickname (May include spaces)' , - `gear_type` INT(1) NOT NULL COMMENT 'Gear type (1 - bike, 2 - shoes, 3 - wetsuit)' , - `user_id` INT(10) NOT NULL COMMENT 'User ID that the token belongs' , - `created_at` DATETIME NOT NULL COMMENT 'Gear creation date (date)' , - `is_active` INT(1) NOT NULL COMMENT 'Is gear active (0 - not active, 1 - active)' , - PRIMARY KEY (`id`) , - INDEX `FK_user_id_idx` (`user_id` ASC) , - CONSTRAINT `FK_gear_user` - FOREIGN KEY (`user_id` ) - REFERENCES `gearguardian`.`users` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION) -ENGINE = InnoDB; - --- ----------------------------------------------------- --- Table `gearguardian`.`user_settings` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `gearguardian`.`user_settings` ( - `id` INT(10) NOT NULL AUTO_INCREMENT , - `user_id` INT(10) NOT NULL COMMENT 'User ID that the activity belongs' , - `activity_type` INT(2) NULL COMMENT 'Gear type' , - `gear_id` INT(10) NULL COMMENT 'Gear ID associated with this activity' , - PRIMARY KEY (`id`) , - INDEX `FK_user_id_idx` (`user_id` ASC) , - CONSTRAINT `FK_user_settings_user` - FOREIGN KEY (`user_id` ) - REFERENCES `gearguardian`.`users` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION, - INDEX `FK_gear_id_idx` (`gear_id` ASC) , - CONSTRAINT `FK_user_settings_gear` - FOREIGN KEY (`gear_id` ) - REFERENCES `gearguardian`.`gear` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION) -ENGINE = InnoDB; - --- ----------------------------------------------------- --- Table `gearguardian`.`activities` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `gearguardian`.`activities` ( - `id` INT(10) NOT NULL AUTO_INCREMENT , - `user_id` INT(10) NOT NULL COMMENT 'User ID that the activity belongs' , - `name` VARCHAR(255) NULL COMMENT 'Activity name (May include spaces)' , - `distance` INT(9) NOT NULL COMMENT 'Distance in meters' , - `activity_type` INT(2) NOT NULL COMMENT 'Gear type' , - `start_time` DATETIME NOT NULL COMMENT 'Actvitiy start date (datetime)' , - `end_time` DATETIME NOT NULL COMMENT 'Actvitiy end date (datetime)' , - `city` VARCHAR(255) NULL COMMENT 'Activity city (May include spaces)' , - `town` VARCHAR(255) NULL COMMENT 'Activity town (May include spaces)' , - `country` VARCHAR(255) NULL COMMENT 'Activity country (May include spaces)' , - `created_at` DATETIME NOT NULL COMMENT 'Actvitiy creation date (datetime)' , - `waypoints` LONGTEXT NULL COMMENT 'Store waypoints data', - `elevation_gain` INT(5) NOT NULL COMMENT 'Elevation gain in meters' , - `elevation_loss` INT(5) NOT NULL COMMENT 'Elevation loss in meters' , - `pace` DECIMAL(20, 10) NOT NULL COMMENT 'Pace seconds per meter (s/m)' , - `average_speed` DECIMAL(20, 10) NOT NULL COMMENT 'Average speed seconds per meter (s/m)' , - `average_power` INT(4) NOT NULL COMMENT 'Average power (watts)' , - `gear_id` INT(10) NULL COMMENT 'Gear ID associated with this activity' , - `strava_activity_id` BIGINT(20) NULL COMMENT 'Strava activity ID' , - PRIMARY KEY (`id`) , - INDEX `FK_user_id_idx` (`user_id` ASC) , - CONSTRAINT `FK_activity_user` - FOREIGN KEY (`user_id` ) - REFERENCES `gearguardian`.`users` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION, - INDEX `FK_gear_id_idx` (`gear_id` ASC) , - CONSTRAINT `FK_activity_gear` - FOREIGN KEY (`gear_id` ) - REFERENCES `gearguardian`.`gear` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION,) -ENGINE = InnoDB; - --- ----------------------------------------------------- --- Table `gearguardian`.`components` --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS `gearguardian`.`component` ( - `id` INT(10) NOT NULL AUTO_INCREMENT , - `brand` VARCHAR(45) NULL COMMENT 'Component brand (May include spaces)' , - `model` VARCHAR(45) NULL COMMENT 'Component piece model (May include spaces)' , - `nickname` VARCHAR(45) NOT NULL COMMENT 'Component piece nickname (May include spaces)' , - `component_type` INT(1) NOT NULL COMMENT 'Component type (1 - wheels, 2 - tires, 3 - cassette, 4 - chain, 5 - brake rotors, 6 - brake pads, 7 - inner tubes)' , - `component_subtype` INT(2) NULL COMMENT 'Component sub type (1 - front wheel, 2 - back wheel, 3 - front tire, 4 - back tire, 5 - front rotor, 6 - back rotor, 7 - front brake pad, 8 - back brake pad, 9 - front inner tube, 10 - back inner tube)' , - `gear_id` INT(10) NOT NULL COMMENT 'User ID that the token belongs' , - `created_at` DATETIME NOT NULL COMMENT 'Component creation date (date)' , - `is_active` INT(1) NOT NULL COMMENT 'Is component active (0 - not active, 1 - active)' , - PRIMARY KEY (`id`) , - INDEX `FK_gear_id_idx` (`gear_id` ASC) , - CONSTRAINT `FK_component_gear` - FOREIGN KEY (`gear_id` ) - REFERENCES `gearguardian`.`gear` (`id` ) - ON DELETE NO ACTION - ON UPDATE NO ACTION) -ENGINE = InnoDB; - -USE `gearguardian` ; - -SET SQL_MODE=@OLD_SQL_MODE; -SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; -SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; \ No newline at end of file diff --git a/backend/dependencies.py b/backend/dependencies.py index d0c6d6d7..8e5a07d6 100644 --- a/backend/dependencies.py +++ b/backend/dependencies.py @@ -1,69 +1,9 @@ -import logging.config -from sqlalchemy.orm import Session -from db.db import Session as BaseSession -from fastapi.responses import JSONResponse -from controllers import sessionController -from fastapi import Depends -from fastapi.security import OAuth2PasswordBearer - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -def get_db_session() -> Session: - """ - Get a SQLAlchemy database session. - - Returns: - - Session: SQLAlchemy database session. - """ - return BaseSession() - -def configure_logger(): - """ - Configures and returns a logger based on settings specified in a logging configuration file. - - Returns: - - logging.Logger: Configured logger instance. - """ - logging.config.fileConfig('logs/logging_config.ini') - return logging.getLogger('myLogger') - - -def get_current_user( - token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session) -): - """ - Get the ID of the current authenticated user. - - Parameters: - - token (str): The authentication token for the user. - - db_session (Session): SQLAlchemy database session. - - Returns: - - int: The ID of the current authenticated user. - - Raises: - - JWTError: If the authentication token is invalid or expired. - - Exception: For other unexpected errors. - """ - sessionController.validate_token(db_session, token) - return sessionController.get_user_id_from_token(token) - - -# Standardized error response function -def create_error_response(code: str, message: str, status_code: int): - """ - Create a JSON error response. - - Parameters: - - code (str): Error code to be included in the response. - - message (str): Error message to be included in the response. - - status_code (int): HTTP status code for the response. - - Returns: - - JSONResponse: JSON response containing the specified error information. - """ - return JSONResponse( - content={"error": {"code": code, "message": message}}, status_code=status_code - ) +from database import SessionLocal + +def get_db(): + # get DB ssession + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/backend/logs/logging_config.ini b/backend/logs/logging_config.ini deleted file mode 100644 index c3d57d32..00000000 --- a/backend/logs/logging_config.ini +++ /dev/null @@ -1,52 +0,0 @@ -; Configuration for Python logging module - -; Loggers section: Define loggers in the application -[loggers] -keys=root,sampleLogger,myLogger - -; Handlers section: Define handlers that determine where log records are output -[handlers] -keys=fileHandler,debugFileHandler - -; Formatters section: Define formatters that specify the layout of log records -[formatters] -keys=sampleFormatter - -; Configuration for the root logger -[logger_root] -level=DEBUG -handlers=fileHandler -qualname=root - -; Configuration for the sampleLogger logger -[logger_sampleLogger] -level=DEBUG -handlers=fileHandler -qualname=sampleLogger -propagate=0 - -; Configuration for the myLogger logger -[logger_myLogger] -level=DEBUG -handlers=fileHandler,debugFileHandler -qualname=myLogger -propagate=0 - -; Configuration for the fileHandler -[handler_fileHandler] -class=FileHandler -level=INFO -formatter=sampleFormatter -args=['logs/app.log', 'a'] - -; Configuration for the debugFileHandler -[handler_debugFileHandler] -class=FileHandler -level=DEBUG -formatter=sampleFormatter -args=['logs/debug.log', 'a'] - -; Configuration for the sampleFormatter -[formatter_sampleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s -datefmt=%Y-%m-%d %H:%M:%S ; Date format for the timestamp \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index a0507874..61905001 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,79 +1,28 @@ -""" -Main Application File for Backend API - -This file contains the configuration and setup for a FastAPI-based backend API. It includes routing for various controllers, background jobs scheduled using the Advanced Python Scheduler (APScheduler), OpenTelemetry for distributed tracing, and event handlers for startup and shutdown processes. - -Modules and Libraries: -- FastAPI: Web framework for building APIs with Python. -- APScheduler: Library for scheduling background jobs. -- OpenTelemetry: A set of APIs, libraries, agents, instrumentation, and instrumentation bindings for observability in software systems. -- SQLAlchemy: SQL toolkit and Object-Relational Mapping (ORM) for Python. -- Other custom modules for controllers, database management, and dependencies. - -Key Features: -- OpenTelemetry Integration: Configures OpenTelemetry for distributed tracing, including Jaeger exporter if specified in the environment. -- Background Jobs: Uses APScheduler to schedule periodic tasks, such as removing expired tokens, refreshing Strava tokens, and fetching Strava activities. -- FastAPI Instrumentation: Instruments the FastAPI app for automatic tracing and observability. -- Database Initialization: Initializes the database and creates tables during startup. -- Event Handlers: Registers startup and shutdown event handlers to execute tasks at the beginning and end of the application lifecycle. - -Environment Variables: -- JAEGER_ENABLED: Flag to enable or disable Jaeger tracing. -- JAEGER_PROTOCOL, JAEGER_HOST, JAGGER_PORT: Jaeger exporter configuration. - -Routes: -- Session, User, Gear, Activity, ActivityStreams, Follower, and Strava controllers are included as routers. - -Event Handlers: -- "startup": Triggers the creation of database tables during application startup. -- "shutdown": Triggers the shutdown of the background scheduler when the application is shutting down. +import logging -Note: Ensure that required dependencies are installed and environment variables are properly configured before running the application. -""" -# Import FastAPI framework for building APIs from fastapi import FastAPI +from routers import session as session_router, users as users_router, activities as activities_router +from constants import API_VERSION +from database import engine +import models -# Import Advanced Python Scheduler for background jobs -from apscheduler.schedulers.background import BackgroundScheduler - -# Import controllers for different routes -from controllers import ( - sessionController, - userController, - gearController, - activityController, - activity_streamsController, - followerController, - stravaController, -) - -# Import datetime for handling date and time -from datetime import datetime, timedelta - -# Import OpenTelemetry for distributed tracing -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor - -# Import FastAPI instrumentation for automatic tracing -from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor - -# Import OS module for handling environment variables -import os - -import logging - -# Import database-related functions and dependencies -from db.db import create_database_tables -from dependencies import get_db_session, configure_logger +models.Base.metadata.create_all(bind=engine) # Define the FastAPI object -app = FastAPI() +app = FastAPI( + docs_url="/docs", + redoc_url=None, + title="Endurain", + summary="Endurain API for the Endurain app", + version=API_VERSION, + license_info={ + "name": "GNU General Public License v3.0", + "identifier": "GPL-3.0-or-later", + "url": "https://spdx.org/licenses/GPL-3.0-or-later.html", + }, +) # Create loggger -#logger = configure_logger() logger = logging.getLogger("myLogger") logger.setLevel(logging.DEBUG) @@ -88,163 +37,7 @@ # Check for required environment variables required_env_vars = ["DB_HOST", "DB_PORT", "DB_USER", "DB_PASSWORD", "DB_DATABASE", "SECRET_KEY", "ALGORITHM", "ACCESS_TOKEN_EXPIRE_MINUTES", "STRAVA_CLIENT_ID", "STRAVA_CLIENT_SECRET", "STRAVA_AUTH_CODE", "JAEGER_ENABLED", "JAEGER_PROTOCOL", "JAEGER_HOST", "JAGGER_PORT", "STRAVA_DAYS_ACTIVITIES_ONLINK", "API_ENDPOINT"] -for var in required_env_vars: - if var not in os.environ: - logger.error(f"Missing required environment variable: {var}", exc_info=True) - raise EnvironmentError(f"Missing required environment variable: {var}") - - -def startup_event(): - """ - Event handler for application startup. - - This function is responsible for creating the database and tables if they don't exist during the startup of the FastAPI application. - - Raises: - - None. - - Returns: - - None. - """ - logger.info("Backend startup event") - # Create the database and tables if they don't exist - create_database_tables() - logger.info("Will check if there is expired tokens to remove") - sessionController.remove_expired_tokens(db_session=get_db_session()) - - -def shutdown_event(): - """ - Event handler for application shutdown. - - This function is responsible for shutting down the background scheduler when the FastAPI application is shutting down. - - Raises: - - None. - - Returns: - - None. - """ - logger.info("Backend shutdown event") - scheduler.shutdown() - - -def remove_expired_tokens_job(): - """ - Background job to remove expired user authentication tokens. - - This job is scheduled to run at regular intervals, and its purpose is to identify and remove authentication tokens - that have expired. It calls the `remove_expired_tokens` function from the sessionController module. - - Parameters: - - None. - - Raises: - - Exception: If an unexpected error occurs during the token removal process. - - Returns: - - None. - """ - try: - sessionController.remove_expired_tokens(db_session=get_db_session()) - except Exception as err: - logger.error(f"Error in remove_expired_tokens_job: {err}", exc_info=True) - - -def refresh_strava_token_job(): - """ - Background job to refresh the Strava authentication token. - - This job is scheduled to run at regular intervals, and its purpose is to refresh the authentication token - used for interacting with the Strava API. It calls the `refresh_strava_token` function from the stravaController module. - - Parameters: - - None. - - Raises: - - Exception: If an unexpected error occurs during the token refresh process. - - Returns: - - None. - """ - try: - stravaController.refresh_strava_token(db_session=get_db_session()) - except Exception as err: - logger.error(f"Error in refresh_strava_token_job: {err}", exc_info=True) - - -def get_strava_activities_job(): - """ - Background job to fetch Strava activities. - - This job is scheduled to run at regular intervals, and its purpose is to retrieve Strava activities - from the past 24 hours. It calls the `get_strava_activities` function from the stravaController module. - - Parameters: - - None. - - Raises: - - Exception: If an unexpected error occurs during the Strava activities retrieval process. - - Returns: - - None. - """ - try: - stravaController.get_strava_activities( - (datetime.utcnow() - timedelta(days=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), - db_session=get_db_session(), - ) - except Exception as err: - logger.error(f"Error in get_strava_activities_job: {err}", exc_info=True) - - -# Check if Jaeger tracing is enabled using the 'JAEGER_ENABLED' environment variable -if os.environ.get("JAEGER_ENABLED") == "true": - # Configure OpenTelemetry with a specified service name - trace.set_tracer_provider( - TracerProvider(resource=Resource.create({"service.name": "backend_api"})) - ) - trace.get_tracer_provider().add_span_processor( - BatchSpanProcessor( - OTLPSpanExporter( - endpoint=os.environ.get("JAEGER_PROTOCOL") - + "://" - + os.environ.get("JAEGER_HOST") - + ":" - + os.environ.get("JAGGER_PORT") - ) - ) - ) - - -# Instrument FastAPI app -FastAPIInstrumentor.instrument_app(app) - # Router files -app.include_router(sessionController.router) -app.include_router(userController.router) -app.include_router(gearController.router) -app.include_router(activityController.router) -app.include_router(activity_streamsController.router) -app.include_router(followerController.router) -app.include_router(stravaController.router) - -# Create a background scheduler instance -scheduler = BackgroundScheduler() -scheduler.start() - - -# Job to remove expired tokens every 5 minutes -scheduler.add_job(remove_expired_tokens_job, "interval", minutes=5) - -# Job to refresh the Strava token every 30 minutes -scheduler.add_job(refresh_strava_token_job, "interval", minutes=30) - -# Job to get Strava activities every hour -scheduler.add_job(get_strava_activities_job, "interval", minutes=60) - -# Register the startup event handler -app.add_event_handler("startup", startup_event) - -# Register the shutdown event handler -app.add_event_handler("shutdown", shutdown_event) +app.include_router(session_router.router) +app.include_router(users_router.router) +app.include_router(activities_router.router) \ No newline at end of file diff --git a/backend/db/db.py b/backend/models.py similarity index 66% rename from backend/db/db.py rename to backend/models.py index 7659106c..f8d92265 100644 --- a/backend/db/db.py +++ b/backend/models.py @@ -1,37 +1,4 @@ -""" -Module: db.py - -This module defines the SQLAlchemy data models and database logic for managing users, followers, -access tokens, gear, and activities. It establishes the database connection, creates tables, -and provides a context manager for handling database sessions. - -Classes: -- Follower -- User -- AccessToken -- Gear -- Activity - -Functions: -- create_database_tables: Creates the necessary database tables. -- get_db_session: Context manager for obtaining a database session. - -Usage: -1. Import this module: `from my_database_logic import create_database_tables, get_db_session` -2. Create tables: `create_database_tables()` -3. Use the context manager for database sessions: - with get_db_session() as session: - # Perform database operations using the session -Note: Ensure that environment variables for database configuration are properly set before using -this module. -""" -import os -import logging -import hashlib -from sqlalchemy.orm import sessionmaker, relationship -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import ( - create_engine, Column, Integer, String, @@ -42,34 +9,9 @@ BigInteger, Boolean, ) +from sqlalchemy.orm import relationship from sqlalchemy.dialects.mysql import JSON -from sqlalchemy.engine.url import URL -from contextlib import contextmanager -from sqlalchemy.orm.exc import NoResultFound - -logger = logging.getLogger("myLogger") - -# Define the database connection URL using environment variables -db_url = URL.create( - drivername="mysql", - username=os.environ.get("DB_USER"), - password=os.environ.get("DB_PASSWORD"), - host=os.environ.get("DB_HOST"), - port=os.environ.get("DB_PORT"), - database=os.environ.get("DB_DATABASE"), -) - -# Create the SQLAlchemy engine -engine = create_engine( - db_url, pool_size=10, max_overflow=20, pool_timeout=180, pool_recycle=3600 -) - -# Create a session factory -Session = sessionmaker(bind=engine) - -# Create a base class for declarative models -Base = declarative_base() - +from database import Base # Data model for followers table using SQLAlchemy's ORM class Follower(Base): @@ -144,7 +86,7 @@ class User(Base): Integer, nullable=False, comment="User gender (one digit)(1 - male, 2 - female)" ) access_type = Column( - Integer, nullable=False, comment="User type (one digit)(1 - student, 2 - admin)" + Integer, nullable=False, comment="User type (one digit)(1 - user, 2 - admin)" ) photo_path = Column(String(length=250), nullable=True, comment="User photo path") photo_path_aux = Column( @@ -153,19 +95,31 @@ class User(Base): is_active = Column( Integer, nullable=False, comment="Is user active (2 - not active, 1 - active)" ) - strava_state = Column(String(length=45), nullable=True) - strava_token = Column(String(length=250), nullable=True) - strava_refresh_token = Column(String(length=250), nullable=True) - strava_token_expires_at = Column(DateTime, nullable=True) + # Define a relationship to UserIntegrations model + users_integrations = relationship( + "UserIntegrations", + back_populates="user", + cascade="all, delete-orphan", + ) # Define a relationship to AccessToken model - access_tokens = relationship("AccessToken", back_populates="user") + access_tokens = relationship( + "AccessToken", + back_populates="user", + cascade="all, delete-orphan", + ) # Define a relationship to Gear model - gear = relationship("Gear", back_populates="user") + gear = relationship( + "Gear", + back_populates="user", + cascade="all, delete-orphan", + ) # Establish a one-to-many relationship with 'activities' - activities = relationship("Activity", back_populates="user") - # Establish a one-to-many relationship between User and UserSettings - # user_settings = relationship("UserSettings", back_populates="user") + activities = relationship( + "Activity", + back_populates="user", + cascade="all, delete-orphan", + ) # Establish a one-to-many relationship between User and Followers followers = relationship( @@ -183,6 +137,32 @@ class User(Base): ) +class UserIntegrations(Base): + __tablename__ = "users_integrations" + + id = Column(Integer, primary_key=True) + user_id = Column( + Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + index=True, + comment="User ID that the token belongs", + ) + strava_state = Column(String(length=45), nullable=True) + strava_token = Column(String(length=250), nullable=True) + strava_refresh_token = Column(String(length=250), nullable=True) + strava_token_expires_at = Column(DateTime, nullable=True) + strava_sync_gear = Column( + Boolean, + nullable=False, + default=False, + comment="Whether Strava gear is to be synced", + ) + + # Define a relationship to the User model + user = relationship("User", back_populates="users_integrations") + + # Data model for access_tokens table using SQLAlchemy's ORM class AccessToken(Base): __tablename__ = "access_tokens" @@ -232,27 +212,12 @@ class Gear(Base): is_active = Column( Integer, nullable=False, comment="Is gear active (0 - not active, 1 - active)" ) + strava_gear_id = Column(BigInteger, nullable=True, comment="Strava gear ID") # Define a relationship to the User model user = relationship("User", back_populates="gear") # Establish a one-to-many relationship with 'activities' activities = relationship("Activity", back_populates="gear") - # Establish a one-to-many relationship between Gear and UserSettings - - -# user_settings = relationship("UserSettings", back_populates="gear") - -# class UserSettings(Base): -# __tablename__ = 'user_settings' - -# id = Column(Integer, primary_key=True, autoincrement=True) -# user_id = Column(Integer, ForeignKey('users.id'), nullable=False, doc='User ID that the activity belongs') -# activity_type = Column(Integer, nullable=True, doc='Gear type') -# gear_id = Column(Integer, ForeignKey('gear.id'), nullable=True, doc='Gear ID associated with this activity') - -# # Define the foreign key relationships -# user = relationship("User", back_populates="user_settings") -# gear = relationship("Gear", back_populates="user_settings") # Data model for activities table using SQLAlchemy's ORM @@ -307,6 +272,11 @@ class Activity(Base): comment="Average speed seconds per meter (s/m)", ) average_power = Column(Integer, nullable=False, comment="Average power (watts)") + calories = Column( + Integer, + nullable=True, + comment="The number of kilocalories consumed during this activity", + ) visibility = Column( Integer, nullable=False, @@ -330,7 +300,11 @@ class Activity(Base): gear = relationship("Gear", back_populates="activities") # Establish a one-to-many relationship with 'activities_streams' - activities_streams = relationship("ActivityStreams", back_populates="activity") + activities_streams = relationship( + "ActivityStreams", + back_populates="activity", + cascade="all, delete-orphan", + ) class ActivityStreams(Base): @@ -355,53 +329,4 @@ class ActivityStreams(Base): ) # Define a relationship to the User model - activity = relationship("Activity", back_populates="activities_streams") - - -def create_database_tables(): - # Create tables - Base.metadata.create_all(bind=engine) - - # Create a user and add it to the database - with get_db_session() as session: - try: - # Check if the user already exists - session.query(User).filter_by(username="admin").one() - print("Admin user already exists. Will skip user creation.") - logger.info("Admin user already exists. Will skip user creation.") - except NoResultFound: - # Create a new SHA-256 hash object - sha256_hash = hashlib.sha256() - - # Update the hash object with the bytes of the input string - sha256_hash.update("admin".encode("utf-8")) - - # Get the hexadecimal representation of the hash - hashed_string = sha256_hash.hexdigest() - - # If the user doesn't exist, create a new user - new_user = User( - name="Administrator", - username="admin", - email="admin@example.com", - password=hashed_string, - preferred_language="en", - gender=1, - access_type=2, - is_active=1, - ) - - # Add the new user to the session - session.add(new_user) - session.commit() - - print("Admin user created successfully.") - - -@contextmanager -def get_db_session(): - session = Session() - try: - yield session - finally: - session.close() + activity = relationship("Activity", back_populates="activities_streams") \ No newline at end of file diff --git a/backend/logs/__init__.py b/backend/routers/__init__.py similarity index 100% rename from backend/logs/__init__.py rename to backend/routers/__init__.py diff --git a/backend/routers/activities.py b/backend/routers/activities.py new file mode 100644 index 00000000..8650de54 --- /dev/null +++ b/backend/routers/activities.py @@ -0,0 +1,343 @@ +import os +import logging +import calendar + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile +from datetime import datetime, timedelta +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from schemas import ( + activities as activities_schema, + access_tokens as access_tokens_schema, +) +from crud import activities as activities_crud +from dependencies import get_db + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +@router.get( + "/activities/{user_id}/week/{week_number}", + response_model=list[activities_schema.Activity], + tags=["activities"], +) +async def read_activities_useractivities_week( + user_id: int, + week_number: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the activities for the requested week for the user or the users that the user is following""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Check if week number is higher or equal than 0 + if not (int(week_number) >= 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid week number", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Calculate the start of the requested week + today = datetime.utcnow().date() + start_of_week = today - timedelta(days=(today.weekday() + 7 * week_number)) + end_of_week = start_of_week + timedelta(days=7) + + if user_id == access_tokens_schema.get_token_user_id(token): + # Get all user activities for the requested week if the user is the owner of the token + activities = activities_crud.get_user_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + else: + # Get user following activities for the requested week if the user is not the owner of the token + activities = activities_crud.get_user_following_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + + # Return the activities + return activities + + +@router.get( + "/activities/{user_id}/thisweek/distances", + response_model=activities_schema.ActivityDistances | None, + tags=["activities"], +) +async def read_activities_useractivities_thisweek_distances( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the distances of the activities for the requested week for the user or the users that the user is following""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Calculate the start of the current week + today = datetime.utcnow().date() + start_of_week = today - timedelta( + days=today.weekday() + ) # Monday is the first day of the week, which is denoted by 0 + end_of_week = start_of_week + timedelta(days=7) + + if user_id == access_tokens_schema.get_token_user_id(token): + # Get all user activities for the requested week if the user is the owner of the token + activities = activities_crud.get_user_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + else: + # Get user following activities for the requested week if the user is not the owner of the token + activities = activities_crud.get_user_following_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + + # Check if activities is None + if activities is None: + # Return None if activities is None + return None + + # Return the activities distances for this week + return activities_schema.calculate_activity_distances(activities) + + +@router.get( + "/activities/{user_id}/thismonth/distances", + response_model=activities_schema.ActivityDistances | None, + tags=["activities"], +) +async def read_activities_useractivities_thismonth_distances( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the distances of the activities for the requested month for the user or the users that the user is following""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Calculate the start of the current month + today = datetime.utcnow().date() + start_of_month = today.replace(day=1) + end_of_month = start_of_month.replace( + day=calendar.monthrange(today.year, today.month)[1] + ) + + if user_id == access_tokens_schema.get_token_user_id(token): + # Get all user activities for the requested month if the user is the owner of the token + activities = activities_crud.get_user_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + else: + # Get user following activities for the requested month if the user is not the owner of the token + activities = activities_crud.get_user_following_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + + if activities is None: + # Return None if activities is None + return None + + # Return the activities distances for this month + return activities_schema.calculate_activity_distances(activities) + + +@router.get( + "/activities/{user_id}/thismonth/number", + response_model=int, + tags=["activities"], +) +async def read_activities_useractivities_thismonth_number( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the number of activities for the requested month for the user or the users that the user is following""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Calculate the start of the current month + today = datetime.utcnow().date() + start_of_month = today.replace(day=1) + end_of_month = start_of_month.replace( + day=calendar.monthrange(today.year, today.month)[1] + ) + + if user_id == access_tokens_schema.get_token_user_id(token): + # Get all user activities for the requested month if the user is the owner of the token + activities = activities_crud.get_user_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + else: + # Get user following activities for the requested month if the user is not the owner of the token + activities = activities_crud.get_user_following_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + + return len(activities) + + +@router.get( + "/activities/{user_id}/number", + response_model=int, + tags=["activities"], +) +async def read_activities_useractivities_number( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Get the number of activities for the user + activities = activities_crud.get_user_activities(user_id, db) + + # Check if activities is None and return 0 if it is + if activities is None: + return 0 + + # Return the number of activities + return len(activities) + + +@router.get( + "/activities/{user_id}/page_number/{page_number}/num_records/{num_records}", + response_model=list[activities_schema.Activity] | None, + tags=["activities"], +) +async def read_activities_useractivities_pagination( + user_id: int, + page_number: int, + num_records: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the activities for the user with pagination""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Check if page_number higher than 0 + if not (int(page_number) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid Page Number", + ) + + # Check if num_records higher than 0 + if not (int(num_records) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid Number of Records", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Get the activities for the user with pagination + activities = activities_crud.get_user_activities_with_pagination(user_id, db, page_number, num_records) + + # Check if activities is None and return None if it is + if activities is None: + return None + + # Return activities + return activities + +@router.post("/activities/{user_id}/create/upload") +async def create_activity_with_uploaded_file( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + file: UploadFile = File(...), + db: Session = Depends(get_db), +): + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + try: + # Ensure the 'uploads' directory exists + upload_dir = "uploads" + os.makedirs(upload_dir, exist_ok=True) + + # Get file extension + _, file_extension = os.path.splitext(file.filename) + + # Save the uploaded file in the 'uploads' directory + with open(file.filename, "wb") as save_file: + save_file.write(file.file.read()) + + # Choose the appropriate parser based on file extension + if file_extension.lower() == ".gpx": + parsed_info = parse_gpx_file(file.filename, user_id) + elif file_extension.lower() == ".fit": + parsed_info = parse_fit_file(file.filename, user_id) + else: + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, + detail="File extension not supported. Supported file extensions are .gpx and .fit", + ) + + except Exception as err: + # Log the exception + logger.error(f"Error in create_activity_with_uploaded_file: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + \ No newline at end of file diff --git a/backend/routers/session.py b/backend/routers/session.py new file mode 100644 index 00000000..c92f6851 --- /dev/null +++ b/backend/routers/session.py @@ -0,0 +1,114 @@ +import logging + +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.orm import Session + +from crud import users as users_crud, user_integrations as user_integrations_crud +from schemas import access_tokens as access_tokens_schema, users as users_schema +from constants import ( + USER_NOT_ACTIVE, +) +from dependencies import get_db + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def authenticate_user(username: str, password: str, db: Session): + """Get the user from the database and verify the password""" + # Get the user from the database + user = users_crud.authenticate_user(username, password, db) + + # Check if the user exists and if the password is correct and if not return False + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Return the user if the password is correct + return user + + +def get_current_user(db: Session, user_id: int): + """Get the current user from the token and then queries the database to get the user data""" + # Get the user from the database + user = users_crud.get_user_by_id(user_id, db) + + # If the user does not exist raise the exception + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials (user not found)", + headers={"WWW-Authenticate": "Bearer"}, + ) + + user_integrations = user_integrations_crud.get_user_integrations_by_user_id(user.id, db) + + if user_integrations is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials (user integrations not found)", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if user_integrations.strava_token is None: + user.is_strava_linked = 0 + else: + user.is_strava_linked = 1 + + # Return the user + return user + + +@router.post("/token", tags=["session"]) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + do_not_expire: bool = False, + db: Session = Depends(get_db), +) -> access_tokens_schema.AccessToken: + user = authenticate_user(form_data.username, form_data.password, db) + + if user.is_active == USER_NOT_ACTIVE: + raise HTTPException( + status_code=status.HTTP_400_UNAUTHORIZED, + detail="Inactive user", + headers={"WWW-Authenticate": "Bearer"}, + ) + + expire = None + if do_not_expire: + expire = datetime.utcnow() + timedelta(days=90) + + access_token = access_tokens_schema.create_access_token( + db, + data={"sub": user.username, "id": user.id, "access_type": user.access_type}, + expires_delta=expire, + ) + + return access_tokens_schema.AccessToken(access_token=access_token, token_type="bearer") + + +@router.get("/users/me", response_model=users_schema.UserMe, tags=["session"]) +async def read_users_me( + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + # Validate the token + access_tokens_schema.validate_token_expiration(db, token) + + # Get the user id from the payload + user_id = access_tokens_schema.get_token_user_id(token) + + return get_current_user(db, user_id) diff --git a/backend/routers/users.py b/backend/routers/users.py new file mode 100644 index 00000000..3eeae961 --- /dev/null +++ b/backend/routers/users.py @@ -0,0 +1,317 @@ +import logging + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from schemas import access_tokens as access_tokens_schema, users as users_schema +from crud import users as users_crud, user_integrations as user_integrations_crud +from dependencies import get_db + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +@router.get("/users/number", response_model=int, tags=["users"]) +async def read_users_number( + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the number of users in the database""" + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Get the number of users in the database + return users_crud.get_users_number(db) + + +@router.get( + "/users/all/page_number/{page_number}/num_records/{num_records}", + response_model=list[users_schema.User], + tags=["users"], +) +async def read_users_all_pagination( + page_number: int, + num_records: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the users from the database with pagination""" + # Check if page_number higher than 0 + if not (int(page_number) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid Page Number", + ) + + # Check if num_records higher than 0 + if not (int(num_records) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid Number of Records", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Get the users from the database with pagination + return users_crud.get_users_with_pagination( + db=db, page_number=page_number, num_records=num_records + ) + + +@router.get( + "/users/username/{username}", response_model=list[users_schema.User], tags=["users"] +) +async def read_users_username( + username: str, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the users from the database by username""" + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Get the users from the database by username + return users_crud.get_user_by_username(username=username, db=db) + + +@router.get("/users/id/{user_id}", response_model=users_schema.User, tags=["users"]) +async def read_users_id( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the users from the database by id""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Get the users from the database by id + return users_crud.get_user_by_id(user_id=user_id, db=db) + + +@router.get("/users/{username}/id", response_model=int, tags=["users"]) +async def read_users_username_id( + username: str, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the users from the database by username and return the user id""" + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Get the users from the database by username + return users_crud.get_user_id_by_username(username, db) + + +@router.get("/users/{user_id}/photo_path", response_model=str | None, tags=["users"]) +async def read_users_id_photo_path( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the photo_path from the database by id""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Get the photo_path from the database by id + return users_crud.get_user_photo_path_by_id(user_id, db) + + +@router.get( + "/users/{user_id}/photo_path_aux", response_model=str | None, tags=["users"] +) +async def read_users_id_photo_path_aux( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Get the photo_path_aux from the database by id""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Get the photo_path_aux from the database by id + return users_crud.get_user_photo_path_aux_by_id(user_id, db) + + +@router.post("/users/create", response_model=int, status_code=201, tags=["users"]) +async def create_user( + user: users_schema.UserCreate, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Create a new user in the database""" + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Create the user in the database + created_user = users_crud.create_user(user, db) + + # Create the user integrations in the database + user_integrations_crud.create_user_integrations(created_user.id, db) + + # Return the user id + return created_user.id + + +@router.put("/users/edit", tags=["users"]) +async def edit_user( + user_attributtes: users_schema.User, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Edit a user in the database""" + # Check if user_id higher than 0 + if not (int(user_attributtes.id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Update the user in the database + users_crud.edit_user(user_attributtes, db) + + # Return success message + return {"detail": f"User ID {user_attributtes.id} updated successfully"} + + +@router.put("/users/edit/password", tags=["users"]) +async def edit_user_password( + user_attributtes: users_schema.UserEditPassword, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Edit a user password in the database""" + # Check if user_id higher than 0 + if not (int(user_attributtes.id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if token id is different of user id. If yes checks if the token has admin access + if user_attributtes.id != access_tokens_schema.get_token_user_id(token): + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Update the user password in the database + users_crud.edit_user_password(user_attributtes.id, user_attributtes.password, db) + + # Return success message + return {"detail": f"User ID {user_attributtes.id} password updated successfully"} + + +@router.put("/users/{user_id}/delete-photo", tags=["users"]) +async def delete_user_photo( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + """Delete a user photo in the database""" + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if token id is different of user id. If yes checks if the token has admin access + if user_id != access_tokens_schema.get_token_user_id(token): + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Update the user photo_path in the database + users_crud.delete_user_photo(user_id, db) + + # Return success message + return {"detail": f"User ID {user_id} photo deleted successfully"} + +@router.delete("/users/{user_id}/delete", tags=["users"]) +async def delete_user( + user_id: int, + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_db), +): + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Validate the token expiration + access_tokens_schema.validate_token_expiration(db, token) + + # Check if the token has admin access + access_tokens_schema.validate_token_admin_access(token) + + # Delete the user in the database + users_crud.delete_user(user_id, db) + + # Return success message + return {"detail": f"User ID {user_id} deleted successfully"} \ No newline at end of file diff --git a/backend/schemas/__init__.py b/backend/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/schemas/access_tokens.py b/backend/schemas/access_tokens.py new file mode 100644 index 00000000..fe3b0176 --- /dev/null +++ b/backend/schemas/access_tokens.py @@ -0,0 +1,201 @@ +import logging + +from pydantic import BaseModel +from datetime import datetime, timedelta, timezone +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from sqlalchemy.orm import Session + +from crud import access_tokens as access_tokens_crud +from constants import ( + JWT_EXPIRATION_IN_MINUTES, + JWT_ALGORITHM, + JWT_SECRET_KEY, + ADMIN_ACCESS, +) + + +class AccessToken(BaseModel): + """Access token schema""" + + access_token: str + token_type: str + + +class Token(BaseModel): + """Token schema""" + + user_id: int | None = None + expires_at: str | None = None + + +class TokenData(Token): + """Token data schema""" + + access_type: int | None = None + + +class CreateToken(Token): + """Create token schema""" + + token: str + + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def decode_token(token: str = Depends(oauth2_scheme)): + """Decode the token and return the payload""" + return jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) + + +def validate_token_expiration(db: Session, token: str = Depends(oauth2_scheme)): + """Validate the token and check if it is expired""" + # Try to decode the token and check if it is expired + try: + # Decode the token + payload = decode_token(token) + + # Get the expiration timestamp from the payload + expiration_timestamp = payload.get("exp") + + # If the expiration timestamp is None or if it is less than the current time raise an exception and log it + if ( + expiration_timestamp is None + or datetime.utcfromtimestamp(expiration_timestamp) < datetime.utcnow() + ): + logger.warning( + "Token expired | Will force remove_expired_tokens to run | Returning 401 response" + ) + remove_expired_tokens(db) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token no longer valid", + headers={"WWW-Authenticate": "Bearer"}, + ) + + except jwt.ExpiredSignatureError: + # Log the error and raise the exception + logger.info( + "Token expired during validation | Will force remove_expired_tokens to run | Returning 401 response" + ) + remove_expired_tokens(db) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token no longer valid", + headers={"WWW-Authenticate": "Bearer"}, + ) + except JWTError as err: + # Log the error and raise the exception + logger.error( + f"Error in validate_token_expiration on payload validation: {err}", + exc_info=True, + ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_token_user_id(token: str = Depends(oauth2_scheme)): + """Get the user id from the token""" + # Decode the token + payload = decode_token(token) + + # Get the user id from the payload + user_id = payload.get("id") + + if user_id is None: + # If the user id is None raise an HTTPException with a 401 Unauthorized status code + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Return the user id + return user_id + + +def get_token_access_type(token: str = Depends(oauth2_scheme)): + """Get the admin access from the token""" + # Decode the token + payload = decode_token(token) + + # Get the admin access from the payload + access_type = payload.get("access_type") + + if access_type is None: + # If the access type is None raise an HTTPException with a 401 Unauthorized status code + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Return the access type + return access_type + + +def validate_token_admin_access(token: str = Depends(oauth2_scheme)): + if get_token_access_type(token) != ADMIN_ACCESS: + # Raise an HTTPException with a 403 Forbidden status code + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Unauthorized Access - Admin Access Required", + ) + + +def create_access_token( + db: Session, data: dict, expires_delta: timedelta | None = None +): + """Creates a new JWT token with the provided data and expiration time""" + # Create a copy of the data to encode + to_encode = data.copy() + + # If an expiration time is provided, calculate the expiration time + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta( + minutes=JWT_EXPIRATION_IN_MINUTES + ) + + # Add the expiration time to the data to encode + to_encode.update({"exp": expire}) + + # Encode the data and return the token + encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + + # Save the token in the database + db_access_token = access_tokens_crud.create_access_token( + CreateToken( + token=encoded_jwt, + user_id=data.get("id"), + expires_at=expire.strftime("%Y-%m-%dT%H:%M:%S"), + ), + db, + ) + if db_access_token: + # Return the token + return encoded_jwt + else: + # If the token could not be saved in the database return None + return None + + +def remove_expired_tokens(db: Session): + """Remove expired tokens from the database""" + # Calculate the expiration time + expiration_time = datetime.utcnow() - timedelta(minutes=JWT_EXPIRATION_IN_MINUTES) + + # Delete the expired tokens from the database + rows_deleted = access_tokens_crud.delete_access_tokens(expiration_time, db) + + # Log the number of tokens deleted + logger.info(f"{rows_deleted} access tokens deleted from the database") diff --git a/backend/schemas/activities.py b/backend/schemas/activities.py new file mode 100644 index 00000000..a4aad338 --- /dev/null +++ b/backend/schemas/activities.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel + + +class Activity(BaseModel): + id: int | None = None + distance: int + name: str + activity_type: str + start_time: str + end_time: str + city: str | None = None + town: str | None = None + country: str | None = None + elevation_gain: int + elevation_loss: int + pace: float + average_speed: float + average_power: int + calories: int | None = None + strava_gear_id: int | None = None + strava_activity_id: int | None = None + + class Config: + orm_mode = True + + +class ActivityDistances(BaseModel): + swim: float + bike: float + run: float + + +def calculate_activity_distances(activities: list[Activity]): + """Calculate the distances of the activities for each type of activity (run, bike, swim)""" + # Initialize the distances + run = bike = swim = 0.0 + + # Calculate the distances + for activity in activities: + if activity.activity_type in [1, 2, 3]: + run += activity.distance + elif activity.activity_type in [4, 5, 6, 7, 8]: + bike += activity.distance + elif activity.activity_type == 9: + swim += activity.distance + + # Return the distances + return ActivityDistances(run=run, bike=bike, swim=swim) diff --git a/backend/schemas/user_integrations.py b/backend/schemas/user_integrations.py new file mode 100644 index 00000000..b52d8cd1 --- /dev/null +++ b/backend/schemas/user_integrations.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + + +class UserIntegrations(BaseModel): + id: int + user_id: int + strava_state: str | None = None + strava_token: str | None = None + strava_refresh_token: str | None = None + strava_token_expires_at: str | None = None + strava_sync_gear: bool + + class Config: + orm_mode = True diff --git a/backend/schemas/users.py b/backend/schemas/users.py new file mode 100644 index 00000000..6c10b0ce --- /dev/null +++ b/backend/schemas/users.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel + + +class User(BaseModel): + id: int | None = None + name: str + username: str + email: str + city: str | None = None + birthdate: str | None = None + preferred_language: str + gender: int + access_type: int + photo_path: str | None = None + photo_path_aux: str | None = None + is_active: int + + class Config: + orm_mode = True + + +class UserCreate(User): + password: str + + +class UserMe(User): + id: int + is_strava_linked: int | None = None + +class UserEditPassword(BaseModel): + id: int + password: str \ No newline at end of file diff --git a/frontend/activities/activity.php b/frontend/activities/activity.php index 8c6b5ae1..729d34da 100755 --- a/frontend/activities/activity.php +++ b/frontend/activities/activity.php @@ -669,13 +669,13 @@ className: 'bg-danger dot' var ctx = document.getElementById('dataChart').getContext('2d'); var activityType = ; - const downsampledDataHr = downsampleData(, 200); + const downsampledDataHr = downsampleData(, 200); - const downsampledDataCad = downsampleData(, 200); + const downsampledDataCad = downsampleData(, 200); - const downsampledDataPower = downsampleData(, 200); + const downsampledDataPower = downsampleData(, 200); - const downsampledDataEle = downsampleData(, 200); + const downsampledDataEle = downsampleData(, 200); if (activityType === 4 || activityType === 5 || activityType === 6 || activityType === 7 || activityType === 8) { const downsampledDataVel = downsampleData(, 200); diff --git a/frontend/gear/gear.php b/frontend/gear/gear.php index b5239ea3..16e336c6 100755 --- a/frontend/gear/gear.php +++ b/frontend/gear/gear.php @@ -161,10 +161,16 @@ class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-em + + + + + - " href="#" role="button" data-bs-toggle="modal" data-bs-target="#editGearModal"> @@ -262,9 +268,7 @@ class="ms-2 badge bg-primary-subtle border border-primary-subtle text-primary-em - " href="#" role="button" data-bs-toggle="modal" + " href="#" role="button" data-bs-toggle="modal" data-bs-target="#deleteGearModal" > diff --git a/frontend/img/strava/api_logo_cptblWith_strava_stack_light.svg b/frontend/img/strava/api_logo_cptblWith_strava_stack_light.svg new file mode 100644 index 00000000..71dd3f71 --- /dev/null +++ b/frontend/img/strava/api_logo_cptblWith_strava_stack_light.svg @@ -0,0 +1 @@ +api_logo \ No newline at end of file diff --git a/frontend/img/strava/btn_strava_connectwith_orange.png b/frontend/img/strava/btn_strava_connectwith_orange.png index ae187150758706504300f5f20e38e1193d6782eb..f8132f134adf5b3fee8fd0cfb5075094938f71e1 100755 GIT binary patch literal 3509 zcmbuC`9Bkm$p@A zvA>`XvNeLy)e?lZ{+6DY80f=8>3=zeb=psXqn1$QnK1ziQv=3KQx&*vb#Zb|xe7Mp z@)z~9hsi+s=Vlw%XG^w=vj0?MgtZ#LuE7{pfLz)>VN1vmFORAwuQJfaom3z|2R zG9H?wqr3LK=LFfA=HGV={xl< z>o6R}_b{ipt9up+YgR6Q4*v^(rFWWYXo?y|=|LV1Qky~K_0C3Z6OTpE<*5zlBQ zOu<%c{$qt}F?I65+vE-yEhSeH#L|+94*%~YW1s7G(bg`13^cAZ7EfltbUB7^PM$9MXSKKVog z>?7GucPzF()|FC1lqtK<>e3T?UPfXHHo6I2U!%?GL;Q=W0o-?KfU@76o>s{H^q$Qp zpg;1;b^)+)tU8)chF+0IKXb->LNK4}fH2wPYkF4U(Af?u%V`suuSr&>bfhPRH&`^a zFG@X@>og;v5#MS}qq`E#dP*}pO!XD4SRv0MshxDR)puW_{T2sV37w3SmG|n~^I#9F z(izFWl2?9C3>;5ML32OA*U+t@7BBdR9W$^7(zr|S9SG3H2SG>0#ykINM^MuYWYzxEJ(HKCqFH@?`10mE$~g*Ad438-laZZ20&WimgS6(if1FmRgoAbs<~N2;+1FZLQeixAA-|rzA}S2Yd7u;mp0Z0`vPiJUz1`QD zhzH-Wz_wi2IpV*eCt=5iMeRcg1Q%(@%!Ah}%Wr4Q({LUIH_#R$SZJCi-1Qvnfqn zn+F|+>tgETrpSh4etUj_ODI!Yp02QdccW`d4PU$XUVq$_AG%O`LD76mg@-<&(tpFEeDKCk;B4;B zM^|=NmuLklIPgbUx!cOx^nm-k5UXkcv~M<^v@Cc1eOD7|U;Hk@g;dEuUau2S`YyXw z0$7BWBmLlgJF@OHEvvM-Al$gH$mz2>$n3^rUTIfFr&+-xW3Voflxo}nJ1}c6T~UZd zty&u2v_#mUM6deW*v=A40izN8k3#6;9bhNIBJ!_*8*F+;&lm2Y=P0(4?!2JF(nz75 z4ZhmD@Go+}4Roz?m0FTkx2kotXYz5}O9|iR73TtdE@Yxw;_-(`l=09@vN2u}38B&l znG55Ha?RVfzU>D(mDAu;-qN%rt=H<9DNbCQ){nygSJ7XW0vM3gE`bfKv~s^}hPM{; zxcNnsQ00X^sZtP$>CiFP3ke!*?8lzvNISg^i+GkzICITcO2p+#n3%B(4M&|ebaQb+gVN##c>aUV`HsTZOg850ahImE37HN_mQOfHpJs@8v0eNi7w7IlB zXfq11a72&NAkN_E0XOP{8IYYzx3mcde->#$Vc4h7k#WuILUm&G=NB9fZRX+6*nQ7d zqn<%-Oo?oVt+AMtO?tk-LC^9l8G%>Gnqdii4>c>x&oVS?wSJgh3lY4S%NM0nlh+@6 ztcSI?x~)k0fMFC(;20%@Nj<(YovG6fwP?eoOm3PRJFttt7KDKeO%Ttx7GsR6;(raP zJ_z^lU4j{2?dDjDJ42x!$&uE})&H5jlz_Me%KVC#tnsVqKB`HzH1%lvb82cf#1-Tw z{!`i`vupQb{zHB*Lw4Cw;ONR;F|T>a?N#FavH@_8ODg z{=F{%OSGjQA^GjSDM$0)@k0Ew;WK~xcL$)#zQ;l~VOm8^PWaHNnaP{1k*KnKeWqQ= zs)*A4KE}|VlizU7&jX(2ObZd5Nv+V!4UTBL5+QTm_3yup)J@&rfwSq%+$4$Xj)bH$ zif{gmsHfc#f!2FHTgSBTAq@W^D}}WUcyhO$hxXYWQZ*ws74o9`$6j89_zFXV9^~1% zScOGaS3Tu(8pgDJ@MUhEQiYzC?^UbnrDxtFJYV}VexL7}+}-_1leE%@*wOoO8xJ-Yr_F5Yl9Vjq>70I`IMT$?&6@#U~MoVu)d*mR~Zmh zH|0iq?O^&u8_P!Dsv1WA!06n$^Oisc*kp6kVRb$Y0ZXPW~)7-`YmS zP@08`+2s}+LmS^kemE$8>gU7{%Xc`qrv09_zGc$(?%mAA+2$?EH{qO}lAfD^AzArn zZ=L3>Zre~3pz6JQBTfYh1;z)7f)ZNf$f4mo$mnyz(X6(>3FQ|Lg5zq)jXDRVp`m$! zK_A2OCY3=1t-?wJj+s9?1mh=Vxi$QHr6B*EgW9T}&U}QBS0gu3+#Hey3%8)o83lRK zs}hJ)>m0y(G#agz z+55+FudJCp7tqL^TnNL%K^pg^S4{-U)}FgkNK|<5Tp=N_wLgb*x&Bbhw_3K;s$MTT z3GBmar5sU?>`TcgpY7>Vmn@ORo4VNbe?UrABS#;w9;c#F*{4GWKK!o{7LvD9qqilL zo|jy+9*iIY*~USlJ(KE9;dFz+Zw)>S|BNk2jPKx*Z$@~fLT~k}`)IYmJvH-d(?te{ zUqzc0%`Mw&b!MaWip#7E-u5^2VIi$Ok23epA|{JbtF=<(D@y+E+U{jtXS0~ZO+$r7 z&d`;`szt{bO3))4(FFsa98Pc^YOe^p@Jn%z)WIN&NBH<^s>$~LS&En0w$ZUl=Q`eM{NMNk)bCQO*3udbCr14d zn4t!*H^{ZepNf%Oip811!>z+yeuv|2IItWi1Uo;Zdih#Nqf-~y7daRPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D79B}MK~#8N?VSnS zRnz*%pS6xLQ!>lkfFu0Z$!K`p4Nl`arb+6Qo3`HcN@+UWuxaC%g%&sAlIYp+- zLx>QW&R+lL_w2RLS$m(o_c`b7?d`ps@8@&aYn}I;cc1s{{j6vBJuCHldu^yfg$fn^ zKxq5_3Kc3;_`^^!Y!xb0_`^{#Y!xb0@X4%3>iB=y!7600jzrrvvxBNp$+19JW#7ufeDFHa!Z~E-Jwf#OyY__)zgBg<$_}cD_vev$`*~7_ooOeE!ME=6W&XK- z`vsXdpCwxIseP_|RNFI1opmF5JsJ^x{fV9B`3`-EHrd9G!kF1ph~9eMjuNP|KB>+F z`9S&O5QfaeONf5?$&UF?JMu-MFWw_>SSLGEKIw){DN_4)B6R@&g2sCit-p!?hx=$g z8GPB(t_z(|g+DP(|3aCekJ|xJ_QVbRo31J}JBTt@JSvBVZ(8r9jpevnX?+ao?$?sq zbXz-79P`@aR5pOs2#kdH&^XErnPq3n$9(n<89qnz@Hkl%N=9c$ z-@K62<~xu#^CmJkpJSh3GjMnh(!IynQ5e_(uiw6Q6p!k3G3mb!v7<0%&UEswIM$As zR_!SB&-=(2L*C!mYy0R8`$TyphoW0HB0XX*>3$PQv1_O{+?-S}>5q;l4fV2GXNmA=+w3J6jxe;3;C%+|^3mei@%zH5VuM&!;tgAa7Nm#ICe^T|orniUP7j<)#D=cI%0(U9 zmvooQCifX&Bvu9L$g@cw*WZq+hT7u@zUcv!gKP z#RrJqd)bbdmWPo-WN1exjv|Ip$qJ=AO&_ISY)S zI_aNwlmaYwqdok5-3FW3$tnOh87}Z9Tib~$07H!TsJEq^C8UxsG>Q{-KAkQ~RM2hWgv8!$^gC9i9I<%D+G-%lPk*-xx6KynZt z%$qov=#@ugE()qcL40mzPbTxozet~V8>w9nlsRC2`a#@ebMMt`g|l`x4?;L~=;?AS zs}8%r`rL7TZoq;BwYlq3GBa<)iP_m0kiR(XN}^Wn*H|=MzC+J4ZeT@CLRa`o^x}?)#G=uUAuhjO%13(PrDr zVitUE^WE1(-}A%bUCkHp2lM=W_r%{du3NRwv7wvOn>>un1U3Y#txkIK4|X(=#;Q}CvbBy+1{q#V?KCy@U8-F6hl{QM(%XYlv-{Stp- zjkQSMx`@<`w^76gfBu% zc3@kg*B-N@LSQiTxqLHQvb}dLsA7m=={ih8)+D@1L&UH|fu=^E=7$dE{nw=A4EH(? zGj}F=mmOtCdDQ7wk>+y%=Is~c22^ao-mBb8nmR$NIvHj5EgmeOIXoxBmVn4$)A91H}=ZE`dE$y zz7h3=&vG}h`FJ54r(NVY2cLUpjQ{JF{`;#U+*&ZBq?f_CogZT~7x_n8Q7NGCK$f zn3og?Hj#Y1ifok8BHGb!7Df6AWBH$>E^{pBRm zY*+%LCf?PnkNF8^$CcL{#zI4rhT_lLdHuh>+{7tot3BHj@#>m}YTQbU8oULR z?qTR;^|WaA2*~jUb086W(d3*_!?_y{&Ams*`^M4&9hd5Ot{pkBG0}sjhm4x}YtB27Y+qHlReieVBZ@S-9qgWrE8!wa5wb`xKgP{gR!ILrB-f@9!ZM+MYL2*39} zI@PEF)vkA_3U3}5<9pjNKo3X=eX$t0#fJz5xwTH=+*%YwD(Zyta++a(*%jfrt9cR} zN|;A(BZ4rH!>JI6(R<`qEwkWh4k;RP*woffFn8Ntj9h@~#__uiagu-`@YvJv1{@-x zly~m#vSJ75gZ^rdl7U<6b=y|LD6GC--+jd32B?GBqs+j;RIO2jOD&K~hOHQL=C`KbNR1GL>+<(vZ`K=duz09g+;Tw7yJoT{9RQ728d441tE1@K;3?}#OC*d| z$MQ{z^vqWu*`KV1AVf{#yLfLlnyimQ)nSkDC}*+e#lcJ)VF!hvLMlSvjpB3Humd#z zNjqY8YE7znN)FJX4(VD}PCe|gxnbVdB^b68MA#eTQ~0Lz_~v(WDw5|J#s5KDv;Xn)bBuhB_PokDsCH-DBYuzjZzA*9?RMm2p14yyzyQ_K|9#a?5b80? z_1HK}RjrTlk6)T4#vA13EUHO9FNgeVP2pYNo#@LC?TA6%?S>w9z~_aCAmUxdVID7D zA#{Nk2Wf{seVF8n^nfW^%Jn<$1u`2sk?Q4LzyK{^O4p_T}cYdGr2SVRhw zAjSm+y0w_1LmUd%Xc;n{u*asHA-mu!M6-O+L00OxkcmSI!{JfVGZk#6x_f-YFCxwm)JbI22k5UhRmed++*`Fu``R6Fb z!ao;TtYO>&mM>$6Vx%1ulBiq5!uqP8I~5wW;zhiK!k0zTRVAtzLWa$_3C$H$R=YPd zJgY>Uyjq{s2f9rx(#cmvh|2+j(Ryi#H~ zGiN%PF}-Cq6eGSeU{4d(YakaNDwQ}6!cg|C(c*&YTtLoib3Fp29->vfv}$lbDL)hC z*X7BtbFbHw;*<-jsNVgf?WlZUKvX|osGWtoCP7tKA9bGp;K)Ecq)fh06UDsCAwK$C zT|@}m$_`3H!cnK_W@g;rS7^(?yE|RCR*VASa0=nO*j)ijiD&tPsvaXr9dxQx8$%b@ zI!+myL%<|VAQu2uW>TwgTyRNbvj-FV*r>k!DQ0sgTyYb(DSw9=Ba~<35JvJ!el^+5 zWv>$nb`&=3f)nJ~T#b?BNvcAL#%HDCWyy@~L)m`&iDwF#N4daz(`Q%B^Q%cjDl?*s z9pyfT0=KB=g1%MRte^;C z!{$`YM9WqzpU`+O(jEGczF<6MM$ebLT8^Kc2I!O)ppbwsp&xCP;xuxL1Y*inMIYuq z$4&pmN;g0)XoWHhot+pEj_z@!B#41|GOuUKd2J98^M#~#cJga+hOIUjt#V)kkGwBr z1ky=^N_!bi<5Jq+0A0X^`J`3vX!@oN`4!d7ws|(>mLO`i zIXTIBy_2r3D~(lX2$54nrwjzoUo7%FX;5kJT#vG-mP4%V6j~B-J@ZEVv*1VEms!vM&3wb=YZI9Ji;K`6(D?? zUXEWCguIjOn0GA;_TF-Tt{fcg;wtlAw`fSZ|0EH#>g(o;mMfQ6 zh0%t-ancZ9PBhj@n8y%^oofZzZU>Jf!9H?#C@K=RUwSIOBSKO_$6R1X@q`T<1Ep~Q+&TDWE)>_YC{+h z?fy4UP0BaQqm`!DO(HAEfwq&gZXjj1M-fZ?--&Wut3Yq8-AE4?vhF?B?^$%&s6Oez z_mhSo87)VA^SD6H5ulFm?~52+uq_nCjTZBKzQUTrDLQC07GuGIoTl8KNrYRr3M?9> zKy>`%B>BHft~%8UbBM=bE57!W)DhB`UI8n>^n6PNRQ-C?tE75fPwLDOvR{;Tx(%>OhAN!3 z2ya~9oNyJIHfcjS;~G*&Um%?V^7jZ@eo#b+RH@+N2T55}BheQJO_Vz2QmL@%0r@JV zXvo}>xW!EqC~U?zW&vd;ewp6joPaCT1z4>^qDf0&*jx;UDpe&U5N2bEwyDe=pUbYT zD;@R_u+xRYjTJ$m-R+2;{QLyU41b0)H!q|NzGG)c;3(oXxvfn#iI_L6o-7 zTcKfN%*}nsdocV#WuwWxI>>GTu4V?+4)3PC0M*6ZF-YVC0cg|F=%ed+Zm9Q9TCA+V zwN`=^Y;>>O>VXvzUXwm!IO)TC?Fm$-hb z3-dIGNvm@AXrvU}B2@$!4{Y`jgpFxJSQsX9+TDctPbG@55oLw8FGtIqBXSQgNW(m1 zH*o;$35N=d4k9(0TG-JbUR#3PN46|?R0}S#hK;b(0UImq!4{~{qevtRVeV-V#3gXkk|9QF{F*`)(a&$k4E|$ z13RcB^rMywgu!Nfeq+W}g_$^*%oj0T1s1hM15#Z$Bux>*B9cf7B~!gN=*gVa5_W=- z!jc0D0Pcc`%{h~HJu$P!#7ArhW#_~%hSC`l*PG3h_K&%2x~=R#Cb*Hc~F$gn?ux z54EF0^wrOh+L?c63?8DGSx!4*kC7t(0_e*{e=nRQ0a4D_g|T8{6hvf)8mIUeVu$9G zxjw$hA0lD&k4FXkm4BtG!k>>;?L@g>&i3RGj6z#w#<8bTBo|zxhK*6@-9{P;hBA?; zs~bBSNnJgFJw4>u<1k--NM4V|`Dm-&mZWcePb%ox=)vuYR_EM8x=lAb3WI+jNA}fA z0r~x;RDy?;*T2KXl8Rxg@aF}&A|%+G?CbBo0tw1`Y;+(_N2tH-8(;Qo-pQ=l4YIAKiIpy?S - - - + + + \ No newline at end of file diff --git a/frontend/inc/Template-Top.php b/frontend/inc/Template-Top.php index 2c87b12a..34caa988 100755 --- a/frontend/inc/Template-Top.php +++ b/frontend/inc/Template-Top.php @@ -18,10 +18,8 @@ $translationsTemplateTop = include $_SERVER['DOCUMENT_ROOT'] . '/lang/inc/Template-Top/en.php'; } ?> - - @@ -42,7 +40,6 @@ - +
+ +
@@ -551,26 +548,8 @@ className: 'bg-danger dot' "Bike", "gear_gear_infoZone_gearisshoe" => "Shoes", "gear_gear_infoZone_geariswetsuit" => "Wetsuit", + "gear_gear_infoZone_strava_gear" => "Synced from Strava", // edit zone "gear_gear_infoZone_editbutton" => "Edit gear", "gear_gear_infoZone_modal_editGear_brandLabel" => "Brand", diff --git a/frontend/lang/login/en.php b/frontend/lang/login/en.php index 837aa532..923ff9bd 100755 --- a/frontend/lang/login/en.php +++ b/frontend/lang/login/en.php @@ -2,7 +2,10 @@ //login.php translations return [ "login_info_session expired" => "User session expired", - "login_error" => "Unable to login. Check username and password", + "login_error_access_token_not_set" => "Unable to login. Access token not set", + "login_error_user_inactive" => "User is inactive. Unable to login", + "login_error_incorrect_user_credentials" => "Incorrect user credentials. Check username and password", + "login_error_undefined" => "Unable to login. Undefined", "login_API_error_-1" => "Connection to API not possible. Failed to execute cURL request", "login_API_error_-2" => "API return code was not 200", "login_API_error_-3" => "User state is inactive. Unable to login", diff --git a/frontend/lang/settings/en.php b/frontend/lang/settings/en.php index 5e8d9bda..a26f9a89 100755 --- a/frontend/lang/settings/en.php +++ b/frontend/lang/settings/en.php @@ -4,239 +4,16 @@ return [ // title zone "settings_title" => "Settings", - "settings_sidebar_spaces" => "Spaces", - "settings_sidebar_rooms" => "Rooms", "settings_sidebar_users" => "Users", "settings_sidebar_global" => "Global settings", - "settings_sidebar_profileSettings" => "Profile settings", - "settings_sidebar_gearSettings" => "Gear", + "settings_sidebar_profileSettings" => "My profile", + "settings_sidebar_securitySettings" => "Security", + "settings_sidebar_integrationsSettings" => "Integrations", // error banner zone "settings_API_error_-1" => "Connection to API not possible. Failed to execute cURL request", "settings_API_error_-2" => "API return code was not 200", - "settings_sidebar_spaces_error_deletespace_-3" => "Cannot delete space due to existing dependencies", - "settings_sidebar_rooms_error_deleteroom_-3" => "Cannot delete room due to existing dependencies", - "settings_sidebar_spaces_error_addSpace_-3" => "Space name already exists. Please verify name", - "settings_sidebar_rooms_error_addRoom_-3" => "Room name already exists. Please verify name", - "settings_sidebar_spaces_error_editSpace_-3" => "Name is mandatory", - "settings_sidebar_spaces_error_addEditSpace_-4" => "Photo path invalid", - "settings_sidebar_spaces_error_addEditSpace_-5" => "Only photos with extension .jpg, .jpeg e .png are allowed", - "settings_sidebar_spaces_error_addEditSpace_-6" => "It was not possible to upload photo", - "settings_sidebar_spaces_error_deleteSpacePhoto_-3" => "It was not possible to delete space photo on filesystem. Database not updated", - "settings_sidebar_rooms_error_deleteRoomPhoto_-3" => "It was not possible to delete room photo on filesystem. Database not updated", - "settings_sidebar_profile_error_deleteProfilePhoto_-3" => "It was not possible to delete profile photo on filesystem. Database not updated", - "settings_sidebar_users_error_addUser_-3" => "Username already exists. Please verify username", - "settings_sidebar_users_error_addEditUser_-4" => "Photo path invalid", - "settings_sidebar_users_error_addEditUser_-5" => "Only photos with extension .jpg, .jpeg e .png are allowed", - "settings_sidebar_users_error_addEditUser_-6" => "It was not possible to upload photo", - "settings_sidebar_users_error_editUser_-3" => "Username and type field required", - "settings_sidebar_users_error_deleteUser_-3" => "User cannot delete himself", - "settings_sidebar_users_error_deleteUser_-409" => "User has dependencies. It is not possible to delete", - "settings_sidebar_users_error_deleteUserPhoto_-3" => "It was not possible to delete user photo on filesystem. Database not updated", // info banner zone - "settings_sidebar_spaces_info_searchSpace_NULL" => "Space not found", - "settings_sidebar_spaces_info_spaces_NULL" => "There is no spaces inserted", - "settings_sidebar_spaces_info_listSpace_-3" => "Space does not exist", - "settings_sidebar_spaces_info_spaceDeleted_photoNotDeleted" => "Space deleted. It was not possible to delete space photo from filesystem.", - "settings_sidebar_rooms_info_searchRoom_NULL" => "Room not found", - "settings_sidebar_rooms_info_rooms_NULL" => "There is no rooms inserted", - "settings_sidebar_rooms_info_listRoom_-3" => "Room does not exist", - "settings_sidebar_rooms_info_roomDeleted_photoNotDeleted" => "Room deleted. It was not possible to delete room photo from filesystem.", - "settings_sidebar_users_info_userDeleted_photoNotDeleted" => "User deleted. It was not possible to delete user photo from filesystem.", - "settings_sidebar_users_error_searchUser_NULL" => "User not found", - // success banner zone - "settings_sidebar_spaces_success_spaceDeleted" => "Space deleted.", - "settings_sidebar_spaces_success_spaceAdded" => "Space added.", - "settings_sidebar_spaces_success_spaceEdited" => "Space edited.", - "settings_sidebar_spaces_success_spacePhotoDeleted" => "Space photo deleted.", - "settings_sidebar_rooms_success_roomDeleted" => "Room deleted.", - "settings_sidebar_rooms_success_roomAdded" => "Room added.", - "settings_sidebar_rooms_success_roomEdited" => "Room edited.", - "settings_sidebar_rooms_success_roomPhotoDeleted" => "Room photo deleted.", - "settings_sidebar_profile_success_profilePhotoDeleted" => "Profile photo deleted.", - "settings_sidebar_profile_success_profileEdited" => "Profile edited", - "settings_sidebar_profile_success_stravaLinked" => "Strava linked with success", - "settings_sidebar_users_success_userAdded" => "User added.", - "settings_sidebar_users_success_userEdited" => "User edited.", - "settings_sidebar_users_success_userDeleted" => "User deleted.", - "settings_sidebar_users_success_userPhotoDeleted" => "User photo deleted.", - // spaces zone - "settings_sidebar_spaces_new" => "New space", - "settings_sidebar_spaces_search" => "Search", - "settings_sidebar_spaces_modal_addSpace_title" => "Add space", - "settings_sidebar_spaces_modal_addEditSpace_photoLabel" => "Space photo", - "settings_sidebar_spaces_modal_addEditSpace_nameLabel" => "Name", - "settings_sidebar_spaces_modal_addEditSpace_namePlaceholder" => "Name (max 45 characters)", - "settings_sidebar_spaces_modal_addEditSpace_cityLabel" => "Town/city", - "settings_sidebar_spaces_modal_addEditSpace_cityPlaceholder" => "Town/city (max 45 characters)", - "settings_sidebar_spaces_modal_addEditSpace_addressLabel" => "Address", - "settings_sidebar_spaces_modal_addEditSpace_addressPlaceholder" => "Address (max 45 characters)", - // spaces list - "settings_sidebar_spaces_list_title1" => "There is a total of", - "settings_sidebar_spaces_list_title2" => " space(s)", - "settings_sidebar_spaces_list_title3" => " per page)", - "settings_sidebar_spaces_list_numberofrooms" => "Number of rooms: ", - // add space zone - "settings_sidebar_button_addSpace" => "Add space", - // edit space zone - "settings_sidebar_button_editSpace_title" => "Edit space", - // delete space/room photo zone - "settings_sidebar_modal_editSpaceRoom_deleteSpaceRoomPhoto" => "Delete photo", - // search space/room zone - "settings_sidebar_form_searchSpaceRoomUser_namePlaceholder" => "Search", - "settings_sidebar_form_searchSpace_namePlaceholder" => "Name", - // delete space modal zone - "settings_sidebar_modal_deleteSpace_title" => "Delete space", - "settings_sidebar_modal_deleteSpace_body" => "Are you sure you want to delete space", - // room zone - "settings_sidebar_rooms_new" => "New room", - // add room zone - "settings_sidebar_button_addRoom" => "Add room", - // edit room zone - "settings_sidebar_button_editRoom_title" => "Edit room", - // rooms zone - "settings_sidebar_rooms_new" => "New room", - "settings_sidebar_rooms_modal_addRoom_title" => "Add room", - "settings_sidebar_rooms_modal_addEditRoom_photoLabel" => "Room photo", - "settings_sidebar_rooms_modal_addEditRoom_nameLabel" => "Name", - "settings_sidebar_rooms_modal_addEditRoom_namePlaceholder" => "Name (max 45 characters)", - "settings_sidebar_rooms_modal_addEditRoom_spaceLabel" => "Space which room belongs", - // room list - "settings_sidebar_rooms_list_title1" => "There is a total of", - "settings_sidebar_rooms_list_title2" => " room(s)", - "settings_sidebar_rooms_list_title3" => " per page)", - "settings_sidebar_rooms_list_spaceName" => "Space: ", - // delete room modal zone - "settings_sidebar_modal_deleteRoom_title" => "Delete room", - "settings_sidebar_modal_deleteRoom_body" => "Are you sure you want to delete room", - // users zone - "settings_sidebar_users_new" => "New user", - "settings_sidebar_users_search" => "Search", - // add room zone - "settings_sidebar_button_addUser" => "Add user", - // add user modal - "settings_sidebar_users_modal_addUser_title" => "Add user", - "settings_sidebar_users_modal_addEditUser_photoLabel" => "User photo", - "settings_sidebar_users_modal_addEditUser_usernameLabel" => "Username", - "settings_sidebar_users_modal_addEditUser_usernamePlaceholder" => "Username (max 45 characters)", - "settings_sidebar_users_modal_addEditUser_emailLabel" => "Email", - "settings_sidebar_users_modal_addEditUser_emailPlaceholder" => "Email (max 45 characters)", - "settings_sidebar_users_modal_addEditUser_nameLabel" => "Name", - "settings_sidebar_users_modal_addEditUser_namePlaceholder" => "Name (max 45 characters)", - "settings_sidebar_users_modal_addEditUser_passwordLabel" => "Password", - "settings_sidebar_users_modal_addEditUser_passwordPlaceholder" => "Password", - "settings_sidebar_users_modal_addEditUser_cityLabel" => "Town/city", - "settings_sidebar_users_modal_addEditUser_cityPlaceholder" => "Town/city (max 45 characters)", - "settings_sidebar_users_modal_addEditUser_birthdateLabel" => "Birth date", - "settings_sidebar_users_modal_addEditUser_genderLabel" => "User gender", - "settings_sidebar_users_modal_addEditUser_genderOption1" => "Male", - "settings_sidebar_users_modal_addEditUser_genderOption2" => "Female", - "settings_sidebar_users_modal_addEditUser_preferredLanguageLabel" => "User prefered language", - "settings_sidebar_users_modal_addEditUser_preferredLanguageOption1" => "English", - "settings_sidebar_users_modal_addEditUser_preferredLanguageOption2" => "Portuguese", - "settings_sidebar_users_modal_addEditUser_typeLabel" => "User type", - "settings_sidebar_users_modal_addEditUser_typeOption1" => "Regular user", - "settings_sidebar_users_modal_addEditUser_typeOption2" => "Administrator", - "settings_sidebar_users_modal_addEditUser_notesLabel" => "Notes", - "settings_sidebar_users_modal_addEditUser_notesPlaceholder" => "Notes (max 250 characters)", - "settings_sidebar_users_modal_addEditUser_isActiveLabel" => "Is active", - "settings_sidebar_users_modal_addEditUser_isActiveOption1" => "Yes", - "settings_sidebar_users_modal_addEditUser_isActiveOption2" => "No", - // search space/room zone - "settings_sidebar_form_searchUser_usernamePlaceholder" => "Username", - // users list - "settings_sidebar_users_list_title1" => "There is a total of", - "settings_sidebar_users_list_title2" => " user(s)", - "settings_sidebar_users_list_title3" => " per page)", - "settings_sidebar_users_list_user_accesstype" => "Access type: ", - "settings_sidebar_users_list_isactive" => "Active", - "settings_sidebar_users_list_isinactive" => "Inactive", - // edit user zone - "settings_sidebar_users_modal_editUser_title" => "Edit user", - // delete user photo zone - "settings_sidebar_users_modal_editUser_deleteUserPhoto" => "Delete photo", - // delete user zone - "settings_sidebar_users_modal_deleteUser_title" => "Delete user", - "settings_sidebar_users_modal_deleteUser_body" => "Are you sure you want to delete user", - // edit profile modal zone - "settings_sidebar_profile_editProfile_title" => "Edit profile", - "settings_sidebar_profile_photo_label" => "Profile photo", - "settings_sidebar_profile_username_label" => "Username", - "settings_sidebar_profile_username_placeholder" => "Username (max 45 characters)", - "settings_sidebar_profile_name_label" => "Name", - "settings_sidebar_profile_name_placeholder" => "Name (max 45 characters)", - "settings_sidebar_profile_email_label" => "Email", - "settings_sidebar_profile_email_placeholder" => "Email (max 45 characters)", - "settings_sidebar_profile_city_label" => "Town/city", - "settings_sidebar_profile_city_placeholder" => "Town/city (max 45 characters)", - "settings_sidebar_profile_birthdate_label" => "Birth date", - "settings_sidebar_profile_gender_label" => "Gender", - "settings_sidebar_profile_gender_option1" => "Male", - "settings_sidebar_profile_gender_option2" => "Female", - "settings_sidebar_profile_preferredLanguage_label" => "Prefered language", - "settings_sidebar_profile_preferredLanguage_option1" => "English", - "settings_sidebar_profile_preferredLanguage_option2" => "Portuguese", - // profile zone - "settings_sidebar_profile_deleteProfilePhoto" => "Delete profile photo", - "settings_sidebar_profile_modal_title_deleteProfilePhoto" => "Delete profile photo", - "settings_sidebar_profile_modal_body_deleteProfilePhoto" => "Are you sure you want to delete the profile photo?", - "settings_sidebar_profile_button_editprofile" => "Edit profile", - "settings_sidebar_profile_gender_male" => "Male", - "settings_sidebar_profile_gender_female" => "Female", - "settings_sidebar_profile_access_type_regular_user" => "Regular user", - "settings_sidebar_profile_access_type_admin" => "Admin", - "settings_sidebar_profile_username_subtitle" => "Username: ", - "settings_sidebar_profile_email_subtitle" => "Email: ", - "settings_sidebar_profile_gender_subtitle" => "Gender: ", - "settings_sidebar_profile_birthdate_subtitle" => "Birthdate: ", - "settings_sidebar_profile_city_subtitle" => "City: ", - "settings_sidebar_profile_access_type_subtitle" => "Access type: ", - "settings_sidebar_profile_preferredlanguage_subtitle" => "Preferred language: ", - // gear zone - // error banner zone - "settings_sidebar_gear_API_error_-1" => "Connection to API not possible. Failed to execute cURL request", - "settings_sidebar_gear_API_error_-2" => "API return code was not 200", - "settings_sidebar_gear_error_addGear_-3" => "Nickname already exists. Please verify nickname", - "settings_sidebar_gear_error_editGear_-3" => "Nickname and type field required", - // info banner zone - "settings_sidebar_gear_info_searchGear_NULL" => "Gear not found", - "settings_sidebar_gear_info_fromGear_invalidGear" => "Gear not found", // success banner zone - "settings_sidebar_gear_success_gearAdded" => "Gear added.", - "settings_sidebar_gear_success_gearEdited" => "Gear edited.", - "settings_sidebar_gear_success_gearDeleted" => "Gear deleted.", - // add and edit gear zone modal - "settings_sidebar_gear_buttonLabel_addGear" => "Add gear:", - "settings_sidebar_gear_button_addGear" => "Add gear", - "settings_sidebar_gear_modal_addGear_title" => "Add gear", - "settings_sidebar_gear_modal_addEditGear_brandLabel" => "Brand", - "settings_sidebar_gear_modal_addEditGear_brandPlaceholder" => "Brand (max 45 characters)", - "settings_sidebar_gear_modal_addEditGear_modelLabel" => "Model", - "settings_sidebar_gear_modal_addEditGear_modelPlaceholder" => "Model (max 45 characters)", - "settings_sidebar_gear_modal_addEditGear_nicknameLabel" => "Nickname", - "settings_sidebar_gear_modal_addEditGear_nicknamePlaceholder" => "Nickname (max 45 characters)", - "settings_sidebar_gear_modal_addEditGear_gearTypeLabel" => "Gear type", - "settings_sidebar_gear_modal_addEditGear_gearTypeOption1" => "Bike", - "settings_sidebar_gear_modal_addEditGear_gearTypeOption2" => "Shoes", - "settings_sidebar_gear_modal_addEditGear_gearTypeOption3" => "Wetsuit", - "settings_sidebar_gear_modal_addEditGear_dateLabel" => "Created date", - "settings_sidebar_gear_modal_addEditGear_datePlaceholder" => "Date", - // search gear zone - "settings_sidebar_gear_button_searchGear" => "Search", - "settings_sidebar_gear_form_searchGear_nicknamePlaceholder" => "Nickname", - // list - "settings_sidebar_gear_list_title1" => "There is a total of", - "settings_sidebar_gear_list_title2" => " gear(s)", - "settings_sidebar_gear_list_title3" => " per page)", - "settings_sidebar_settings_sidebar_gear_type" => "Type: ", - "settings_sidebar_gear_list_isactive" => "Active", - "settings_sidebar_gear_list_isinactive" => "Inactive", - // edit gear zone - "settings_sidebar_gear_modal_editGear_title" => "Edit gear", - "settings_sidebar_gear_modal_editGear_gearIsActiveLabel" => "Is active", - "settings_sidebar_gear_modal_editGear_gearIsActiveOption1" => "Yes", - "settings_sidebar_gear_modal_editGear_gearIsActiveOption2" => "No", - // delete gear zone - "settings_sidebar_gear_modal_deleteGear_title" => "Delete gear", - "settings_sidebar_gear_modal_deleteGear_body" => "Are you sure you want to delete gear" + ]; diff --git a/frontend/lang/settings/inc/integration-settings/en.php b/frontend/lang/settings/inc/integration-settings/en.php new file mode 100644 index 00000000..aeb1915f --- /dev/null +++ b/frontend/lang/settings/inc/integration-settings/en.php @@ -0,0 +1,17 @@ + "Strava linked with success", + "settings_integration_settings_success_stravaGear" => "Strava gear retrieved", + "settings_integration_settings_success_stravaActivities" => "Strava activities retrieved", + // card zone + "settings_integration_settings_strava_title" => "Strava", + "settings_integration_settings_strava_body" => "Strava is an American internet service for tracking physical exercise which incorporates social network features.", + "settings_integration_settings_strava_body_linked" => "Strava is linked to your account", + // general + "settings_integration_settings_connect_button" => "Connect", + "settings_integration_settings_retrieve_last_week_activities_button" => "Retrieve last week activities", + "settings_integration_settings_retrieve_gear_button" => "Retrieve gear", +]; \ No newline at end of file diff --git a/frontend/lang/settings/inc/profile-settings/en.php b/frontend/lang/settings/inc/profile-settings/en.php new file mode 100644 index 00000000..05619470 --- /dev/null +++ b/frontend/lang/settings/inc/profile-settings/en.php @@ -0,0 +1,50 @@ + "It was not possible to delete profile photo on filesystem. Database not updated", + "settings_profile_settings_error_editProfile_-3" => "Photo path invalid", + "settings_profile_settings_error_editProfile_-4" => "Only photos with extension .jpg, .jpeg e .png are allowed", + "settings_profile_settings_error_editProfile_-5" => "It was not possible to upload photo", + // info banner zone + + // success banner zone + "settings_sidebar_profile_success_profilePhotoDeleted" => "Profile photo deleted.", + "settings_sidebar_profile_success_profileEdited" => "Profile edited", + + // edit profile modal zone + "settings_sidebar_profile_editProfile_title" => "Edit profile", + "settings_sidebar_profile_photo_label" => "Profile photo", + "settings_sidebar_profile_username_label" => "Username", + "settings_sidebar_profile_username_placeholder" => "Username (max 45 characters)", + "settings_sidebar_profile_name_label" => "Name", + "settings_sidebar_profile_name_placeholder" => "Name (max 45 characters)", + "settings_sidebar_profile_email_label" => "Email", + "settings_sidebar_profile_email_placeholder" => "Email (max 45 characters)", + "settings_sidebar_profile_city_label" => "Town/city", + "settings_sidebar_profile_city_placeholder" => "Town/city (max 45 characters)", + "settings_sidebar_profile_birthdate_label" => "Birth date", + "settings_sidebar_profile_gender_label" => "Gender", + "settings_sidebar_profile_gender_option1" => "Male", + "settings_sidebar_profile_gender_option2" => "Female", + "settings_sidebar_profile_preferredLanguage_label" => "Prefered language", + "settings_sidebar_profile_preferredLanguage_option1" => "English", + "settings_sidebar_profile_preferredLanguage_option2" => "Portuguese", + // profile zone + "settings_sidebar_profile_deleteProfilePhoto" => "Delete profile photo", + "settings_sidebar_profile_modal_title_deleteProfilePhoto" => "Delete profile photo", + "settings_sidebar_profile_modal_body_deleteProfilePhoto" => "Are you sure you want to delete the profile photo?", + "settings_sidebar_profile_button_editprofile" => "Edit profile", + "settings_sidebar_profile_gender_male" => "Male", + "settings_sidebar_profile_gender_female" => "Female", + "settings_sidebar_profile_access_type_regular_user" => "Regular user", + "settings_sidebar_profile_access_type_admin" => "Admin", + "settings_sidebar_profile_username_subtitle" => "Username: ", + "settings_sidebar_profile_email_subtitle" => "Email: ", + "settings_sidebar_profile_gender_subtitle" => "Gender: ", + "settings_sidebar_profile_birthdate_subtitle" => "Birthdate: ", + "settings_sidebar_profile_city_subtitle" => "City: ", + "settings_sidebar_profile_access_type_subtitle" => "Access type: ", + "settings_sidebar_profile_preferredlanguage_subtitle" => "Preferred language: ", +]; diff --git a/frontend/lang/settings/inc/security-settings/en.php b/frontend/lang/settings/inc/security-settings/en.php new file mode 100644 index 00000000..705d79ac --- /dev/null +++ b/frontend/lang/settings/inc/security-settings/en.php @@ -0,0 +1,24 @@ + "Passwords don't match", + "settings_security_settings_error_password_complexity-4" => "Password complexity not met", + + // info banner zone + "settings_security_settings_info_password_requirements" => "Password requirements includes:", + "settings_security_settings_info_password_requirements_characters" => " - 8 characters;", + "settings_security_settings_info_password_requirements_capital_letters" => " - 1 capital letter;", + "settings_security_settings_info_password_requirements_numbers" => " - 1 number;", + "settings_security_settings_info_password_requirements_special_characters" => " - 1 special character.", + + // success banner zone + "settings_security_settings_success_password_edited" => "User password edited", + + // form change password + "settings_security_settings_subtitle_change_password" => "Change password", + "settings_security_settings_subtitle_change_password_password" => "Password", + "settings_security_settings_subtitle_change_password_repeat_password" => "Repeat password", + "settings_security_settings_subtitle_change_password_button" => "Change password", +]; diff --git a/frontend/lang/settings/inc/users-settings/en.php b/frontend/lang/settings/inc/users-settings/en.php new file mode 100644 index 00000000..f3859de3 --- /dev/null +++ b/frontend/lang/settings/inc/users-settings/en.php @@ -0,0 +1,86 @@ + "Username already exists. Please verify username", + "settings_users_settings_error_addEditUser_-4" => "Photo path invalid", + "settings_users_settings_error_addEditUser_-5" => "Only photos with extension .jpg, .jpeg e .png are allowed", + "settings_users_settings_error_addEditUser_-6" => "It was not possible to upload photo", + "settings_users_settings_error_editUser_-3" => "Username and type field required", + "settings_users_settings_error_deleteUser_-3" => "User cannot delete himself", + "settings_users_settings_error_deleteUser_-409" => "User has dependencies. It is not possible to delete", + "settings_users_settings_error_deleteUserPhoto_-3" => "It was not possible to delete user photo on filesystem. Database not updated", + "settings_users_settings_error_passwords_dont_match-3" => "Passwords don't match", + "settings_users_settings_error_password_complexity-4" => "Password complexity not met", + "settings_users_settings_error_user_id_not_valid-5" => "User ID not valid", + // info banner zone + "settings_users_settings_info_userDeleted_photoNotDeleted" => "User deleted. It was not possible to delete user photo from filesystem.", + "settings_users_settings_error_searchUser_NULL" => "User not found", + // success banner zone + "settings_users_settings_success_userAdded" => "User added.", + "settings_users_settings_success_userEdited" => "User edited.", + "settings_users_settings_success_userDeleted" => "User deleted.", + "settings_users_settings_success_userPhotoDeleted" => "User photo deleted.", + "settings_users_settings_success_password_edited" => "User password edited", + // users zone + "settings_users_settings_new" => "New user", + "settings_users_settings_search" => "Search", + "settings_sidebar_button_addUser" => "Add user", + // add user modal + "settings_users_settings_modal_addUser_title" => "Add user", + "settings_users_settings_modal_addEditUser_photoLabel" => "User photo", + "settings_users_settings_modal_addEditUser_usernameLabel" => "Username", + "settings_users_settings_modal_addEditUser_usernamePlaceholder" => "Username (max 45 characters)", + "settings_users_settings_modal_addEditUser_emailLabel" => "Email", + "settings_users_settings_modal_addEditUser_emailPlaceholder" => "Email (max 45 characters)", + "settings_users_settings_modal_addEditUser_nameLabel" => "Name", + "settings_users_settings_modal_addEditUser_namePlaceholder" => "Name (max 45 characters)", + "settings_users_settings_modal_addEditUser_passwordLabel" => "Password", + "settings_users_settings_modal_addEditUser_passwordPlaceholder" => "Password", + "settings_users_settings_modal_addEditUser_cityLabel" => "Town/city", + "settings_users_settings_modal_addEditUser_cityPlaceholder" => "Town/city (max 45 characters)", + "settings_users_settings_modal_addEditUser_birthdateLabel" => "Birth date", + "settings_users_settings_modal_addEditUser_genderLabel" => "User gender", + "settings_users_settings_modal_addEditUser_genderOption1" => "Male", + "settings_users_settings_modal_addEditUser_genderOption2" => "Female", + "settings_users_settings_modal_addEditUser_preferredLanguageLabel" => "User prefered language", + "settings_users_settings_modal_addEditUser_preferredLanguageOption1" => "English", + "settings_users_settings_modal_addEditUser_preferredLanguageOption2" => "Portuguese", + "settings_users_settings_modal_addEditUser_typeLabel" => "User type", + "settings_users_settings_modal_addEditUser_typeOption1" => "Regular user", + "settings_users_settings_modal_addEditUser_typeOption2" => "Administrator", + "settings_users_settings_modal_addEditUser_notesLabel" => "Notes", + "settings_users_settings_modal_addEditUser_notesPlaceholder" => "Notes (max 250 characters)", + "settings_users_settings_modal_addEditUser_isActiveLabel" => "Is active", + "settings_users_settings_modal_addEditUser_isActiveOption1" => "Yes", + "settings_users_settings_modal_addEditUser_isActiveOption2" => "No", + // search user zone + "settings_sidebar_form_searchUser_usernamePlaceholder" => "Username", + // users list + "settings_users_settings_list_title1" => "There is a total of", + "settings_users_settings_list_title2" => " user(s)", + "settings_users_settings_list_title3" => " per page)", + "settings_users_settings_list_user_accesstype" => "Access type: ", + "settings_users_settings_list_isactive" => "Active", + "settings_users_settings_list_isinactive" => "Inactive", + // edit user zone + "settings_users_settings_modal_editUser_title" => "Edit user", + // delete user photo zone + "settings_users_settings_modal_editUser_deleteUserPhoto" => "Delete photo", + // delete user zone + "settings_users_settings_modal_deleteUser_title" => "Delete user", + "settings_users_settings_modal_deleteUser_body" => "Are you sure you want to delete user", + // edit user password zone + // info banner zone + "settings_users_settings_info_password_requirements" => "Password requirements includes:", + "settings_users_settings_info_password_requirements_characters" => " - 8 characters;", + "settings_users_settings_info_password_requirements_capital_letters" => " - 1 capital letter;", + "settings_users_settings_info_password_requirements_numbers" => " - 1 number;", + "settings_users_settings_info_password_requirements_special_characters" => " - 1 special character.", + // modal + "settings_users_settings_modal_changeUserPassword_title" => "Change password", + "settings_users_settings_modal_changeUserPassword_body" => "Change password for user ", + "settings_users_settings_modal_change_password_password" => "Password", + "settings_users_settings_modal_change_password_repeat_password" => "Repeat password", +]; diff --git a/frontend/login.php b/frontend/login.php index c0a594cc..e31b35da 100755 --- a/frontend/login.php +++ b/frontend/login.php @@ -11,18 +11,8 @@ header("Location: ../index.php"); } -// Load the language file based on the user's preferred language -/*switch ($_SESSION["preferred_language"]) { - case 'en': - $translationsLogin = include $_SERVER['DOCUMENT_ROOT'].'/lang/login/en.php'; - break; - case 'pt': - $translationsLogin = include $_SERVER['DOCUMENT_ROOT'].'/lang/login/pt.php'; - break;*/ -// ... -//default: +// Load the en language file $translationsLogin = include $_SERVER['DOCUMENT_ROOT'] . '/lang/login/en.php'; -//} $error = 0; @@ -34,51 +24,65 @@ $neverExpires = true; } $result = loginUser($_POST["loginUsername"], $hashPassword, $neverExpires); - $response = json_decode($result, true); - if (isset($response['access_token'])) { - $error = setUserRelatedInfoSession($response['access_token']); - if ($error == 0) { - header("Location: ../index.php"); - die(); + + if ($result[1] === 200) { + $responseContent = json_decode($result[0], true); + if (isset($responseContent['access_token'])) { + $error = setUserRelatedInfoSession($responseContent['access_token']); + if ($error == 0) { + header("Location: ../index.php"); + die(); + } + }else{ + $error = 1; } } else { - $error = 1; + if ($result[1] === 400) { + if(json_decode($result[0], true)["error"]["message"] === "User is not active"){ + $error = 2; + }else{ + if(json_decode($result[0], true)["error"]["message"] == "Incorrect username or password"){ + $error = 3; + } + } + }else{ + $error = 4; + } } } - -$random_number = mt_rand(1, 2); ?>
- - - - - + + + + + + + +
-

Endurain


diff --git a/frontend/settings/inc/integration-settings.php b/frontend/settings/inc/integration-settings.php new file mode 100644 index 00000000..60af7d00 --- /dev/null +++ b/frontend/settings/inc/integration-settings.php @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + +
+
+
+ Compatible with Strava image +
+

+

+ "> + +
+ + + + +
+
+
+
\ No newline at end of file diff --git a/frontend/settings/inc/profile-settings.php b/frontend/settings/inc/profile-settings.php new file mode 100644 index 00000000..fd76e50b --- /dev/null +++ b/frontend/settings/inc/profile-settings.php @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + +
+
+
+ alt="Profile picture" width="180" height="180" class="rounded-circle"> +
+ + + + + + + + + + + "> + + + + + +
+ + +
+

+

+

+

+

+

+

+

+

+
\ No newline at end of file diff --git a/frontend/settings/inc/security-settings.php b/frontend/settings/inc/security-settings.php new file mode 100644 index 00000000..9689fd59 --- /dev/null +++ b/frontend/settings/inc/security-settings.php @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + +

+ + + + +
+ + + " required> + + + + " required> + +

*

+ + +
\ No newline at end of file diff --git a/frontend/settings/inc/users-settings.php b/frontend/settings/inc/users-settings.php new file mode 100644 index 00000000..ae01e4bf --- /dev/null +++ b/frontend/settings/inc/users-settings.php @@ -0,0 +1,655 @@ + 0){ + if($_POST["passUserEditAdmin"] == $_POST["passRepeatUserEditAdmin"]){ + // Check password complexity + if (preg_match('/^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[A-Za-z\d!@#$%^&*()_+]{8,}$/', $_POST["passUserEditAdmin"])) { + $editUserPasswordAdminAction = editUserPassword($_GET["userID"], $_POST["passUserEditAdmin"]); + }else{ + $editUserPasswordAdminAction = -4; + } + }else{ + $editUserPasswordAdminAction = -3; + } + }else{ + $editUserPasswordAdminAction = -5; + } + } +?> + + + + + + + + + + + + + + + + +
+
+ + + + + +
+
+
+ + + + + +
+
+
+ + + + +
+

( :

+ + + +
+ + + \ No newline at end of file diff --git a/frontend/settings/settings.php b/frontend/settings/settings.php index 418b8968..bae7f09d 100755 --- a/frontend/settings/settings.php +++ b/frontend/settings/settings.php @@ -6,34 +6,22 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/inc/sqlFunctions.php"; $page = "settings"; -// users -$numUsers = 0; -$users = []; -$addUserAction = -9000; -$photoDeleted = -9000; -$deleteAction = -9000; -$deletePhotoUserAction = -9000; -$editUserAction = -9000; -$pageNumberUsers = 1; -// profile -$editProfileAction = -9000; -$deletePhotoProfileAction = -9000; - -// general -$numRecords = 5; if (!isLogged()) { header("Location: ../login.php"); } -if (!isTokenValid($_SESSION["token"])) { - header("Location: ../logout.php?sessionExpired=1"); -} +#if (!isTokenValid($_SESSION["token"])) { +# header("Location: ../logout.php?sessionExpired=1"); +#} if ($_SESSION["access_type"] != 2) { $_GET["profileSettings"] = 1; } +// general +$numRecords = 5; + // Load the language file based on the user's preferred language switch ($_SESSION["preferred_language"]) { case 'en': @@ -47,875 +35,74 @@ $translationsSettings = include $_SERVER['DOCUMENT_ROOT'] . '/lang/settings/en.php'; } -# Users section -/* Add user action */ -if (isset($_POST["addUser"]) && $_GET["addUser"] == 1) { - if (empty(trim($_POST["userNameAdd"]))) { - $_POST["userNameAdd"] = NULL; - } - - if (isset($_FILES["userImgAdd"]) && $_FILES["userImgAdd"]["error"] == 0) { - $target_dir = "../img/users_img/"; - $info = pathinfo($_FILES["userImgAdd"]["name"]); - $ext = $info['extension']; // get the extension of the file - #$number=lastIdUsers()+1; - $newname = "img_" . rand(1, 20) . "_" . rand(1, 9999999) . "_" . rand(1, 20) . "." . $ext; - $target_file = $target_dir . $newname; - $uploadOk = 1; - $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION)); - // Check if image file is a actual image or fake image - $check = getimagesize($_FILES["userImgAdd"]["tmp_name"]); - if ($check !== false) { - $uploadOk = 1; - } else { - $addUserAction = -4; - $uploadOk = 0; - } - // Allow certain file formats - if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") { - $addUserAction = -5; - $uploadOk = 0; - } - // Check if $uploadOk is set to 0 by an error - if ($uploadOk == 1) { - if (move_uploaded_file($_FILES["userImgAdd"]["tmp_name"], $target_file)) { - $photoPath = "..\img\users_img\\" . $newname; - $photoPath_aux = $target_file; - } else { - $addUserAction = -6; - $uploadOk = 0; - } - } - } else { - $photoPath = NULL; - $photoPath_aux = NULL; - $uploadOk = 1; - } - if ($uploadOk == 1) { - $userCity = trim($_POST["userCityAdd"]); - if (empty($userCity)) { - $userCity = NULL; - } - if (empty($_POST["userBirthDateAdd"])) { - $_POST["userBirthDateAdd"] = NULL; - } - - $addUserAction = newUser(trim($_POST["userNameAdd"]), trim($_POST["userUsernameAdd"]), trim($_POST["userEmailAdd"]), $_POST["passUserAdd"], $_POST["userGenderAdd"], $_POST["userPreferredLanguageAdd"], $userCity, $_POST["userBirthDateAdd"], $_POST["userAccessTypeAdd"], $photoPath, $photoPath_aux, 1); - } -} - -/* Edit user action */ -if (isset($_POST["userEdit"])) { - if (empty(trim($_POST["userNameEdit"]))) { - $_POST["userNameEdit"] = NULL; - } - if (empty($_POST["userTypeEdit"])) { - $_POST["userTypeEdit"] = NULL; - } - - if (empty($_POST["userUsernameEdit"]) && empty($_POST["userTypeEdit"])) { - $editAction = -3; - $userImg = getUserPhotoFromID($_GET["userID"]); - } else { - $userCity = trim($_POST["userCityEdit"]); - if (empty($userCity)) { - $userCity = NULL; - } - if (empty($_POST["userBirthDateEdit"])) { - $_POST["userBirthDateEdit"] = NULL; - } - if (isset($_FILES["userImgEdit"]) && $_FILES["userImgEdit"]["error"] == 0) { - $target_dir = "../img/users_img/"; - $info = pathinfo($_FILES["userImgEdit"]["name"]); - $ext = $info['extension']; // get the extension of the file - #$newname = $_GET["userID"].".".$ext; - $newname = "img_" . rand(1, 20) . "_" . rand(1, 9999999) . "_" . rand(1, 20) . "." . $ext; - $target_file = $target_dir . $newname; - $uploadOk = 1; - $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION)); - // Check if image file is a actual image or fake image - $check = getimagesize($_FILES["userImgEdit"]["tmp_name"]); - if ($check !== false) { - $uploadOk = 1; - } else { - $editUserAction = -4; - $uploadOk = 0; - } - // Allow certain file formats - if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") { - $editUserAction = -5; - $uploadOk = 0; - } - // Check if $uploadOk is set to 0 by an error - if ($uploadOk == 1) { - if(getUserPhotoAuxFromID($_GET["userID"])[0] != null){ - if (unlink(getUserPhotoAuxFromID($_GET["userID"])[0])) { - unsetUserPhoto($_GET["userID"]); - } - } - if (move_uploaded_file($_FILES["userImgEdit"]["tmp_name"], $target_file)) { - $photoPath = "..\img\users_img\\" . $newname; - $photoPath_aux = $target_file; - $editUserAction = editUser(trim($_POST["userNameEdit"]), trim($_POST["userUsernameEdit"]), trim($_POST["userEmailEdit"]), $_GET["userID"], $_POST["userPreferredLanguageEdit"], $userCity, $_POST["userBirthDateEdit"], $_POST["userGenderEdit"], $_POST["userTypeEdit"], $photoPath, $photoPath_aux, $_POST["userIsActiveEdit"]); - if ($_GET["userID"] == $_SESSION["id"]) { - setUserRelatedInfoSession($_SESSION["token"]); - } - } else { - $editUserAction = -6; - $uploadOk = 0; - } - } - } else { - $userPhoto = getUserPhotoFromID($_GET["userID"])[0]; - if (empty($userPhoto)) { - $userPhoto = NULL; - } - $userPhotoAux = getUserPhotoAuxFromID($_GET["userID"])[0]; - if (empty($userPhotoAux)) { - $userPhotoAux = NULL; - } - $editUserAction = editUser(trim($_POST["userNameEdit"]), trim($_POST["userUsernameEdit"]), trim($_POST["userEmailEdit"]), $_GET["userID"], $_POST["userPreferredLanguageEdit"], $userCity, $_POST["userBirthDateEdit"], $_POST["userGenderEdit"], $_POST["userTypeEdit"], $userPhoto, $userPhotoAux, $_POST["userIsActiveEdit"]); - if ($_GET["userID"] == $_SESSION["id"]) { - setUserRelatedInfoSession($_SESSION["token"]); - } - } - } -} - -/* Delete user photo */ -if (isset($_GET["deletePhotoUser"]) && $_GET["deletePhotoUser"] == 1) { - if (unlink(getUserPhotoAuxFromID($_GET["userID"])[0])) { - $deletePhotoUserAction = unsetUserPhoto($_GET["userID"]); - if ($_GET["userID"] == $_SESSION["id"]) { - setUserRelatedInfoSession($_SESSION["token"]); - } - } else { - $deletePhotoUserAction = -3; - } -} - -/* Delete user */ -if (isset($_GET["deleteUser"]) && $_GET["deleteUser"] == 1) { - if ($_GET["userID"] != $_SESSION["id"]) { - $photo_path = getUserPhotoAuxFromID($_GET["userID"]); - $deleteAction = deleteUser($_GET["userID"]); - if ($deleteAction == 0) { - if (!is_null($photo_path[0])) { - if (unlink($photo_path[0])) { - $photoDeleted = 0; - } else { - $photoDeleted = 1; - } - } else { - $photoDeleted = 2; - } - } - } else { - $deleteAction = -3; - } -} - -if (isset($_GET["pageNumberUsers"])) { - $pageNumberUsers = $_GET["pageNumberUsers"]; -} - -if (!isset($_POST["userSearch"])) { - $users = getUsersPagination($pageNumberUsers, $numRecords); - $numUsers = numUsers(); - $total_pages = ceil($numUsers / $numRecords); -} else { - $users = getUserFromUsername(urlencode(trim($_POST["userUsername"]))); - if ($users == NULL) { - $numUsers = 0; - } else { - $numUsers = 1; - } -} - -// User profile section -/* Edit action */ -if (isset($_POST["editProfile"])) { - if (empty(trim($_POST["profileNameEdit"]))) { - $_POST["profileNameEdit"] = NULL; - } - - if (isset($_FILES["profileImgEdit"]) && $_FILES["profileImgEdit"]["error"] == 0) { - $target_dir = "../img/users_img/"; - $info = pathinfo($_FILES["profileImgEdit"]["name"]); - $ext = $info['extension']; // get the extension of the file - #$newname = $_GET["userID"].".".$ext; - $newname = "img_" . rand(1, 20) . "_" . rand(1, 9999999) . "_" . rand(1, 20) . "." . $ext; - $target_file = $target_dir . $newname; - $uploadOk = 1; - $imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION)); - // Check if image file is a actual image or fake image - $check = getimagesize($_FILES["profileImgEdit"]["tmp_name"]); - if ($check !== false) { - $uploadOk = 1; - } else { - $editProfileAction = -3; - $uploadOk = 0; - } - // Allow certain file formats - if ($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") { - $editProfileAction = -4; - $uploadOk = 0; - } - // Check if $uploadOk is set to 0 by an error - if ($uploadOk == 1) { - if(getUserPhotoAuxFromID($_SESSION["id"])[0] != null){ - if (unlink(getUserPhotoAuxFromID($_SESSION["id"])[0])) { - unsetUserPhoto($_SESSION["id"]); - } - } - if (move_uploaded_file($_FILES["profileImgEdit"]["tmp_name"], $target_file)) { - $photoPath = "..\img\users_img\\" . $newname; - $photoPath_aux = $target_file; - $editProfileAction = editUser(trim($_POST["profileNameEdit"]), trim($_POST["profileUsernameEdit"]), trim($_POST["profileEmailEdit"]), $_SESSION["id"], $_POST["profilePreferredLanguageEdit"], trim($_POST["profileCityEdit"]), $_POST["profileBirthDateEdit"], $_POST["profileGenderEdit"], $_SESSION["access_type"], $photoPath, $photoPath_aux, 1); - setUserRelatedInfoSession($_SESSION["token"]); - } else { - $editProfileAction = -5; - $uploadOk = 0; - } - } - } else { - $profilePhoto = getUserPhotoFromID($_SESSION["id"])[0]; - $profilePhotoAux = getUserPhotoAuxFromID($_SESSION["id"])[0]; - $editProfileAction = editUser(trim($_POST["profileNameEdit"]), trim($_POST["profileUsernameEdit"]), trim($_POST["profileEmailEdit"]), $_SESSION["id"], $_POST["profilePreferredLanguageEdit"], trim($_POST["profileCityEdit"]), $_POST["profileBirthDateEdit"], $_POST["profileGenderEdit"], $_SESSION["access_type"], $profilePhoto, $profilePhotoAux, 1); - setUserRelatedInfoSession($_SESSION["token"]); - } -} - -/* Delete user photo */ -if (isset($_GET["deleteProfilePhoto"]) && $_GET["deleteProfilePhoto"] == 1) { - if (unlink(getUserPhotoAuxFromID($_SESSION["id"])[0])) { - $deletePhotoProfileAction = unsetUserPhoto($_SESSION["id"]); - if ($deletePhotoProfileAction == 0) { - $_SESSION["photo_path"] = NULL; - $_SESSION["photo_path_aux"] = NULL; - } - } else { - $deletePhotoProfileAction = -3; - } -} - -if (isset($_GET["linkStrava"]) && $_GET["linkStrava"] == 1) { - $state = bin2hex(random_bytes(16)); - $generate_user_state = setUniqueUserStateStravaLink($state); - if ($generate_user_state == 0) { - // Example PHP code for the authentication link - linkStrava($state); - } - #$unset_user_state = unsetUniqueUserStateStravaLink(); -} - -if (isset($_GET["stravaLinked"]) && $_GET["stravaLinked"] == 1) { - setUserRelatedInfoSession($_SESSION["token"]); - $unset_user_state = unsetUniqueUserStateStravaLink(); - #getStravaActivities(); -} ?> - - -
-

-
-
+
+

+

- -
;"> - - - - - - - - - - - - - - - -
-
- - - - - -
-
-
- - - - - -
-
-
- - - - -
-

( :

- -
    - -
  • -
    - alt="userPicture" class="rounded-circle" width="55" height="55"> -
    -
    - -
    - -
    -
    -
    - - - - - - - "> - - - - - - "> - - - - -
    -
  • - -
- -
- - - -
- - -
;"> - +
;"> +
;"> - - - - - - - - - - - - - -
-
-
- alt="Profile picture" width="180" height="180" class="rounded-circle"> -
- - - - - - - - - - - "> - - - + +
- - -
- Strava already linked -
- - " style="--bs-btn-bg: #FC4C02; --bs-btn-active-bg: #FC4C02; --bs-btn-hover-bg: #FC4C02; --bs-btn-disabled-bg: #FC4C02; --bs-btn-disabled-border-color: #FC4C02;" href="../settings/settings.php?profileSettings=1&linkStrava=1" role="button">Link with strava button -
+ +
;"> + +
- -
-

-

-

-

-

-

-

-

-

-
+ +
;"> +
-
-
-
- -
@@ -924,12 +111,15 @@ if (window.location.search.indexOf("users=1") !== -1) { changeActive(null, 'divUsers'); } - if (window.location.search.indexOf("global=1") !== -1) { - changeActive(null, 'divGlobal'); - } if (window.location.search.indexOf("profileSettings=1") !== -1) { changeActive(null, 'divProfileSettings'); } + if (window.location.search.indexOf("securitySettings=1") !== -1) { + changeActive(null, 'divSecuritySettings'); + } + if (window.location.search.indexOf("integrationsSettings=1") !== -1) { + changeActive(null, 'divIntegrationsSettings'); + } }); function changeActive(event, div) { @@ -947,23 +137,27 @@ function changeActive(event, div) { if (div == "divUsers") { document.getElementById("divUsers").style.display = 'block'; - document.getElementById("divGlobal").style.display = 'none'; document.getElementById("divProfileSettings").style.display = 'none'; + document.getElementById("divSecuritySettings").style.display = 'none'; + document.getElementById("divIntegrationsSettings").style.display = 'none'; } else { - if (div == "divGlobal") { + if (div == "divProfileSettings") { document.getElementById("divUsers").style.display = 'none'; - document.getElementById("divGlobal").style.display = 'block'; - document.getElementById("divProfileSettings").style.display = 'none'; + document.getElementById("divProfileSettings").style.display = 'block'; + document.getElementById("divSecuritySettings").style.display = 'none'; + document.getElementById("divIntegrationsSettings").style.display = 'none'; } else { - if (div == "divProfileSettings") { + if (div == "divSecuritySettings") { document.getElementById("divUsers").style.display = 'none'; - document.getElementById("divGlobal").style.display = 'none'; - document.getElementById("divProfileSettings").style.display = 'block'; - } else { - if (div == "divGearSettings") { + document.getElementById("divProfileSettings").style.display = 'none'; + document.getElementById("divSecuritySettings").style.display = 'block'; + document.getElementById("divIntegrationsSettings").style.display = 'none'; + }else{ + if (div == "divIntegrationsSettings") { document.getElementById("divUsers").style.display = 'none'; - document.getElementById("divGlobal").style.display = 'none'; document.getElementById("divProfileSettings").style.display = 'none'; + document.getElementById("divSecuritySettings").style.display = 'none'; + document.getElementById("divIntegrationsSettings").style.display = 'block'; } } } diff --git a/frontend/users/user.php b/frontend/users/user.php index 34aca05f..22b59d3c 100755 --- a/frontend/users/user.php +++ b/frontend/users/user.php @@ -527,7 +527,7 @@ class="fa-solid fa-gear">
- Date: Wed, 7 Feb 2024 16:05:38 +0000 Subject: [PATCH 02/10] Backend revamp - [backend] Session functions finished - [backend] User functions finished - [backend] Activities functions started - [backend] Activity streams functions started - [backend] Gear functions started - [backend] Separated dependencies structure to several files --- backend/crud/activities.py | 151 ------ ...access_tokens.py => crud_access_tokens.py} | 0 backend/crud/crud_activities.py | 466 ++++++++++++++++ backend/crud/crud_activity_streams.py | 129 +++++ backend/crud/crud_gear.py | 242 +++++++++ ...egrations.py => crud_user_integrations.py} | 1 - backend/crud/{users.py => crud_users.py} | 143 ++++- backend/dependencies/__init__.py | 0 .../dependencies/dependencies_activities.py | 9 + .../dependencies_activity_streams.py | 5 + .../dependencies_database.py} | 5 +- backend/dependencies/dependencies_gear.py | 9 + backend/dependencies/dependencies_global.py | 40 ++ backend/dependencies/dependencies_session.py | 87 +++ backend/dependencies/dependencies_users.py | 5 + backend/main.py | 38 +- backend/models.py | 9 +- backend/processors/__init__.py | 0 backend/processors/activity_processor.py | 360 +++++++++++++ backend/processors/fit_processor.py | 2 + backend/processors/gpx_processor.py | 308 +++++++++++ backend/routers/activities.py | 343 ------------ backend/routers/router_activities.py | 496 ++++++++++++++++++ backend/routers/router_activity_streams.py | 69 +++ backend/routers/router_gear.py | 163 ++++++ .../routers/{session.py => router_session.py} | 57 +- backend/routers/router_users.py | 212 ++++++++ backend/routers/users.py | 317 ----------- ...cess_tokens.py => schema_access_tokens.py} | 33 +- .../{activities.py => schema_activities.py} | 8 +- backend/schemas/schema_activity_streams.py | 13 + backend/schemas/schema_gear.py | 15 + ...rations.py => schema_user_integrations.py} | 0 backend/schemas/{users.py => schema_users.py} | 0 backend/uploads/__init__.py | 0 custom_php.ini | 3 + frontend/activities/activity.php | 140 ++--- frontend/gear/gear.php | 70 +-- frontend/gear/gears.php | 14 +- frontend/inc/func/activities-funcs.php | 32 +- .../inc/func/activities-streams-funcs.php | 4 +- frontend/inc/func/gear-funcs.php | 32 +- frontend/inc/func/session-funcs.php | 7 +- frontend/index.php | 382 +++++++------- frontend/login.php | 1 + frontend/settings/settings.php | 8 +- frontend/users/user.php | 67 +-- requirements.txt | 3 +- 48 files changed, 3258 insertions(+), 1240 deletions(-) delete mode 100644 backend/crud/activities.py rename backend/crud/{access_tokens.py => crud_access_tokens.py} (100%) create mode 100644 backend/crud/crud_activities.py create mode 100644 backend/crud/crud_activity_streams.py create mode 100644 backend/crud/crud_gear.py rename backend/crud/{user_integrations.py => crud_user_integrations.py} (96%) rename backend/crud/{users.py => crud_users.py} (78%) create mode 100644 backend/dependencies/__init__.py create mode 100644 backend/dependencies/dependencies_activities.py create mode 100644 backend/dependencies/dependencies_activity_streams.py rename backend/{dependencies.py => dependencies/dependencies_database.py} (50%) create mode 100644 backend/dependencies/dependencies_gear.py create mode 100644 backend/dependencies/dependencies_global.py create mode 100644 backend/dependencies/dependencies_session.py create mode 100644 backend/dependencies/dependencies_users.py create mode 100644 backend/processors/__init__.py create mode 100644 backend/processors/activity_processor.py create mode 100644 backend/processors/fit_processor.py create mode 100644 backend/processors/gpx_processor.py delete mode 100644 backend/routers/activities.py create mode 100644 backend/routers/router_activities.py create mode 100644 backend/routers/router_activity_streams.py create mode 100644 backend/routers/router_gear.py rename backend/routers/{session.py => router_session.py} (66%) create mode 100644 backend/routers/router_users.py delete mode 100644 backend/routers/users.py rename backend/schemas/{access_tokens.py => schema_access_tokens.py} (85%) rename backend/schemas/{activities.py => schema_activities.py} (83%) create mode 100644 backend/schemas/schema_activity_streams.py create mode 100644 backend/schemas/schema_gear.py rename backend/schemas/{user_integrations.py => schema_user_integrations.py} (100%) rename backend/schemas/{users.py => schema_users.py} (100%) create mode 100644 backend/uploads/__init__.py create mode 100644 custom_php.ini diff --git a/backend/crud/activities.py b/backend/crud/activities.py deleted file mode 100644 index 599676a9..00000000 --- a/backend/crud/activities.py +++ /dev/null @@ -1,151 +0,0 @@ -import logging - -from operator import and_, or_ -from fastapi import HTTPException, status -from datetime import datetime -from sqlalchemy import func, desc -from sqlalchemy.orm import Session - -from schemas import activities as activities_schemas -import models - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - - -def get_user_activities( - user_id: int, - db: Session, -): - try: - # Get the activities from the database - activities = ( - db.query(models.Activity) - .filter(models.Activity.user_id == user_id) - .order_by(desc(models.Activity.start_time)) - .all() - ) - - # Check if there are activities if not return None - if not activities: - return None - - # Return the activities - return activities - - except Exception as err: - # Log the exception - logger.error(f"Error in get_user_activities: {err}", exc_info=True) - # Raise an HTTPException with a 500 Internal Server Error status code - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err - - -def get_user_activities_with_pagination( - user_id: int, db: Session, page_number: int = 1, num_records: int = 5 -): - try: - # Get the activities from the database - activities = ( - db.query(models.Activity) - .filter(models.Activity.user_id == user_id) - .order_by(desc(models.Activity.start_time)) - .offset((page_number - 1) * num_records) - .limit(num_records) - .all() - ) - - # Check if there are activities if not return None - if not activities: - return None - - # Return the activities - return activities - - except Exception as err: - # Log the exception - logger.error(f"Error in get_user_activities_with_pagination: {err}", exc_info=True) - # Raise an HTTPException with a 500 Internal Server Error status code - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err - - -def get_user_activities_per_timeframe( - user_id: int, - start: datetime, - end: datetime, - db: Session, -): - """Get the activities of a user for a given week""" - try: - # Get the activities from the database - activities = ( - db.query(models.Activity) - .filter( - models.Activity.user_id == user_id, - func.date(models.Activity.start_time) >= start, - func.date(models.Activity.start_time) <= end, - ) - .order_by(desc(models.Activity.start_time)) - ).all() - - # Check if there are activities if not return None - if not activities: - return None - - # Return the activities - return activities - - except Exception as err: - # Log the exception - logger.error(f"Error in get_user_activities_per_timeframe: {err}", exc_info=True) - # Raise an HTTPException with a 500 Internal Server Error status code - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err - - -def get_user_following_activities_per_timeframe( - user_id: int, - start: datetime, - end: datetime, - db: Session, -): - """Get the activities of the users that the user is following for a given week""" - try: - # Get the activities from the database - activities = ( - db.query(models.Activity) - .filter( - and_( - models.Activity.user_id == user_id, - models.Activity.visibility.in_([0, 1]), - ), - func.date(models.Activity.start_time) >= start, - func.date(models.Activity.start_time) <= end, - ) - .order_by(desc(models.Activity.start_time)) - ).all() - - # Check if there are activities if not return None - if not activities: - return None - - # Return the activities - return activities - - except Exception as err: - # Log the exception - logger.error( - f"Error in get_user_following_activities_per_timeframe: {err}", exc_info=True - ) - # Raise an HTTPException with a 500 Internal Server Error status code - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err diff --git a/backend/crud/access_tokens.py b/backend/crud/crud_access_tokens.py similarity index 100% rename from backend/crud/access_tokens.py rename to backend/crud/crud_access_tokens.py diff --git a/backend/crud/crud_activities.py b/backend/crud/crud_activities.py new file mode 100644 index 00000000..9f835c58 --- /dev/null +++ b/backend/crud/crud_activities.py @@ -0,0 +1,466 @@ +import logging + +from operator import and_, or_ +from fastapi import HTTPException, status +from datetime import datetime +from sqlalchemy import func, desc +from sqlalchemy.orm import Session, joinedload + +import models +from schemas import schema_activities + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def get_user_activities( + user_id: int, + db: Session, +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter(models.Activity.user_id == user_id) + .order_by(desc(models.Activity.start_time)) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error(f"Error in get_user_activities: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_activities_with_pagination( + user_id: int, db: Session, page_number: int = 1, num_records: int = 5 +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter(models.Activity.user_id == user_id) + .order_by(desc(models.Activity.start_time)) + .offset((page_number - 1) * num_records) + .limit(num_records) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error( + f"Error in get_user_activities_with_pagination: {err}", exc_info=True + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_activities_per_timeframe( + user_id: int, + start: datetime, + end: datetime, + db: Session, +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter( + models.Activity.user_id == user_id, + func.date(models.Activity.start_time) >= start, + func.date(models.Activity.start_time) <= end, + ) + .order_by(desc(models.Activity.start_time)) + ).all() + + # Check if there are activities if not return None + if not activities: + return None + + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error( + f"Error in get_user_activities_per_timeframe: {err}", exc_info=True + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_following_activities_per_timeframe( + user_id: int, + start: datetime, + end: datetime, + db: Session, +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter( + and_( + models.Activity.user_id == user_id, + models.Activity.visibility.in_([0, 1]), + ), + func.date(models.Activity.start_time) >= start, + func.date(models.Activity.start_time) <= end, + ) + .order_by(desc(models.Activity.start_time)) + ).all() + + # Check if there are activities if not return None + if not activities: + return None + + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + + except Exception as err: + # Log the exception + logger.error( + f"Error in get_user_following_activities_per_timeframe: {err}", + exc_info=True, + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_following_activities_with_pagination( + user_id: int, page_number: int, num_records: int, db: Session +): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .join( + models.Follower, models.Follower.following_id == models.Activity.user_id + ) + .filter( + and_( + models.Follower.follower_id == user_id, + models.Follower.is_accepted == True, + ), + models.Activity.visibility.in_([0, 1]), + ) + .order_by(desc(models.Activity.start_time)) + .offset((page_number - 1) * num_records) + .limit(num_records) + .options(joinedload(models.Activity.user)) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + # Iterate and format the dates + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + except Exception as err: + # Log the exception + logger.error(f"Error in get_activity_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_following_activities(user_id, db): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity.id) + .join( + models.Follower, models.Follower.following_id == models.Activity.user_id + ) + .filter( + and_( + models.Follower.follower_id == user_id, + models.Follower.is_accepted == True, + ), + models.Activity.visibility.in_([0, 1]), + ) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + # Iterate and format the dates + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + except Exception as err: + # Log the exception + logger.error(f"Error in get_activity_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_user_activities_by_gear_id_and_user_id(user_id: int, gear_id: int, db: Session): + try: + # Get the activities from the database + activities = ( + db.query(models.Activity) + .filter( + models.Activity.user_id == user_id, models.Activity.gear_id == gear_id + ) + .order_by(desc(models.Activity.start_time)) + .all() + ) + + # Check if there are activities if not return None + if not activities: + return None + + # Iterate and format the dates + for activity in activities: + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activities + except Exception as err: + # Log the exception + logger.error(f"Error in get_activity_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_activity_by_id_from_user_id_or_has_visibility( + activity_id: int, user_id: int, db: Session +): + try: + # Get the activities from the database + activity = ( + db.query(models.Activity) + .filter( + or_( + models.Activity.user_id == user_id, + models.Activity.visibility.in_([0, 1]), + ), + models.Activity.id == activity_id, + ) + .first() + ) + + # Check if there are activities if not return None + if not activity: + return None + + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activity + + except Exception as err: + # Log the exception + logger.error( + f"Error in get_activity_by_id_from_user_id_or_has_visibility: {err}", + exc_info=True, + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_activity_by_id_from_user_id(activity_id: int, user_id: int, db: Session): + try: + # Get the activities from the database + activity = ( + db.query(models.Activity) + .filter( + models.Activity.user_id == user_id, + models.Activity.id == activity_id, + ) + .first() + ) + + # Check if there are activities if not return None + if not activity: + return None + + activity.start_time = activity.start_time.strftime("%Y-%m-%d %H:%M:%S") + activity.end_time = activity.end_time.strftime("%Y-%m-%d %H:%M:%S") + activity.created_at = activity.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the activities + return activity + + except Exception as err: + # Log the exception + logger.error(f"Error in get_activity_by_id_from_user_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def create_activity(activity: schema_activities.Activity, db: Session): + """Create a new activity in the database""" + try: + # Create a new activity + db_activity = models.Activity( + user_id=activity.user_id, + distance=activity.distance, + name=activity.name, + activity_type=activity.activity_type, + start_time=activity.start_time, + end_time=activity.end_time, + city=activity.city, + town=activity.town, + country=activity.country, + created_at=func.now(), + elevation_gain=activity.elevation_gain, + elevation_loss=activity.elevation_loss, + pace=activity.pace, + average_speed=activity.average_speed, + average_power=activity.average_power, + calories=activity.calories, + visibility=activity.visibility, + gear_id=activity.gear_id, + strava_gear_id=activity.strava_gear_id, + strava_activity_id=activity.strava_activity_id, + ) + + # Add the activity to the database + db.add(db_activity) + db.commit() + db.refresh(db_activity) + + # Return the user + return db_activity + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in create_activity: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def add_gear_to_activity(activity_id: int, gear_id: int, db: Session): + try: + # Get the activity from the database + activity = ( + db.query(models.Activity).filter(models.Activity.id == activity_id).first() + ) + + # Update the activity + activity.gear_id = gear_id + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in add_gear_to_activity: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def delete_activity(activity_id: int, db: Session): + try: + # Delete the activity + num_deleted = db.query(models.Activity).filter(models.Activity.id == activity_id).delete() + + # Check if the activity was found and deleted + if num_deleted == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Activity with id {activity_id} not found", + ) + + # Commit the transaction + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in delete_user: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err \ No newline at end of file diff --git a/backend/crud/crud_activity_streams.py b/backend/crud/crud_activity_streams.py new file mode 100644 index 00000000..e7aaa325 --- /dev/null +++ b/backend/crud/crud_activity_streams.py @@ -0,0 +1,129 @@ +import logging + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from schemas import schema_activity_streams +import models + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def get_activity_streams(activity_id: int, db: Session): + try: + # Get the activity streams from the database + activity_streams = ( + db.query(models.ActivityStreams) + .filter( + models.ActivityStreams.activity_id == activity_id, + ) + .all() + ) + + # Check if there are activity streams if not return None + if not activity_streams: + return None + + # Return the activity streams + return activity_streams + except Exception as err: + # Log the exception + logger.error(f"Error in get_activity_streams: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_activity_stream_by_type(activity_id: int, stream_type: int, db: Session): + """ + Retrieve activity streams by activity ID and stream type from the database. + + Args: + activity_id (int): The ID of the activity. + stream_type (int): The type of the stream. + db (Session): The database session. + + Returns: + List[ActivityStreams] or None: A list of activity streams matching the given activity ID and stream type, + or None if no activity streams are found. + + Raises: + HTTPException: If there is an error retrieving the activity streams from the database. + """ + try: + # Get the activity stream from the database + activity_stream = ( + db.query(models.ActivityStreams) + .filter( + models.ActivityStreams.activity_id == activity_id, + models.ActivityStreams.stream_type == stream_type, + ) + .first() + ) + + # Check if there are activity stream if not return None + if not activity_stream: + return None + + # Return the activity stream + return activity_stream + except Exception as err: + # Log the exception + logger.error(f"Error in get_activity_stream_by_type: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def create_activity_streams( + activity_streams: [schema_activity_streams.ActivityStreams], db: Session +): + """ + Create a list of ActivityStreams objects in the database. + + Args: + activity_streams (list): A list of ActivityStreams objects. + db (Session): The database session. + + Raises: + HTTPException: If there is an internal server error. + + Returns: + None + """ + try: + # Create a list to store the ActivityStreams objects + streams = [] + + # Iterate over the list of ActivityStreams objects + for stream in activity_streams: + # Create an ActivityStreams object + db_stream = models.ActivityStreams( + activity_id=stream.activity_id, + stream_type=stream.stream_type, + stream_waypoints=stream.stream_waypoints, + strava_activity_stream_id=stream.strava_activity_stream_id, + ) + + # Append the object to the list + streams.append(db_stream) + + # Bulk insert the list of ActivityStreams objects + db.bulk_save_objects(streams) + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in create_activity_streams: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err diff --git a/backend/crud/crud_gear.py b/backend/crud/crud_gear.py new file mode 100644 index 00000000..1b31bec5 --- /dev/null +++ b/backend/crud/crud_gear.py @@ -0,0 +1,242 @@ +import logging + +from fastapi import HTTPException, status +from sqlalchemy import func +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError +from urllib.parse import unquote + +import models +from schemas import schema_gear + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def get_gear_user_by_id(user_id: int, gear_id: int, db: Session): + try: + gear = ( + db.query(models.Gear) + .filter(models.Gear.id == gear_id, models.Gear.user_id == user_id) + .first() + ) + + # Check if gear is None and return None if it is + if gear is None: + return None + + gear.created_at = gear.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the gear + return gear + except Exception as err: + # Log the exception + logger.error(f"Error in get_gear_user_by_id: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_gear_users_with_pagination( + user_id: int, db: Session, page_number: int = 1, num_records: int = 5 +): + try: + # Get the gear by user ID from the database + gear = ( + db.query(models.Gear) + .filter(models.Gear.user_id == user_id) + .order_by(models.Gear.nickname.asc()) + .offset((page_number - 1) * num_records) + .limit(num_records) + .all() + ) + + # Check if gear is None and return None if it is + if gear is None: + return None + + # Format the created_at date + for g in gear: + g.created_at = g.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the gear + return gear + except Exception as err: + # Log the exception + logger.error(f"Error in get_gear_users_with_pagination: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_gear_user(user_id: int, db: Session): + try: + # Get the gear by user ID from the database + gear = db.query(models.Gear).filter(models.Gear.user_id == user_id).all() + + # Check if gear is None and return None if it is + if gear is None: + return None + + # Format the created_at date + for g in gear: + g.created_at = g.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the gear + return gear + except Exception as err: + # Log the exception + logger.error(f"Error in get_gear_user: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_gear_user_by_nickname(user_id: int, nickname: str, db: Session): + try: + # Unquote the nickname and change "+" to whitespace + parsed_nickname = unquote(nickname).replace("+", " ") + + # Get the gear by user ID and nickname from the database + gear = ( + db.query(models.Gear) + .filter( + models.Gear.nickname.like(f"%{parsed_nickname}%"), + models.Gear.user_id == user_id, + ) + .all() + ) + + # Check if gear is None and return None if it is + if gear is None: + return None + + # Format the created_at date + for g in gear: + g.created_at = g.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # return the gear + return gear + except Exception as err: + # Log the exception + logger.error(f"Error in get_gear_user_by_nickname: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def get_gear_by_type_and_user(gear_type: int, user_id: int, db: Session): + try: + # Get the gear by type from the database + gear = ( + db.query(models.Gear) + .filter(models.Gear.gear_type == gear_type, models.Gear.user_id == user_id) + .order_by(models.Gear.nickname) + .all() + ) + + # Check if gear is None and return None if it is + if gear is None: + return None + + # Format the created_at date + for g in gear: + g.created_at = g.created_at.strftime("%Y-%m-%d %H:%M:%S") + + # Return the gear + return gear + except Exception as err: + # Log the exception + logger.error(f"Error in get_gear_by_type_and_user: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +def create_gear(gear: schema_gear.Gear, user_id: int, db: Session): + try: + # Set the created date to now + created_date = func.now() + + # If the created_at date is not None, set it to the created_date + if gear.created_at is not None: + created_date = gear.created_at + + # Create a new gear object + db_gear = models.Gear( + brand=unquote(gear.brand).replace("+", " ") if gear.brand is not None else None, + model=unquote(gear.model).replace("+", " ") if gear.model is not None else None, + nickname=unquote(gear.nickname).replace("+", " "), + gear_type=gear.gear_type, + user_id=user_id, + created_at=created_date, + is_active=True, + ) + + # Add the gear to the database + db.add(db_gear) + db.commit() + db.refresh(db_gear) + + # Return the gear + return db_gear + except IntegrityError as integrity_error: + # Rollback the transaction + db.rollback() + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Duplicate entry error. Check if nickname is unique", + ) from integrity_error + + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in create_gear: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + +def delete_gear(gear_id: int, db: Session): + try: + # Delete the user + num_deleted = db.query(models.Gear).filter(models.Gear.id == gear_id).delete() + + # Check if the user was found and deleted + if num_deleted == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Gear with id {gear_id} not found", + ) + + # Commit the transaction + db.commit() + except Exception as err: + # Rollback the transaction + db.rollback() + + # Log the exception + logger.error(f"Error in delete_gear: {err}", exc_info=True) + + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err \ No newline at end of file diff --git a/backend/crud/user_integrations.py b/backend/crud/crud_user_integrations.py similarity index 96% rename from backend/crud/user_integrations.py rename to backend/crud/crud_user_integrations.py index 56ac58ab..198899ce 100644 --- a/backend/crud/user_integrations.py +++ b/backend/crud/crud_user_integrations.py @@ -1,6 +1,5 @@ import logging -from schemas import user_integrations as user_integrations_schema import models from fastapi import HTTPException, status diff --git a/backend/crud/users.py b/backend/crud/crud_users.py similarity index 78% rename from backend/crud/users.py rename to backend/crud/crud_users.py index 96d4b56f..cee0a8cb 100644 --- a/backend/crud/users.py +++ b/backend/crud/crud_users.py @@ -5,7 +5,7 @@ from sqlalchemy.exc import IntegrityError from urllib.parse import unquote -from schemas import users as users_schema +from schemas import schema_users import models # Define a loggger created on main.py @@ -13,12 +13,35 @@ def format_user_birthdate(user): + """ + Formats the birthdate of a user object. + + Args: + user (User): The user object to format. + + Returns: + User: The user object with the birthdate formatted as a string in the format "YYYY-MM-DD", + or None if the birthdate is None. + """ user.birthdate = user.birthdate.strftime("%Y-%m-%d") if user.birthdate else None return user def authenticate_user(username: str, password: str, db: Session): - """Get the user from the database and verify the password""" + """ + Get the user from the database and verify the password. + + Args: + username (str): The username of the user. + password (str): The password of the user. + db (Session): The database session. + + Returns: + User: The authenticated user if the password is correct, None otherwise. + + Raises: + HTTPException: If there is an internal server error. + """ try: # Get the user from the database user = ( @@ -46,7 +69,18 @@ def authenticate_user(username: str, password: str, db: Session): def get_users_number(db: Session): - """Get the number of users in the database""" + """ + Get the number of users in the database. + + Args: + db (Session): The database session. + + Returns: + int: The number of users in the database. + + Raises: + HTTPException: If there is an error retrieving the number of users. + """ try: return db.query(models.User).count() except Exception as err: @@ -60,7 +94,20 @@ def get_users_number(db: Session): def get_users_with_pagination(db: Session, page_number: int = 1, num_records: int = 5): - """Get the users from the database with pagination""" + """ + Get the users from the database with pagination. + + Args: + db (Session): The database session. + page_number (int, optional): The page number for pagination. Defaults to 1. + num_records (int, optional): The number of records per page. Defaults to 5. + + Returns: + List[User] or None: The list of users or None if no users found. + + Raises: + HTTPException: If there is an internal server error. + """ try: # Get the users from the database users = ( @@ -91,7 +138,18 @@ def get_users_with_pagination(db: Session, page_number: int = 1, num_records: in def get_user_by_username(username: str, db: Session): - """Get the user from the database by username""" + """ + Get the user from the database by username. + + Args: + username (str): The username of the user to retrieve. + db (Session): The database session. + + Returns: + List[User]: A list of User objects matching the username. + Raises: + HTTPException: If there is an internal server error. + """ try: # Define a search term partial_username = unquote(username).replace("+", " ") @@ -124,7 +182,18 @@ def get_user_by_username(username: str, db: Session): def get_user_by_id(user_id: int, db: Session): - """Get the user from the database by id""" + """ + Get the user from the database by id. + + Args: + user_id (int): The id of the user. + db (Session): The database session. + + Returns: + User: The user object if found, None otherwise. + Raises: + HTTPException: If there is an internal server error. + """ try: # Get the user from the database user = db.query(models.User).filter(models.User.id == user_id).first() @@ -149,7 +218,19 @@ def get_user_by_id(user_id: int, db: Session): def get_user_id_by_username(username: str, db: Session): - """Get the user id from the database by username""" + """ + Get the user id from the database by username. + + Args: + username (str): The username of the user. + db (Session): The database session. + + Returns: + int or None: The user id if found, None otherwise. + + Raises: + HTTPException: If there is an internal server error. + """ try: # Get the user from the database user_id = ( @@ -175,6 +256,19 @@ def get_user_id_by_username(username: str, db: Session): def get_user_photo_path_by_id(user_id: int, db: Session): + """ + Retrieve the photo path of a user by their ID. + + Args: + user_id (int): The ID of the user. + db (Session): The database session. + + Returns: + str: The photo path of the user. + + Raises: + HTTPException: If there is an internal server error. + """ try: # Get the user from the database user_db = ( @@ -198,6 +292,19 @@ def get_user_photo_path_by_id(user_id: int, db: Session): def get_user_photo_path_aux_by_id(user_id: int, db: Session): + """ + Retrieve the photo_path_aux value of a user from the database by user ID. + + Args: + user_id (int): The ID of the user. + db (Session): The database session. + + Returns: + str: The photo_path_aux value of the user. + + Raises: + HTTPException: If there is an error retrieving the user or a 500 Internal Server Error occurs. + """ try: # Get the user from the database user_db = ( @@ -222,8 +329,20 @@ def get_user_photo_path_aux_by_id(user_id: int, db: Session): ) from err -def create_user(user: users_schema.UserCreate, db: Session): - """Create a new user in the database""" +def create_user(user: schema_users.UserCreate, db: Session): + """ + Create a new user in the database. + + Args: + user (schema_users.UserCreate): The user data to be created. + db (Session): The database session. + + Returns: + models.User: The created user. + + Raises: + HTTPException: If there is a duplicate entry error or an internal server error occurs. + """ try: # Create a new user db_user = models.User( @@ -271,8 +390,7 @@ def create_user(user: users_schema.UserCreate, db: Session): ) from err -def edit_user(user: users_schema.User, db: Session): - """Edit the user in the database""" +def edit_user(user: schema_users.User, db: Session): try: # Get the user from the database db_user = db.query(models.User).filter(models.User.id == user.id).first() @@ -327,7 +445,6 @@ def edit_user(user: users_schema.User, db: Session): def edit_user_password(user_id: int, password: str, db: Session): - """Edit the user password in the database""" try: # Get the user from the database db_user = db.query(models.User).filter(models.User.id == user_id).first() @@ -352,7 +469,6 @@ def edit_user_password(user_id: int, password: str, db: Session): def delete_user_photo(user_id: int, db: Session): - """Delete the user photo path in the database""" try: # Get the user from the database db_user = db.query(models.User).filter(models.User.id == user_id).first() @@ -378,7 +494,6 @@ def delete_user_photo(user_id: int, db: Session): def delete_user(user_id: int, db: Session): - """Delete the user in the database""" try: # Delete the user num_deleted = db.query(models.User).filter(models.User.id == user_id).delete() diff --git a/backend/dependencies/__init__.py b/backend/dependencies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/dependencies/dependencies_activities.py b/backend/dependencies/dependencies_activities.py new file mode 100644 index 00000000..21d86676 --- /dev/null +++ b/backend/dependencies/dependencies_activities.py @@ -0,0 +1,9 @@ +from dependencies import dependencies_global + +def validate_activity_id(activity_id: int): + # Check if id higher than 0 + dependencies_global.validate_id(id=activity_id, min=0, message="Invalid activity ID") + +def validate_week_number(week_number: int): + # Check if gear type is between 0 and 52 + dependencies_global.validate_type(type=week_number, min=0, max=52, message="Invalid week number") diff --git a/backend/dependencies/dependencies_activity_streams.py b/backend/dependencies/dependencies_activity_streams.py new file mode 100644 index 00000000..518f623a --- /dev/null +++ b/backend/dependencies/dependencies_activity_streams.py @@ -0,0 +1,5 @@ +from dependencies import dependencies_global + +def validate_activity_stream_type(stream_type: int): + # Check if gear type is between 1 and 3 + dependencies_global.validate_type(type=stream_type, min=1, max=7, message="Invalid activity stream type") \ No newline at end of file diff --git a/backend/dependencies.py b/backend/dependencies/dependencies_database.py similarity index 50% rename from backend/dependencies.py rename to backend/dependencies/dependencies_database.py index 8e5a07d6..d32c9bd7 100644 --- a/backend/dependencies.py +++ b/backend/dependencies/dependencies_database.py @@ -1,9 +1,12 @@ from database import SessionLocal def get_db(): - # get DB ssession + # Create a new database session and return it db = SessionLocal() + try: + # Yield the database session yield db finally: + # Close the database session db.close() \ No newline at end of file diff --git a/backend/dependencies/dependencies_gear.py b/backend/dependencies/dependencies_gear.py new file mode 100644 index 00000000..30dbc97b --- /dev/null +++ b/backend/dependencies/dependencies_gear.py @@ -0,0 +1,9 @@ +from dependencies import dependencies_global + +def validate_gear_id(gear_id: int): + # Check if id higher than 0 + dependencies_global.validate_id(id=gear_id, min=0, message="Invalid gear ID") + +def validate_gear_type(gear_type: int): + # Check if gear type is between 1 and 3 + dependencies_global.validate_type(type=gear_type, min=1, max=3, message="Invalid gear type") \ No newline at end of file diff --git a/backend/dependencies/dependencies_global.py b/backend/dependencies/dependencies_global.py new file mode 100644 index 00000000..b67b9b22 --- /dev/null +++ b/backend/dependencies/dependencies_global.py @@ -0,0 +1,40 @@ +from fastapi import HTTPException, status + + +def validate_id(id: int, min: int, message: str): + # Check if id higher than 0 + if not (int(id) > min): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=message, + ) + + +def validate_type(type: int, min: int, max: id, message: str): + # Check if gear_type is between 1 and 3 + if not (min <= int(type) <= max): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=message, + ) + + +def validate_pagination_values( + page_number: int, + num_records: int, +): + # Check if page_number higher than 0 + if not (int(page_number) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid Page Number", + ) + + # Check if num_records higher than 0 + if not (int(num_records) > 0): + # Raise an HTTPException with a 422 Unprocessable Entity status code + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid Number of Records", + ) diff --git a/backend/dependencies/dependencies_session.py b/backend/dependencies/dependencies_session.py new file mode 100644 index 00000000..9de4ade1 --- /dev/null +++ b/backend/dependencies/dependencies_session.py @@ -0,0 +1,87 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from dependencies import dependencies_database +from schemas import schema_access_tokens, schema_users + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +def validate_token(token: str = Depends(oauth2_scheme), db: Session = Depends(dependencies_database.get_db)): + # Validate the token expiration + schema_access_tokens.validate_token_expiration(db, token) + + +def validate_token_and_get_authenticated_user_id( + token: str = Depends(oauth2_scheme), db: Session = Depends(dependencies_database.get_db) +): + # Validate the token expiration + schema_access_tokens.validate_token_expiration(db, token) + + # Return the user ID associated with the token + return schema_access_tokens.get_token_user_id(token) + + +def validate_token_and_validate_admin_access( + token: str = Depends(oauth2_scheme), db: Session = Depends(dependencies_database.get_db) +): + # Validate the token expiration + schema_access_tokens.validate_token_expiration(db, token) + + # Check if the token has admin access + schema_access_tokens.validate_token_admin_access(token) + + +def validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access( + user_id: int | None, + token: str = Depends(oauth2_scheme), + db: Session = Depends(dependencies_database.get_db), +): + # Validate the token expiration + schema_access_tokens.validate_token_expiration(db, token) + + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Check if token id is different from user id. If yes, check if the token has admin access + if user_id != schema_access_tokens.get_token_user_id(token): + # Check if the token has admin access + schema_access_tokens.validate_token_admin_access(token) + + +def validate_token_and_if_user_id_equals_token_user_attributtes_id_if_not_validate_admin_access( + user_attributtes: schema_users.User, + token: str = Depends(oauth2_scheme), + db: Session = Depends(dependencies_database.get_db), +): + validate_token_user_id_admin_access(db, token, user_attributtes.id) + + +def validate_token_and_if_user_id_equals_token_user_attributtes_password_id_if_not_validate_admin_access( + user_attributtes: schema_users.UserEditPassword, + token: str = Depends(oauth2_scheme), + db: Session = Depends(dependencies_database.get_db), +): + validate_token_user_id_admin_access(db, token, user_attributtes.id) + + +def validate_token_user_id_admin_access(db, token, user_id): + # Validate the token expiration + schema_access_tokens.validate_token_expiration(db, token) + + # Check if user_id higher than 0 + if not (int(user_id) > 0): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Invalid user ID", + ) + + # Check if token id is different from user id. If yes, check if the token has admin access + if user_id != schema_access_tokens.get_token_user_id(token): + # Check if the token has admin access + schema_access_tokens.validate_token_admin_access(token) \ No newline at end of file diff --git a/backend/dependencies/dependencies_users.py b/backend/dependencies/dependencies_users.py new file mode 100644 index 00000000..e8ae554d --- /dev/null +++ b/backend/dependencies/dependencies_users.py @@ -0,0 +1,5 @@ +from dependencies import dependencies_global + +def validate_user_id(user_id: int): + # Check if id higher than 0 + dependencies_global.validate_id(id=user_id, min=0, message="Invalid user ID") \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 61905001..8bc97e0e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,13 @@ import logging from fastapi import FastAPI -from routers import session as session_router, users as users_router, activities as activities_router +from routers import ( + router_session, + router_users, + router_activities, + router_activity_streams, + router_gear, +) from constants import API_VERSION from database import engine import models @@ -11,7 +17,7 @@ # Define the FastAPI object app = FastAPI( docs_url="/docs", - redoc_url=None, + redoc_url="/redoc", title="Endurain", summary="Endurain API for the Endurain app", version=API_VERSION, @@ -35,9 +41,29 @@ logger.addHandler(file_handler) # Check for required environment variables -required_env_vars = ["DB_HOST", "DB_PORT", "DB_USER", "DB_PASSWORD", "DB_DATABASE", "SECRET_KEY", "ALGORITHM", "ACCESS_TOKEN_EXPIRE_MINUTES", "STRAVA_CLIENT_ID", "STRAVA_CLIENT_SECRET", "STRAVA_AUTH_CODE", "JAEGER_ENABLED", "JAEGER_PROTOCOL", "JAEGER_HOST", "JAGGER_PORT", "STRAVA_DAYS_ACTIVITIES_ONLINK", "API_ENDPOINT"] +required_env_vars = [ + "DB_HOST", + "DB_PORT", + "DB_USER", + "DB_PASSWORD", + "DB_DATABASE", + "SECRET_KEY", + "ALGORITHM", + "ACCESS_TOKEN_EXPIRE_MINUTES", + "STRAVA_CLIENT_ID", + "STRAVA_CLIENT_SECRET", + "STRAVA_AUTH_CODE", + "JAEGER_ENABLED", + "JAEGER_PROTOCOL", + "JAEGER_HOST", + "JAGGER_PORT", + "STRAVA_DAYS_ACTIVITIES_ONLINK", + "API_ENDPOINT", +] # Router files -app.include_router(session_router.router) -app.include_router(users_router.router) -app.include_router(activities_router.router) \ No newline at end of file +app.include_router(router_session.router) +app.include_router(router_users.router) +app.include_router(router_activities.router) +app.include_router(router_activity_streams.router) +app.include_router(router_gear.router) diff --git a/backend/models.py b/backend/models.py index f8d92265..27c4adee 100644 --- a/backend/models.py +++ b/backend/models.py @@ -13,6 +13,7 @@ from sqlalchemy.dialects.mysql import JSON from database import Base + # Data model for followers table using SQLAlchemy's ORM class Follower(Base): __tablename__ = "followers" @@ -197,7 +198,11 @@ class Gear(Base): String(length=45), nullable=True, comment="Gear model (May include spaces)" ) nickname = Column( - String(length=45), nullable=False, comment="Gear nickname (May include spaces)" + String(length=45), + unique=True, + index=True, + nullable=False, + comment="Gear nickname (May include spaces)", ) gear_type = Column( Integer, nullable=False, comment="Gear type (1 - bike, 2 - shoes, 3 - wetsuit)" @@ -329,4 +334,4 @@ class ActivityStreams(Base): ) # Define a relationship to the User model - activity = relationship("Activity", back_populates="activities_streams") \ No newline at end of file + activity = relationship("Activity", back_populates="activities_streams") diff --git a/backend/processors/__init__.py b/backend/processors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/processors/activity_processor.py b/backend/processors/activity_processor.py new file mode 100644 index 00000000..3df3933a --- /dev/null +++ b/backend/processors/activity_processor.py @@ -0,0 +1,360 @@ +import logging +import os +import requests +import math + +from datetime import datetime +from urllib.parse import urlencode +from statistics import mean + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def location_based_on_coordinates(latitude, longitude): + """Get the location based on the coordinates using the geocode API. + + Args: + latitude (float): The latitude coordinate. + longitude (float): The longitude coordinate. + + Returns: + dict: A dictionary containing the location information, including city, town, and country. + """ + # Create a dictionary with the parameters for the request + url_params = { + "lat": latitude, + "lon": longitude, + "api_key=": os.environ.get("GEOCODES_MAPS_API"), + } + + # Create the URL for the request + url = f"https://geocode.maps.co/reverse?{urlencode(url_params)}" + + # Make the request and get the response + try: + # Make the request and get the response + response = requests.get(url) + response.raise_for_status() + + # Get the data from the response + data = response.json().get("address", {}) + + # Return the data + return { + "city": data.get("city"), + "town": data.get("town"), + "country": data.get("country"), + } + + except requests.exceptions.RequestException as err: + # Log the error + logger.error( + f"Error in upload_file querying local from geocode: {err}", + exc_info=True, + ) + + +import math + +def calculate_distance(lat1, lon1, lat2, lon2): + """Calculate the distance between two points on the Earth's surface using the Haversine formula. + + Args: + lat1 (float): Latitude of the first point in degrees. + lon1 (float): Longitude of the first point in degrees. + lat2 (float): Latitude of the second point in degrees. + lon2 (float): Longitude of the second point in degrees. + + Returns: + float: The distance between the two points in meters. + """ + # The radius of the Earth in meters (mean value) + EARTH_RADIUS = 6371000 # 6,371 km = 6,371,000 meters + + # Convert latitude and longitude from degrees to radians + lat1_rad, lon1_rad, lat2_rad, lon2_rad = map(math.radians, [lat1, lon1, lat2, lon2]) + + # Haversine formula + lat_diff = lat2_rad - lat1_rad + lon_diff = lon2_rad - lon1_rad + a = ( + math.sin(lat_diff / 2) ** 2 + + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(lon_diff / 2) ** 2 + ) + c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + # Calculate the distance + distance = EARTH_RADIUS * c + + # Return the distance + return distance + + +def calculate_instant_speed( + prev_time, time, latitude, longitude, prev_latitude, prev_longitude +): + """Calculate the instant speed based on two consecutive waypoints and their timestamps. + + Args: + prev_time (datetime): The timestamp of the previous waypoint. + time (datetime): The timestamp of the current waypoint. + latitude (float): The latitude of the current waypoint. + longitude (float): The longitude of the current waypoint. + prev_latitude (float): The latitude of the previous waypoint. + prev_longitude (float): The longitude of the previous waypoint. + + Returns: + float: The calculated instant speed in meters per second. + """ + # Convert the time strings to datetime objects + time_calc = datetime.fromisoformat(time.strftime("%Y-%m-%dT%H:%M:%S")) + + # If prev_time is None, return a default value + if prev_time is None: + return 0 + + # Convert the time strings to datetime objects + prev_time_calc = datetime.fromisoformat(prev_time.strftime("%Y-%m-%dT%H:%M:%S")) + + # Calculate the time difference in seconds + time_difference = (time_calc - prev_time_calc).total_seconds() + + # If the time difference is positive, calculate the instant speed + if time_difference > 0: + # Calculate the distance in meters + distance = calculate_distance( + prev_latitude, prev_longitude, latitude, longitude + ) + + # Calculate the instant speed in m/s + instant_speed = distance / time_difference + else: + # If the time difference is not positive, return a default value + instant_speed = 0 + + # Return the instant speed + return instant_speed + + +def calculate_elevation_gain_loss(waypoints): + """Calculate the elevation gain and loss based on the waypoints. + + Args: + waypoints (list): A list of dictionaries representing the waypoints. Each dictionary should have an "ele" key representing the elevation. + + Returns: + dict: A dictionary containing the elevation gain and loss. The keys are "elevation_gain" and "elevation_loss". + """ + # Initialize the variables for the elevation gain and loss + elevation_gain = 0 + elevation_loss = 0 + prev_elevation = None + + # Iterate over the waypoints and calculate the elevation gain and loss + for waypoint in waypoints: + # Get the elevation from the waypoint + elevation = waypoint["ele"] + + # If prev_elevation is not None, calculate the elevation change + if prev_elevation is not None: + # Calculate the elevation change + elevation_change = elevation - prev_elevation + if elevation_change > 0: + # If the elevation change is positive, add it to the elevation gain + elevation_gain += elevation_change + else: + # If the elevation change is negative, add its absolute value to the elevation loss + elevation_loss -= elevation_change + + # Update the prev_elevation variable + prev_elevation = elevation + + # Return the elevation gain and loss + return {"elevation_gain": elevation_gain, "elevation_loss": elevation_loss} + + +def calculate_pace(distance, first_waypoint_time, last_waypoint_time): + """Calculate the pace based on the distance and the time between two waypoints. + + Args: + distance (float): The distance between two waypoints in meters. + first_waypoint_time (datetime): The time of the first waypoint. + last_waypoint_time (datetime): The time of the last waypoint. + + Returns: + float: The pace in seconds per meter. + """ + # If the distance is 0, return 0 + if distance == 0: + return 0 + + # Convert the time strings to datetime objects + start_datetime = datetime.fromisoformat( + first_waypoint_time.strftime("%Y-%m-%dT%H:%M:%S") + ) + end_datetime = datetime.fromisoformat( + last_waypoint_time.strftime("%Y-%m-%dT%H:%M:%S") + ) + + # Calculate the time difference in seconds + total_time_in_seconds = (end_datetime - start_datetime).total_seconds() + + # Calculate pace in seconds per meter + pace_seconds_per_meter = total_time_in_seconds / distance + + # Return the pace + return pace_seconds_per_meter + + +def calculate_average_speed(distance, first_waypoint_time, last_waypoint_time): + """Calculate the average speed based on the distance and the time between two waypoints. + + Args: + distance (float): The distance between two waypoints in meters. + first_waypoint_time (datetime): The timestamp of the first waypoint. + last_waypoint_time (datetime): The timestamp of the last waypoint. + + Returns: + float: The average speed in meters per second. + """ + # If the distance is 0, return 0 + if distance == 0: + return 0 + + # Convert the time strings to datetime objects + start_datetime = datetime.fromisoformat( + first_waypoint_time.strftime("%Y-%m-%dT%H:%M:%S") + ) + end_datetime = datetime.fromisoformat( + last_waypoint_time.strftime("%Y-%m-%dT%H:%M:%S") + ) + + # Calculate the time difference in seconds + total_time_in_seconds = (end_datetime - start_datetime).total_seconds() + + if total_time_in_seconds == 0: + # If the time difference is 0, return 0 + return 0 + + # Calculate average speed in meters per second + average_speed = distance / total_time_in_seconds + + # Return the average speed + return average_speed + + +def calculate_average_power(waypoints): + """ + Calculate the average power based on the power values in the waypoints. + + Parameters: + - waypoints (list): A list of dictionaries representing waypoints, each containing a "power" key. + + Returns: + - float: The average power calculated from the power values in the waypoints. + """ + try: + # Get the power values from the waypoints + power_values = [float(waypoint["power"]) for waypoint in waypoints] + except (ValueError, KeyError): + # If there are no valid power values, return 0 + return 0 + + if power_values: + # Calculate the average power + average_power = mean(power_values) + + # Return the average power + return average_power + else: + # If there are no power values, return 0 + return 0 + + +def define_activity_type(activity_type): + """Define the activity type based on the activity type string. + + Args: + activity_type (str): The activity type string. + + Returns: + int: The defined activity type. + + """ + # Default value + auxType = 10 + + # Define the mapping for the activity types + type_mapping = { + "Run": 1, + "running": 1, + "trail running": 2, + "TrailRun": 2, + "VirtualRun": 3, + "cycling": 4, + "Ride": 4, + "GravelRide": 5, + "EBikeRide": 6, + "VirtualRide": 7, + "virtual_ride": 7, + "swimming": 8, + "open_water_swimming": 8, + "Walk": 9, + } + # "AlpineSki", + # "BackcountrySki", + # "Badminton", + # "Canoeing", + # "Crossfit", + # "EBikeRide", + # "Elliptical", + # "EMountainBikeRide", + # "Golf", + # "GravelRide", + # "Handcycle", + # "HighIntensityIntervalTraining", + # "Hike", + # "IceSkate", + # "InlineSkate", + # "Kayaking", + # "Kitesurf", + # "MountainBikeRide", + # "NordicSki", + # "Pickleball", + # "Pilates", + # "Racquetball", + # "Ride", + # "RockClimbing", + # "RollerSki", + # "Rowing", + # "Run", + # "Sail", + # "Skateboard", + # "Snowboard", + # "Snowshoe", + # "Soccer", + # "Squash", + # "StairStepper", + # "StandUpPaddling", + # "Surfing", + # "Swim", + # "TableTennis", + # "Tennis", + # "TrailRun", + # "Velomobile", + # "VirtualRide", + # "VirtualRow", + # "VirtualRun", + # "Walk", + # "WeightTraining", + # "Wheelchair", + # "Windsurf", + # "Workout", + # "Yoga" + + # Get the activity type from the mapping + auxType = type_mapping.get(activity_type, 10) + + # Return the activity type + return auxType diff --git a/backend/processors/fit_processor.py b/backend/processors/fit_processor.py new file mode 100644 index 00000000..871bf526 --- /dev/null +++ b/backend/processors/fit_processor.py @@ -0,0 +1,2 @@ +def parse_fit_file(file, user_id): + print("Parsing FIT file") \ No newline at end of file diff --git a/backend/processors/gpx_processor.py b/backend/processors/gpx_processor.py new file mode 100644 index 00000000..4122ce2b --- /dev/null +++ b/backend/processors/gpx_processor.py @@ -0,0 +1,308 @@ +import gpxpy +import gpxpy.gpx +import logging + +from fastapi import HTTPException, status +from sqlalchemy import func + +from processors import activity_processor +from schemas import schema_activities, schema_activity_streams + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +def parse_activity_streams_from_gpx_file(parsed_info: dict, activity_id: int): + """ + Parse activity streams from parsed GPX data and return as a list of ActivityStreams objects. + + Args: + parsed_info (dict): Parsed GPX data containing stream information. + activity_id (int): ID of the activity. + + Returns: + list: List of ActivityStreams objects representing the parsed activity streams. + """ + # Create a list of tuples containing stream type, is_set, and waypoints + stream_data_list = [ + (1, parsed_info["is_heart_rate_set"], parsed_info["hr_waypoints"]), + (2, parsed_info["is_power_set"], parsed_info["power_waypoints"]), + (3, parsed_info["is_cadence_set"], parsed_info["cad_waypoints"]), + (4, parsed_info["is_elevation_set"], parsed_info["ele_waypoints"]), + (5, parsed_info["is_velocity_set"], parsed_info["vel_waypoints"]), + (6, parsed_info["is_velocity_set"], parsed_info["pace_waypoints"]), + ( + 7, + parsed_info["prev_latitude"] is not None + and parsed_info["prev_longitude"] is not None, + parsed_info["lat_lon_waypoints"], + ), + ] + + # Filter the list to include only those with is_set True + stream_data_list = [ + (stream_type, is_set, waypoints) + for stream_type, is_set, waypoints in stream_data_list + if is_set + ] + + # Return activity streams as a list of ActivityStreams objects + return [ + schema_activity_streams.ActivityStreams( + activity_id=activity_id, + stream_type=stream_type, + stream_waypoints=waypoints, + strava_activity_stream_id=None, + ) + for stream_type, is_set, waypoints in stream_data_list + ] + + +def parse_gpx_file(file: str, user_id: int) -> dict: + """ + Parse a GPX file and return parsed data as a dictionary. + + Args: + file (str): The path to the GPX file. + user_id (int): The ID of the user. + + Returns: + dict: A dictionary containing the parsed data, including an Activity object and various waypoints. + + Raises: + HTTPException: If there is an error parsing the GPX file. + """ + # Parse the GPX file + try: + gpx = gpxpy.parse(open(file, "r")) + except Exception as err: + # Log the exception + logger.error(f"Error in parse_gpx_file: {err}", exc_info=True) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Can't open GPX file", + ) from err + + # Initialize default values for various variables + activity_name = "Workout" + activity_type = "Workout" + distance = 0 + first_waypoint_time = None + last_waypoint_time = None + city = None + town = None + country = None + process_one_time_fields = 0 + pace = 0 + + # Arrays to store waypoint data + lat_lon_waypoints = [] + ele_waypoints = [] + hr_waypoints = [] + cad_waypoints = [] + power_waypoints = [] + vel_waypoints = [] + pace_waypoints = [] + + # Initialize variables to store previous latitude and longitude + prev_latitude, prev_longitude = None, None + + # Initialize variables to store whether elevation, power, heart rate, cadence, and velocity are set + is_elevation_set = False + is_power_set = False + is_heart_rate_set = False + is_cadence_set = False + is_velocity_set = False + + # Iterate over tracks in the GPX file + for track in gpx.tracks: + # Set activity name and type if available + activity_name = track.name if track.name else "Workout" + activity_type = track.type if track.type else "Workout" + + # Iterate over segments in each track + for segment in track.segments: + # Iterate over points in each segment + for point in segment.points: + # Extract latitude and longitude from the point + latitude, longitude = point.latitude, point.longitude + + # Calculate distance between waypoints + if prev_latitude is not None and prev_longitude is not None: + distance += activity_processor.calculate_distance( + prev_latitude, prev_longitude, latitude, longitude + ) + + # Extract elevation, time, and location details + elevation, time = point.elevation, point.time + + if elevation != 0: + is_elevation_set = True + + if first_waypoint_time is None: + first_waypoint_time = point.time + + if process_one_time_fields == 0: + # Use geocoding API to get city, town, and country based on coordinates + location_data = activity_processor.location_based_on_coordinates( + latitude, longitude + ) + city = location_data["city"] + town = location_data["town"] + country = location_data["country"] + + process_one_time_fields = 1 + + # Extract heart rate, cadence, and power data from point extensions + heart_rate, cadence, power = 0, 0, 0 + + if point.extensions: + # Iterate through each extension element + for extension in point.extensions: + if extension.tag.endswith("TrackPointExtension"): + hr_element = extension.find( + ".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}hr" + ) + if hr_element is not None: + heart_rate = hr_element.text + cad_element = extension.find( + ".//{http://www.garmin.com/xmlschemas/TrackPointExtension/v1}cad" + ) + if cad_element is not None: + cadence = cad_element.text + elif extension.tag.endswith("power"): + # Extract 'power' value + power = extension.text + + # Check if heart rate, cadence, power are set + if heart_rate != 0: + is_heart_rate_set = True + + if cadence != 0: + is_cadence_set = True + + if power != 0: + is_power_set = True + + # Calculate instant speed, pace, and update waypoint arrays + instant_speed = activity_processor.calculate_instant_speed( + last_waypoint_time, + time, + latitude, + longitude, + prev_latitude, + prev_longitude, + ) + + # Calculate instance pace + instant_pace = 0 + if instant_speed > 0: + instant_pace = 1 / instant_speed + is_velocity_set = True + + # Append waypoint data to respective arrays + if latitude is not None and longitude is not None: + lat_lon_waypoints.append( + { + "time": time.strftime("%Y-%m-%dT%H:%M:%S"), + "lat": latitude, + "lon": longitude, + } + ) + + if elevation is not None: + ele_waypoints.append( + {"time": time.strftime("%Y-%m-%dT%H:%M:%S"), "ele": elevation} + ) + + if heart_rate is not None: + hr_waypoints.append( + {"time": time.strftime("%Y-%m-%dT%H:%M:%S"), "hr": heart_rate} + ) + + if cadence is not None: + cad_waypoints.append( + {"time": time.strftime("%Y-%m-%dT%H:%M:%S"), "cad": cadence} + ) + + if power is not None: + power_waypoints.append( + {"time": time.strftime("%Y-%m-%dT%H:%M:%S"), "power": power} + ) + + if instant_speed is not None and instant_speed != 0: + vel_waypoints.append( + { + "time": time.strftime("%Y-%m-%dT%H:%M:%S"), + "vel": instant_speed, + } + ) + + if instant_pace != 0: + pace_waypoints.append( + { + "time": time.strftime("%Y-%m-%dT%H:%M:%S"), + "pace": instant_pace, + } + ) + + # Update previous latitude, longitude, and last waypoint time + prev_latitude, prev_longitude, last_waypoint_time = ( + latitude, + longitude, + time, + ) + + # Calculate elevation gain/loss, pace, average speed, and average power + elevation_data = activity_processor.calculate_elevation_gain_loss(ele_waypoints) + elevation_gain = elevation_data["elevation_gain"] + elevation_loss = elevation_data["elevation_loss"] + pace = activity_processor.calculate_pace( + distance, first_waypoint_time, last_waypoint_time + ) + + average_speed = activity_processor.calculate_average_speed( + distance, first_waypoint_time, last_waypoint_time + ) + + average_power = activity_processor.calculate_average_power(power_waypoints) + + # Create an Activity object with parsed data + activity = schema_activities.Activity( + user_id=user_id, + name=activity_name, + distance=distance, + activity_type=activity_processor.define_activity_type(activity_type), + start_time=first_waypoint_time.strftime("%Y-%m-%dT%H:%M:%S"), + end_time=last_waypoint_time.strftime("%Y-%m-%dT%H:%M:%S"), + city=city, + town=town, + country=country, + elevation_gain=elevation_gain, + elevation_loss=elevation_loss, + pace=pace, + average_speed=average_speed, + average_power=average_power, + strava_gear_id=None, + strava_activity_id=None, + ) + + # Return parsed data as a dictionary + return { + "activity": activity, + "is_elevation_set": is_elevation_set, + "ele_waypoints": ele_waypoints, + "is_power_set": is_power_set, + "power_waypoints": power_waypoints, + "is_heart_rate_set": is_heart_rate_set, + "hr_waypoints": hr_waypoints, + "is_velocity_set": is_velocity_set, + "vel_waypoints": vel_waypoints, + "pace_waypoints": pace_waypoints, + "is_cadence_set": is_cadence_set, + "cad_waypoints": cad_waypoints, + "lat_lon_waypoints": lat_lon_waypoints, + "prev_latitude": prev_latitude, + "prev_longitude": prev_longitude, + } diff --git a/backend/routers/activities.py b/backend/routers/activities.py deleted file mode 100644 index 8650de54..00000000 --- a/backend/routers/activities.py +++ /dev/null @@ -1,343 +0,0 @@ -import os -import logging -import calendar - -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile -from datetime import datetime, timedelta -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session - -from schemas import ( - activities as activities_schema, - access_tokens as access_tokens_schema, -) -from crud import activities as activities_crud -from dependencies import get_db - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -# Define the API router -router = APIRouter() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - - -@router.get( - "/activities/{user_id}/week/{week_number}", - response_model=list[activities_schema.Activity], - tags=["activities"], -) -async def read_activities_useractivities_week( - user_id: int, - week_number: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the activities for the requested week for the user or the users that the user is following""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Check if week number is higher or equal than 0 - if not (int(week_number) >= 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid week number", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Calculate the start of the requested week - today = datetime.utcnow().date() - start_of_week = today - timedelta(days=(today.weekday() + 7 * week_number)) - end_of_week = start_of_week + timedelta(days=7) - - if user_id == access_tokens_schema.get_token_user_id(token): - # Get all user activities for the requested week if the user is the owner of the token - activities = activities_crud.get_user_activities_per_timeframe( - user_id, start_of_week, end_of_week, db - ) - else: - # Get user following activities for the requested week if the user is not the owner of the token - activities = activities_crud.get_user_following_activities_per_timeframe( - user_id, start_of_week, end_of_week, db - ) - - # Return the activities - return activities - - -@router.get( - "/activities/{user_id}/thisweek/distances", - response_model=activities_schema.ActivityDistances | None, - tags=["activities"], -) -async def read_activities_useractivities_thisweek_distances( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the distances of the activities for the requested week for the user or the users that the user is following""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Calculate the start of the current week - today = datetime.utcnow().date() - start_of_week = today - timedelta( - days=today.weekday() - ) # Monday is the first day of the week, which is denoted by 0 - end_of_week = start_of_week + timedelta(days=7) - - if user_id == access_tokens_schema.get_token_user_id(token): - # Get all user activities for the requested week if the user is the owner of the token - activities = activities_crud.get_user_activities_per_timeframe( - user_id, start_of_week, end_of_week, db - ) - else: - # Get user following activities for the requested week if the user is not the owner of the token - activities = activities_crud.get_user_following_activities_per_timeframe( - user_id, start_of_week, end_of_week, db - ) - - # Check if activities is None - if activities is None: - # Return None if activities is None - return None - - # Return the activities distances for this week - return activities_schema.calculate_activity_distances(activities) - - -@router.get( - "/activities/{user_id}/thismonth/distances", - response_model=activities_schema.ActivityDistances | None, - tags=["activities"], -) -async def read_activities_useractivities_thismonth_distances( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the distances of the activities for the requested month for the user or the users that the user is following""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Calculate the start of the current month - today = datetime.utcnow().date() - start_of_month = today.replace(day=1) - end_of_month = start_of_month.replace( - day=calendar.monthrange(today.year, today.month)[1] - ) - - if user_id == access_tokens_schema.get_token_user_id(token): - # Get all user activities for the requested month if the user is the owner of the token - activities = activities_crud.get_user_activities_per_timeframe( - user_id, start_of_month, end_of_month, db - ) - else: - # Get user following activities for the requested month if the user is not the owner of the token - activities = activities_crud.get_user_following_activities_per_timeframe( - user_id, start_of_month, end_of_month, db - ) - - if activities is None: - # Return None if activities is None - return None - - # Return the activities distances for this month - return activities_schema.calculate_activity_distances(activities) - - -@router.get( - "/activities/{user_id}/thismonth/number", - response_model=int, - tags=["activities"], -) -async def read_activities_useractivities_thismonth_number( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the number of activities for the requested month for the user or the users that the user is following""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Calculate the start of the current month - today = datetime.utcnow().date() - start_of_month = today.replace(day=1) - end_of_month = start_of_month.replace( - day=calendar.monthrange(today.year, today.month)[1] - ) - - if user_id == access_tokens_schema.get_token_user_id(token): - # Get all user activities for the requested month if the user is the owner of the token - activities = activities_crud.get_user_activities_per_timeframe( - user_id, start_of_month, end_of_month, db - ) - else: - # Get user following activities for the requested month if the user is not the owner of the token - activities = activities_crud.get_user_following_activities_per_timeframe( - user_id, start_of_month, end_of_month, db - ) - - return len(activities) - - -@router.get( - "/activities/{user_id}/number", - response_model=int, - tags=["activities"], -) -async def read_activities_useractivities_number( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Get the number of activities for the user - activities = activities_crud.get_user_activities(user_id, db) - - # Check if activities is None and return 0 if it is - if activities is None: - return 0 - - # Return the number of activities - return len(activities) - - -@router.get( - "/activities/{user_id}/page_number/{page_number}/num_records/{num_records}", - response_model=list[activities_schema.Activity] | None, - tags=["activities"], -) -async def read_activities_useractivities_pagination( - user_id: int, - page_number: int, - num_records: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the activities for the user with pagination""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Check if page_number higher than 0 - if not (int(page_number) > 0): - # Raise an HTTPException with a 422 Unprocessable Entity status code - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid Page Number", - ) - - # Check if num_records higher than 0 - if not (int(num_records) > 0): - # Raise an HTTPException with a 422 Unprocessable Entity status code - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid Number of Records", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Get the activities for the user with pagination - activities = activities_crud.get_user_activities_with_pagination(user_id, db, page_number, num_records) - - # Check if activities is None and return None if it is - if activities is None: - return None - - # Return activities - return activities - -@router.post("/activities/{user_id}/create/upload") -async def create_activity_with_uploaded_file( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - file: UploadFile = File(...), - db: Session = Depends(get_db), -): - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - try: - # Ensure the 'uploads' directory exists - upload_dir = "uploads" - os.makedirs(upload_dir, exist_ok=True) - - # Get file extension - _, file_extension = os.path.splitext(file.filename) - - # Save the uploaded file in the 'uploads' directory - with open(file.filename, "wb") as save_file: - save_file.write(file.file.read()) - - # Choose the appropriate parser based on file extension - if file_extension.lower() == ".gpx": - parsed_info = parse_gpx_file(file.filename, user_id) - elif file_extension.lower() == ".fit": - parsed_info = parse_fit_file(file.filename, user_id) - else: - raise HTTPException( - status_code=status.HTTP_406_NOT_ACCEPTABLE, - detail="File extension not supported. Supported file extensions are .gpx and .fit", - ) - - except Exception as err: - # Log the exception - logger.error(f"Error in create_activity_with_uploaded_file: {err}", exc_info=True) - # Raise an HTTPException with a 500 Internal Server Error status code - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Internal Server Error", - ) from err - \ No newline at end of file diff --git a/backend/routers/router_activities.py b/backend/routers/router_activities.py new file mode 100644 index 00000000..4f64bcc9 --- /dev/null +++ b/backend/routers/router_activities.py @@ -0,0 +1,496 @@ +import os +import logging +import calendar + +from typing import Annotated, Callable + +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile +from datetime import datetime, timedelta +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from schemas import schema_activities +from crud import crud_activities, crud_activity_streams, crud_gear +from dependencies import ( + dependencies_database, + dependencies_session, + dependencies_users, + dependencies_activities, + dependencies_gear, + dependencies_global, +) +from processors import gpx_processor, fit_processor + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +@router.get( + "/activities/user/{user_id}/week/{week_number}", + response_model=list[schema_activities.Activity] | None, + tags=["activities"], +) +async def read_activities_useractivities_week( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + week_number: int, + validate_week_number: Annotated[ + Callable, Depends(dependencies_activities.validate_week_number) + ], + token_user_id: Annotated[ + Callable, + Depends(dependencies_session.validate_token_and_get_authenticated_user_id), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Calculate the start of the requested week + today = datetime.utcnow().date() + start_of_week = today - timedelta(days=(today.weekday() + 7 * week_number)) + end_of_week = start_of_week + timedelta(days=7) + + if user_id == token_user_id: + # Get all user activities for the requested week if the user is the owner of the token + activities = crud_activities.get_user_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + else: + # Get user following activities for the requested week if the user is not the owner of the token + activities = crud_activities.get_user_following_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + + # Check if activities is None + if activities is None: + # Return None if activities is None + return None + + # Return the activities + return activities + + +@router.get( + "/activities/user/{user_id}/thisweek/distances", + response_model=schema_activities.ActivityDistances | None, + tags=["activities"], +) +async def read_activities_useractivities_thisweek_distances( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + token_user_id: Annotated[ + Callable, + Depends(dependencies_session.validate_token_and_get_authenticated_user_id), + ], + db: Session = Depends(dependencies_database.get_db), +): + + # Calculate the start of the current week + today = datetime.utcnow().date() + start_of_week = today - timedelta( + days=today.weekday() + ) # Monday is the first day of the week, which is denoted by 0 + end_of_week = start_of_week + timedelta(days=7) + + if user_id == token_user_id: + # Get all user activities for the requested week if the user is the owner of the token + activities = crud_activities.get_user_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + else: + # Get user following activities for the requested week if the user is not the owner of the token + activities = crud_activities.get_user_following_activities_per_timeframe( + user_id, start_of_week, end_of_week, db + ) + + # Check if activities is None + if activities is None: + # Return None if activities is None + return None + + # Return the activities distances for this week + return schema_activities.calculate_activity_distances(activities) + + +@router.get( + "/activities/user/{user_id}/thismonth/distances", + response_model=schema_activities.ActivityDistances | None, + tags=["activities"], +) +async def read_activities_useractivities_thismonth_distances( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + token_user_id: Annotated[ + Callable, + Depends(dependencies_session.validate_token_and_get_authenticated_user_id), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Calculate the start of the current month + today = datetime.utcnow().date() + start_of_month = today.replace(day=1) + end_of_month = start_of_month.replace( + day=calendar.monthrange(today.year, today.month)[1] + ) + + if user_id == token_user_id: + # Get all user activities for the requested month if the user is the owner of the token + activities = crud_activities.get_user_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + else: + # Get user following activities for the requested month if the user is not the owner of the token + activities = crud_activities.get_user_following_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + + if activities is None: + # Return None if activities is None + return None + + # Return the activities distances for this month + return schema_activities.calculate_activity_distances(activities) + + +@router.get( + "/activities/user/{user_id}/thismonth/number", + response_model=int, + tags=["activities"], +) +async def read_activities_useractivities_thismonth_number( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + token_user_id: Annotated[ + Callable, + Depends(dependencies_session.validate_token_and_get_authenticated_user_id), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Calculate the start of the current month + today = datetime.utcnow().date() + start_of_month = today.replace(day=1) + end_of_month = start_of_month.replace( + day=calendar.monthrange(today.year, today.month)[1] + ) + + if user_id == token_user_id: + # Get all user activities for the requested month if the user is the owner of the token + activities = crud_activities.get_user_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + else: + # Get user following activities for the requested month if the user is not the owner of the token + activities = crud_activities.get_user_following_activities_per_timeframe( + user_id, start_of_month, end_of_month, db + ) + + return len(activities) + + +@router.get( + "/activities/user/{user_id}/gear/{gear_id}", + response_model=list[schema_activities.Activity] | None, + tags=["activities"], +) +async def read_activities_gearactivities( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + gear_id: int, + validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], + validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access: Annotated[ + Callable, + Depends( + dependencies_session.validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access + ), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activities for the gear + return crud_activities.get_user_activities_by_gear_id_and_user_id(user_id, gear_id, db) + + +@router.get( + "/activities/user/{user_id}/number", + response_model=int, + tags=["activities"], +) +async def read_activities_useractivities_number( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the number of activities for the user + activities = crud_activities.get_user_activities(user_id, db) + + # Check if activities is None and return 0 if it is + if activities is None: + return 0 + + # Return the number of activities + return len(activities) + + +@router.get( + "/activities/user/{user_id}/page_number/{page_number}/num_records/{num_records}", + response_model=list[schema_activities.Activity] | None, + tags=["activities"], +) +async def read_activities_useractivities_pagination( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + page_number: int, + num_records: int, + validate_pagination_values: Annotated[ + Callable, Depends(dependencies_global.validate_pagination_values) + ], + validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activities for the user with pagination + activities = crud_activities.get_user_activities_with_pagination( + user_id, db, page_number, num_records + ) + + # Check if activities is None and return None if it is + if activities is None: + return None + + # Return activities + return activities + + +@router.get( + "/activities/user/{user_id}/followed/page_number/{page_number}/num_records/{num_records}", + response_model=schema_activities.Activity | None, + tags=["activities"], +) +async def read_activities_followed_user_activities_pagination( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + page_number: int, + num_records: int, + validate_pagination_values: Annotated[ + Callable, Depends(dependencies_global.validate_pagination_values) + ], + validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activities for the following users with pagination + return crud_activities.get_user_following_activities_with_pagination( + user_id, page_number, num_records, db + ) + + +@router.get( + "/activities/user/{user_id}/followed/number", + response_model=int, + tags=["activities"], +) +async def read_activities_followed_useractivities_number( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the number of activities for the following users + activities = crud_activities.get_user_following_activities(user_id, db) + + # Check if activities is None and return 0 if it is + if activities is None: + return 0 + + # Return the number of activities + return len(activities) + + +@router.get( + "/activities/{activity_id}", + response_model=schema_activities.Activity | None, + tags=["activities"], +) +async def read_activities_activity_from_id( + activity_id: int, + validate_activity_id: Annotated[ + Callable, Depends(dependencies_activities.validate_activity_id) + ], + token_user_id: Annotated[ + Callable, + Depends(dependencies_session.validate_token_and_get_authenticated_user_id), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activity from the database and return it + return crud_activities.get_activity_by_id_from_user_id_or_has_visibility(activity_id, token_user_id, db) + + +@router.post( + "/activities/{user_id}/create/upload", + status_code=201, + response_model=int, + tags=["activities"], +) +async def create_activity_with_uploaded_file( + user_id: int, + validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + file: UploadFile, + validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)], + db: Session = Depends(dependencies_database.get_db), +): + try: + # Ensure the 'uploads' directory exists + upload_dir = "uploads" + os.makedirs(upload_dir, exist_ok=True) + + # Get file extension + _, file_extension = os.path.splitext(file.filename) + + # Save the uploaded file in the 'uploads' directory + with open(file.filename, "wb") as save_file: + save_file.write(file.file.read()) + + # Choose the appropriate parser based on file extension + if file_extension.lower() == ".gpx": + # Parse the GPX file + parsed_info = gpx_processor.parse_gpx_file(file.filename, user_id) + elif file_extension.lower() == ".fit": + # Parse the FIT file + parsed_info = fit_processor.parse_fit_file(file.filename, user_id) + else: + # file extension not supported raise an HTTPException with a 406 Not Acceptable status code + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, + detail="File extension not supported. Supported file extensions are .gpx and .fit", + ) + + # create the activity in the database + created_activity = crud_activities.create_activity(parsed_info["activity"], db) + + # Check if created_activity is None + if created_activity is None: + # raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Error creating activity", + ) + + # Parse the activity streams from the parsed info + activity_streams = gpx_processor.parse_activity_streams_from_gpx_file( + parsed_info, created_activity.id + ) + + # Create activity streams in the database + crud_activity_streams.create_activity_streams(activity_streams, db) + + # Remove the file after processing + os.remove(file.filename) + + # Return activity ID + return created_activity.id + except Exception as err: + # Log the exception + logger.error( + f"Error in create_activity_with_uploaded_file: {err}", exc_info=True + ) + # Raise an HTTPException with a 500 Internal Server Error status code + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Internal Server Error", + ) from err + + +@router.put("/activities/{activity_id}/addgear/{gear_id}", + tags=["activities"], +) +async def activity_add_gear( + activity_id: int, + validate_activity_id: Annotated[ + Callable, Depends(dependencies_activities.validate_activity_id) + ], + gear_id: int, + validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], + token_user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the gear by user id and gear id + gear = crud_gear.get_gear_user_by_id(token_user_id, gear_id, db) + + # Check if gear is None and raise an HTTPException with a 404 Not Found status code if it is + if gear is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Gear ID {gear_id} for user {token_user_id} not found", + ) + + # Get the activity by id from user id + activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db) + + # Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is + if activity is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Activity ID {activity_id} for user {token_user_id} not found", + ) + + # Add the gear to the activity + crud_activities.add_gear_to_activity(activity_id, gear_id, db) + + # Return success message + return {"detail": f"Gear ID {gear_id} added to activity successfully"} + +@router.put("/activities/{activity_id}/deletegear", + tags=["activities"], +) +async def delete_activity_gear( + activity_id: int, + validate_activity_id: Annotated[ + Callable, Depends(dependencies_activities.validate_activity_id) + ], + token_user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activity by id from user id + activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db) + + # Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is + if activity is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Activity ID {activity_id} for user {token_user_id} not found", + ) + + # Delete gear from the activity + crud_activities.add_gear_to_activity(activity_id, None, db) + + # Return success message + return {"detail": f"Gear ID {activity.gear_id} deleted from activity successfully"} + +@router.delete("/activities/{activity_id}/delete") +async def delete_activity( + activity_id: int, + validate_activity_id: Annotated[ + Callable, Depends(dependencies_activities.validate_activity_id) + ], + token_user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activity by id from user id + activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db) + + # Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is + if activity is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Activity ID {activity_id} for user {token_user_id} not found", + ) + + # Delete the activity + crud_activities.delete_activity(activity_id, db) + + # Return success message + return {"detail": f"Activity {activity_id} deleted successfully"} + \ No newline at end of file diff --git a/backend/routers/router_activity_streams.py b/backend/routers/router_activity_streams.py new file mode 100644 index 00000000..b988c03a --- /dev/null +++ b/backend/routers/router_activity_streams.py @@ -0,0 +1,69 @@ +import logging + +from typing import Annotated, Callable + +from fastapi import APIRouter, Depends +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from schemas import schema_activity_streams +from crud import crud_activity_streams +from dependencies import ( + dependencies_database, + dependencies_session, + dependencies_activities, + dependencies_activity_streams, +) + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +@router.get( + "/activities/streams/activity_id/{activity_id}/all", + response_model=list[schema_activity_streams.ActivityStreams] | None, + tags=["activity_streams"], +) +async def read_activities_streams_for_activity_all( + activity_id: int, + validate_id: Annotated[ + Callable, Depends(dependencies_activities.validate_activity_id) + ], + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activity streams from the database and return them + return crud_activity_streams.get_activity_streams(activity_id, db) + + +@router.get( + "/activities/streams/activity_id/{activity_id}/stream_type/{stream_type}", + response_model=schema_activity_streams.ActivityStreams | None, + tags=["activity_streams"], +) +async def read_activities_streams_for_activity_stream_type( + activity_id: int, + validate_activity_id: Annotated[ + Callable, Depends(dependencies_activities.validate_activity_id) + ], + stream_type: int, + validate_activity_stream_type: Annotated[ + Callable, Depends(dependencies_activity_streams.validate_activity_stream_type) + ], + validate_token: Annotated[ + Callable, Depends(dependencies_session.validate_token) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the activity stream from the database and return them + return crud_activity_streams.get_activity_stream_by_type( + activity_id, stream_type, db + ) diff --git a/backend/routers/router_gear.py b/backend/routers/router_gear.py new file mode 100644 index 00000000..fa159555 --- /dev/null +++ b/backend/routers/router_gear.py @@ -0,0 +1,163 @@ +import logging + +from typing import Annotated, Callable + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from schemas import schema_gear +from crud import crud_gear +from dependencies import ( + dependencies_database, + dependencies_session, + dependencies_global, + dependencies_gear, +) + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +@router.get( + "/gear/id/{gear_id}", + response_model=schema_gear.Gear | None, + tags=["gear"], +) +async def read_gear_id( + gear_id: int, + validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], + user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Annotated[Session, Depends(dependencies_database.get_db)], +): + # Return the gear + return crud_gear.get_gear_user_by_id(user_id, gear_id, db) + + +@router.get( + "/gear/page_number/{page_number}/num_records/{num_records}", + response_model=list[schema_gear.Gear] | None, + tags=["gear"], +) +async def read_gear_user_pagination( + page_number: int, + num_records: int, + validate_pagination_values: Annotated[ + Callable, Depends(dependencies_global.validate_pagination_values) + ], + user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Return the gear + return crud_gear.get_gear_users_with_pagination( + user_id, db, page_number, num_records + ) + + +@router.get( + "/gear/number", + response_model=int, + tags=["gear"], +) +async def read_gear_user_number( + user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the gear + gear = crud_gear.get_gear_user(user_id, db) + + # Check if gear is None and return 0 if it is + if gear is None: + return 0 + + # Return the number of gears + return len(gear) + + +@router.get( + "/gear/nickname/{nickname}", + response_model=list[schema_gear.Gear] | None, + tags=["gear"], +) +async def read_gear_user_by_nickname( + nickname: str, + user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Return the gear + return crud_gear.get_gear_user_by_nickname(user_id, nickname, db) + + +@router.get( + "/gear/type/{gear_type}", + response_model=list[schema_gear.Gear] | None, + tags=["gear"], +) +async def read_gear_user_by_type( + gear_type: int, + validate_type: Annotated[Callable, Depends(dependencies_gear.validate_gear_type)], + user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Return the gear + return crud_gear.get_gear_by_type_and_user(gear_type, user_id, db) + + +@router.post( + "/gear/create", + status_code=201, + tags=["gear"], +) +async def create_gear( + gear: schema_gear.Gear, + user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Create the gear + gear_created = crud_gear.create_gear(gear, user_id, db) + + # Return the ID of the gear created + return gear_created.id + +@router.delete("/gear/{gear_id}/delete", tags=["gear"]) +async def delete_user( + gear_id: int, + validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], + token_user_id: Annotated[ + int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the gear by id + gear = crud_gear.get_gear_user_by_id(token_user_id, gear_id, db) + + # Check if gear is None and raise an HTTPException if it is + if gear is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Gear ID {gear_id} for user {token_user_id} not found", + ) + + # Delete the gear + crud_gear.delete_gear(gear_id, db) + + # Return success message + return {"detail": f"Gear ID {gear_id} deleted successfully"} \ No newline at end of file diff --git a/backend/routers/session.py b/backend/routers/router_session.py similarity index 66% rename from backend/routers/session.py rename to backend/routers/router_session.py index c92f6851..8053e6fc 100644 --- a/backend/routers/session.py +++ b/backend/routers/router_session.py @@ -1,18 +1,18 @@ import logging from datetime import datetime, timedelta -from typing import Annotated +from typing import Annotated, Callable from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session -from crud import users as users_crud, user_integrations as user_integrations_crud -from schemas import access_tokens as access_tokens_schema, users as users_schema +from crud import crud_user_integrations, crud_users +from schemas import schema_access_tokens, schema_users from constants import ( USER_NOT_ACTIVE, ) -from dependencies import get_db +from dependencies import dependencies_database, dependencies_session # Define the OAuth2 scheme for handling bearer tokens oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @@ -25,9 +25,8 @@ def authenticate_user(username: str, password: str, db: Session): - """Get the user from the database and verify the password""" # Get the user from the database - user = users_crud.authenticate_user(username, password, db) + user = crud_users.authenticate_user(username, password, db) # Check if the user exists and if the password is correct and if not return False if not user: @@ -42,9 +41,8 @@ def authenticate_user(username: str, password: str, db: Session): def get_current_user(db: Session, user_id: int): - """Get the current user from the token and then queries the database to get the user data""" # Get the user from the database - user = users_crud.get_user_by_id(user_id, db) + user = crud_users.get_user_by_id(user_id, db) # If the user does not exist raise the exception if user is None: @@ -53,8 +51,10 @@ def get_current_user(db: Session, user_id: int): detail="Could not validate credentials (user not found)", headers={"WWW-Authenticate": "Bearer"}, ) - - user_integrations = user_integrations_crud.get_user_integrations_by_user_id(user.id, db) + + user_integrations = crud_user_integrations.get_user_integrations_by_user_id( + user.id, db + ) if user_integrations is None: raise HTTPException( @@ -62,7 +62,7 @@ def get_current_user(db: Session, user_id: int): detail="Could not validate credentials (user integrations not found)", headers={"WWW-Authenticate": "Bearer"}, ) - + if user_integrations.strava_token is None: user.is_strava_linked = 0 else: @@ -72,12 +72,14 @@ def get_current_user(db: Session, user_id: int): return user -@router.post("/token", tags=["session"]) +@router.post( + "/token", response_model=schema_access_tokens.AccessToken, tags=["session"] +) async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], do_not_expire: bool = False, - db: Session = Depends(get_db), -) -> access_tokens_schema.AccessToken: + db: Session = Depends(dependencies_database.get_db), +): user = authenticate_user(form_data.username, form_data.password, db) if user.is_active == USER_NOT_ACTIVE: @@ -91,24 +93,29 @@ async def login_for_access_token( if do_not_expire: expire = datetime.utcnow() + timedelta(days=90) - access_token = access_tokens_schema.create_access_token( + access_token = schema_access_tokens.create_access_token( db, data={"sub": user.username, "id": user.id, "access_type": user.access_type}, expires_delta=expire, ) - return access_tokens_schema.AccessToken(access_token=access_token, token_type="bearer") + return schema_access_tokens.AccessToken( + access_token=access_token, token_type="bearer" + ) -@router.get("/users/me", response_model=users_schema.UserMe, tags=["session"]) +@router.get("/users/me", response_model=schema_users.UserMe, tags=["session"]) async def read_users_me( - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), + user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)], + db: Session = Depends(dependencies_database.get_db), ): - # Validate the token - access_tokens_schema.validate_token_expiration(db, token) - - # Get the user id from the payload - user_id = access_tokens_schema.get_token_user_id(token) - return get_current_user(db, user_id) + + +@router.get("/validate_token", tags=["session"]) +async def validate_token( + validate_token: Callable = Depends(dependencies_session.validate_token), + db: Session = Depends(dependencies_database.get_db), +): + # Return None if the token is valid + return None \ No newline at end of file diff --git a/backend/routers/router_users.py b/backend/routers/router_users.py new file mode 100644 index 00000000..ea853738 --- /dev/null +++ b/backend/routers/router_users.py @@ -0,0 +1,212 @@ +import logging + +from typing import Annotated, Callable + +from fastapi import APIRouter, Depends +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from schemas import schema_users +from crud import crud_user_integrations, crud_users +from dependencies import ( + dependencies_database, + dependencies_session, + dependencies_global, + dependencies_users, +) + +# Define the OAuth2 scheme for handling bearer tokens +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Define the API router +router = APIRouter() + +# Define a loggger created on main.py +logger = logging.getLogger("myLogger") + + +@router.get("/users/number", response_model=int, tags=["users"]) +async def read_users_number( + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + return crud_users.get_users_number(db) + + +@router.get( + "/users/all/page_number/{page_number}/num_records/{num_records}", + response_model=list[schema_users.User] | None, + tags=["users"], +) +async def read_users_all_pagination( + page_number: int, + num_records: int, + validate_pagination_values: Annotated[ + Callable, Depends(dependencies_global.validate_pagination_values) + ], + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the users from the database with pagination + return crud_users.get_users_with_pagination( + db=db, page_number=page_number, num_records=num_records + ) + + +@router.get( + "/users/username/{username}", + response_model=list[schema_users.User] | None, + tags=["users"], +) +async def read_users_username( + username: str, + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the users from the database by username + return crud_users.get_user_by_username(username=username, db=db) + + +@router.get("/users/id/{user_id}", response_model=schema_users.User, tags=["users"]) +async def read_users_id( + user_id: int, + validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + validate_token: Callable = Depends(dependencies_session.validate_token), + db: Session = Depends(dependencies_database.get_db), +): + # Get the users from the database by id + return crud_users.get_user_by_id(user_id=user_id, db=db) + + +@router.get("/users/{username}/id", response_model=int, tags=["users"]) +async def read_users_username_id( + username: str, + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the users from the database by username + return crud_users.get_user_id_by_username(username, db) + + +@router.get("/users/{user_id}/photo_path", response_model=str | None, tags=["users"]) +async def read_users_id_photo_path( + user_id: int, + validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the photo_path from the database by id + return crud_users.get_user_photo_path_by_id(user_id, db) + + +@router.get( + "/users/{user_id}/photo_path_aux", response_model=str | None, tags=["users"] +) +async def read_users_id_photo_path_aux( + user_id: int, + validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Get the photo_path_aux from the database by id + return crud_users.get_user_photo_path_aux_by_id(user_id, db) + + +@router.post("/users/create", response_model=int, status_code=201, tags=["users"]) +async def create_user( + user: schema_users.UserCreate, + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Create the user in the database + created_user = crud_users.create_user(user, db) + + # Create the user integrations in the database + crud_user_integrations.create_user_integrations(created_user.id, db) + + # Return the user id + return created_user.id + + +@router.put("/users/edit", tags=["users"]) +async def edit_user( + user_attributtes: schema_users.User, + validate_token_user_id: Annotated[ + Callable, + Depends( + dependencies_session.validate_token_and_if_user_id_equals_token_user_attributtes_id_if_not_validate_admin_access + ), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Update the user in the database + crud_users.edit_user(user_attributtes, db) + + # Return success message + return {"detail": f"User ID {user_attributtes.id} updated successfully"} + + +@router.put("/users/edit/password", tags=["users"]) +async def edit_user_password( + user_attributtes: schema_users.UserEditPassword, + validate_token_user_id: Annotated[ + Callable, + Depends( + dependencies_session.validate_token_and_if_user_id_equals_token_user_attributtes_password_id_if_not_validate_admin_access + ), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Update the user password in the database + crud_users.edit_user_password(user_attributtes.id, user_attributtes.password, db) + + # Return success message + return {"detail": f"User ID {user_attributtes.id} password updated successfully"} + + +@router.put("/users/{user_id}/delete-photo", tags=["users"]) +async def delete_user_photo( + user_id: int, + validate_token_user_id: Annotated[ + Callable, + Depends( + dependencies_session.validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access + ), + ], + db: Session = Depends(dependencies_database.get_db), +): + # Update the user photo_path in the database + crud_users.delete_user_photo(user_id, db) + + # Return success message + return {"detail": f"User ID {user_id} photo deleted successfully"} + + +@router.delete("/users/{user_id}/delete", tags=["users"]) +async def delete_user( + user_id: int, + validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], + validate_token_validate_admin_access: Annotated[ + Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) + ], + db: Session = Depends(dependencies_database.get_db), +): + # Delete the user in the database + crud_users.delete_user(user_id, db) + + # Return success message + return {"detail": f"User ID {user_id} deleted successfully"} diff --git a/backend/routers/users.py b/backend/routers/users.py deleted file mode 100644 index 3eeae961..00000000 --- a/backend/routers/users.py +++ /dev/null @@ -1,317 +0,0 @@ -import logging - -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session - -from schemas import access_tokens as access_tokens_schema, users as users_schema -from crud import users as users_crud, user_integrations as user_integrations_crud -from dependencies import get_db - -# Define the OAuth2 scheme for handling bearer tokens -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -# Define the API router -router = APIRouter() - -# Define a loggger created on main.py -logger = logging.getLogger("myLogger") - - -@router.get("/users/number", response_model=int, tags=["users"]) -async def read_users_number( - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the number of users in the database""" - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Get the number of users in the database - return users_crud.get_users_number(db) - - -@router.get( - "/users/all/page_number/{page_number}/num_records/{num_records}", - response_model=list[users_schema.User], - tags=["users"], -) -async def read_users_all_pagination( - page_number: int, - num_records: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the users from the database with pagination""" - # Check if page_number higher than 0 - if not (int(page_number) > 0): - # Raise an HTTPException with a 422 Unprocessable Entity status code - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid Page Number", - ) - - # Check if num_records higher than 0 - if not (int(num_records) > 0): - # Raise an HTTPException with a 422 Unprocessable Entity status code - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid Number of Records", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Get the users from the database with pagination - return users_crud.get_users_with_pagination( - db=db, page_number=page_number, num_records=num_records - ) - - -@router.get( - "/users/username/{username}", response_model=list[users_schema.User], tags=["users"] -) -async def read_users_username( - username: str, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the users from the database by username""" - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Get the users from the database by username - return users_crud.get_user_by_username(username=username, db=db) - - -@router.get("/users/id/{user_id}", response_model=users_schema.User, tags=["users"]) -async def read_users_id( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the users from the database by id""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - # Raise an HTTPException with a 422 Unprocessable Entity status code - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Get the users from the database by id - return users_crud.get_user_by_id(user_id=user_id, db=db) - - -@router.get("/users/{username}/id", response_model=int, tags=["users"]) -async def read_users_username_id( - username: str, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the users from the database by username and return the user id""" - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Get the users from the database by username - return users_crud.get_user_id_by_username(username, db) - - -@router.get("/users/{user_id}/photo_path", response_model=str | None, tags=["users"]) -async def read_users_id_photo_path( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the photo_path from the database by id""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Get the photo_path from the database by id - return users_crud.get_user_photo_path_by_id(user_id, db) - - -@router.get( - "/users/{user_id}/photo_path_aux", response_model=str | None, tags=["users"] -) -async def read_users_id_photo_path_aux( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Get the photo_path_aux from the database by id""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Get the photo_path_aux from the database by id - return users_crud.get_user_photo_path_aux_by_id(user_id, db) - - -@router.post("/users/create", response_model=int, status_code=201, tags=["users"]) -async def create_user( - user: users_schema.UserCreate, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Create a new user in the database""" - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Create the user in the database - created_user = users_crud.create_user(user, db) - - # Create the user integrations in the database - user_integrations_crud.create_user_integrations(created_user.id, db) - - # Return the user id - return created_user.id - - -@router.put("/users/edit", tags=["users"]) -async def edit_user( - user_attributtes: users_schema.User, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Edit a user in the database""" - # Check if user_id higher than 0 - if not (int(user_attributtes.id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Update the user in the database - users_crud.edit_user(user_attributtes, db) - - # Return success message - return {"detail": f"User ID {user_attributtes.id} updated successfully"} - - -@router.put("/users/edit/password", tags=["users"]) -async def edit_user_password( - user_attributtes: users_schema.UserEditPassword, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Edit a user password in the database""" - # Check if user_id higher than 0 - if not (int(user_attributtes.id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if token id is different of user id. If yes checks if the token has admin access - if user_attributtes.id != access_tokens_schema.get_token_user_id(token): - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Update the user password in the database - users_crud.edit_user_password(user_attributtes.id, user_attributtes.password, db) - - # Return success message - return {"detail": f"User ID {user_attributtes.id} password updated successfully"} - - -@router.put("/users/{user_id}/delete-photo", tags=["users"]) -async def delete_user_photo( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - """Delete a user photo in the database""" - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if token id is different of user id. If yes checks if the token has admin access - if user_id != access_tokens_schema.get_token_user_id(token): - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Update the user photo_path in the database - users_crud.delete_user_photo(user_id, db) - - # Return success message - return {"detail": f"User ID {user_id} photo deleted successfully"} - -@router.delete("/users/{user_id}/delete", tags=["users"]) -async def delete_user( - user_id: int, - token: Annotated[str, Depends(oauth2_scheme)], - db: Session = Depends(get_db), -): - # Check if user_id higher than 0 - if not (int(user_id) > 0): - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Invalid user ID", - ) - - # Validate the token expiration - access_tokens_schema.validate_token_expiration(db, token) - - # Check if the token has admin access - access_tokens_schema.validate_token_admin_access(token) - - # Delete the user in the database - users_crud.delete_user(user_id, db) - - # Return success message - return {"detail": f"User ID {user_id} deleted successfully"} \ No newline at end of file diff --git a/backend/schemas/access_tokens.py b/backend/schemas/schema_access_tokens.py similarity index 85% rename from backend/schemas/access_tokens.py rename to backend/schemas/schema_access_tokens.py index fe3b0176..875e8d0d 100644 --- a/backend/schemas/access_tokens.py +++ b/backend/schemas/schema_access_tokens.py @@ -7,7 +7,7 @@ from jose import JWTError, jwt from sqlalchemy.orm import Session -from crud import access_tokens as access_tokens_crud +from crud import crud_access_tokens from constants import ( JWT_EXPIRATION_IN_MINUTES, JWT_ALGORITHM, @@ -50,12 +50,11 @@ class CreateToken(Token): def decode_token(token: str = Depends(oauth2_scheme)): - """Decode the token and return the payload""" + # Decode the token and return the payload return jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) def validate_token_expiration(db: Session, token: str = Depends(oauth2_scheme)): - """Validate the token and check if it is expired""" # Try to decode the token and check if it is expired try: # Decode the token @@ -78,13 +77,27 @@ def validate_token_expiration(db: Session, token: str = Depends(oauth2_scheme)): detail="Token no longer valid", headers={"WWW-Authenticate": "Bearer"}, ) - - except jwt.ExpiredSignatureError: + except Exception: + # Log the error and raise the exception + logger.info( + "Token expired during validation | Will force remove_expired_tokens to run | Returning 401 response" + ) + # Remove expired tokens from the database + remove_expired_tokens(db) + # Raise an HTTPException with a 401 Unauthorized status code + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token no longer valid", + headers={"WWW-Authenticate": "Bearer"}, + ) + """ except jwt.ExpiredSignatureError: # Log the error and raise the exception logger.info( "Token expired during validation | Will force remove_expired_tokens to run | Returning 401 response" ) + # Remove expired tokens from the database remove_expired_tokens(db) + # Raise an HTTPException with a 401 Unauthorized status code raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token no longer valid", @@ -99,11 +112,10 @@ def validate_token_expiration(db: Session, token: str = Depends(oauth2_scheme)): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal Server Error", - ) from err + ) from err """ def get_token_user_id(token: str = Depends(oauth2_scheme)): - """Get the user id from the token""" # Decode the token payload = decode_token(token) @@ -123,7 +135,6 @@ def get_token_user_id(token: str = Depends(oauth2_scheme)): def get_token_access_type(token: str = Depends(oauth2_scheme)): - """Get the admin access from the token""" # Decode the token payload = decode_token(token) @@ -154,7 +165,6 @@ def validate_token_admin_access(token: str = Depends(oauth2_scheme)): def create_access_token( db: Session, data: dict, expires_delta: timedelta | None = None ): - """Creates a new JWT token with the provided data and expiration time""" # Create a copy of the data to encode to_encode = data.copy() @@ -173,7 +183,7 @@ def create_access_token( encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) # Save the token in the database - db_access_token = access_tokens_crud.create_access_token( + db_access_token = crud_access_tokens.create_access_token( CreateToken( token=encoded_jwt, user_id=data.get("id"), @@ -190,12 +200,11 @@ def create_access_token( def remove_expired_tokens(db: Session): - """Remove expired tokens from the database""" # Calculate the expiration time expiration_time = datetime.utcnow() - timedelta(minutes=JWT_EXPIRATION_IN_MINUTES) # Delete the expired tokens from the database - rows_deleted = access_tokens_crud.delete_access_tokens(expiration_time, db) + rows_deleted = crud_access_tokens.delete_access_tokens(expiration_time, db) # Log the number of tokens deleted logger.info(f"{rows_deleted} access tokens deleted from the database") diff --git a/backend/schemas/activities.py b/backend/schemas/schema_activities.py similarity index 83% rename from backend/schemas/activities.py rename to backend/schemas/schema_activities.py index a4aad338..3b3003d4 100644 --- a/backend/schemas/activities.py +++ b/backend/schemas/schema_activities.py @@ -3,6 +3,7 @@ class Activity(BaseModel): id: int | None = None + user_id: int | None = None distance: int name: str activity_type: str @@ -11,12 +12,15 @@ class Activity(BaseModel): city: str | None = None town: str | None = None country: str | None = None + created_at: str | None = None elevation_gain: int elevation_loss: int pace: float average_speed: float average_power: int calories: int | None = None + visibility: int | None = None + gear_id: int | None = None strava_gear_id: int | None = None strava_activity_id: int | None = None @@ -39,9 +43,9 @@ def calculate_activity_distances(activities: list[Activity]): for activity in activities: if activity.activity_type in [1, 2, 3]: run += activity.distance - elif activity.activity_type in [4, 5, 6, 7, 8]: + elif activity.activity_type in [4, 5, 6, 7]: bike += activity.distance - elif activity.activity_type == 9: + elif activity.activity_type in [8, 9]: swim += activity.distance # Return the distances diff --git a/backend/schemas/schema_activity_streams.py b/backend/schemas/schema_activity_streams.py new file mode 100644 index 00000000..045e2e3a --- /dev/null +++ b/backend/schemas/schema_activity_streams.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel +from typing import List + + +class ActivityStreams(BaseModel): + id: int | None = None + activity_id: int + stream_type: str + stream_waypoints: List[dict] + strava_activity_stream_id: int | None = None + + class Config: + orm_mode = True \ No newline at end of file diff --git a/backend/schemas/schema_gear.py b/backend/schemas/schema_gear.py new file mode 100644 index 00000000..bc384b00 --- /dev/null +++ b/backend/schemas/schema_gear.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + +class Gear(BaseModel): + id: int | None = None + brand: str | None = None + model: str | None = None + nickname: str + gear_type: int + user_id: int | None = None + created_at: str + is_active: bool | None = None + strava_gear_id: int | None = None + + class Config: + orm_mode = True \ No newline at end of file diff --git a/backend/schemas/user_integrations.py b/backend/schemas/schema_user_integrations.py similarity index 100% rename from backend/schemas/user_integrations.py rename to backend/schemas/schema_user_integrations.py diff --git a/backend/schemas/users.py b/backend/schemas/schema_users.py similarity index 100% rename from backend/schemas/users.py rename to backend/schemas/schema_users.py diff --git a/backend/uploads/__init__.py b/backend/uploads/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/custom_php.ini b/custom_php.ini new file mode 100644 index 00000000..816e68c0 --- /dev/null +++ b/custom_php.ini @@ -0,0 +1,3 @@ +; Custom php.ini +upload_max_filesize = 20M +post_max_size = 26M \ No newline at end of file diff --git a/frontend/activities/activity.php b/frontend/activities/activity.php index 729d34da..555dd317 100755 --- a/frontend/activities/activity.php +++ b/frontend/activities/activity.php @@ -14,14 +14,17 @@ if (!isLogged()) { header("Location: ../login.php"); + die(); } if (!isTokenValid($_SESSION["token"])) { header("Location: ../logout.php?sessionExpired=1"); + die(); } if (!isset($_GET["activityID"])) { header("Location: ../index.php?invalidActivity=1"); + die(); } // Load the language file based on the user's preferred language @@ -57,15 +60,16 @@ $deleteActivity = deleteActivity($_GET["activityID"]); if ($deleteActivity == 0) { header("Location: ../index.php?deleteActivity=1"); + die(); } } $activity = getActivityFromId($_GET["activityID"]); if ($activity == NULL) { header("Location: ../index.php?invalidActivity=1"); + die(); } - -$activityStreams = getActivityActivitiesStream($activity[0]["id"]); +$activityStreams = getActivityActivitiesStream($activity["id"]); $hrStream = []; $cadStream = []; $powerStream = []; @@ -98,7 +102,7 @@ $latlonStream = $stream["stream_waypoints"]; } #$velStream[] = (float) number_format(($waypoint['vel'] * 3.6), 0); - if ($activity[0]["activity_type"] == 1 || $activity[0]["activity_type"] == 2 || $activity[0]["activity_type"] == 3) { + if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) { if($stream["stream_type"] == 6){ foreach($stream["stream_waypoints"] as $paceData){ if ($paceData['pace'] == 0 || $paceData['pace'] == null) { @@ -109,7 +113,7 @@ } } } else { - if ($activity[0]["activity_type"] == 9) { + if ($activity["activity_type"] == 9) { if($stream["stream_type"] == 6){ foreach($stream["stream_waypoints"] as $paceData){ if ($paceData['pace'] == 0 || $paceData['pace'] == null) { @@ -123,20 +127,22 @@ } } -$activityUser = getUserFromId($activity[0]['user_id']); +$activityUser = getUserFromId($activity['user_id']); -if ($activity[0]["gear_id"] != null) { - $activityGear = getGearFromId($activity[0]["gear_id"]); +if ($activity["gear_id"] != null) { + $activityGear = getGearFromId($activity["gear_id"]); } -if ($activity[0]["activity_type"] == 1 || $activity[0]["activity_type"] == 2 || $activity[0]["activity_type"] == 3) { - $activityGearOptions = getGearFromType(2); -} else { - if ($activity[0]["activity_type"] == 4 || $activity[0]["activity_type"] == 5 || $activity[0]["activity_type"] == 6 || $activity[0]["activity_type"] == 7 || $activity[0]["activity_type"] == 8) { - $activityGearOptions = getGearFromType(1); +if($activityUser["id"] == $_SESSION["id"]){ + if ($activity["activity_type"] == 1 || $activity["activity_type"] == 2 || $activity["activity_type"] == 3) { + $activityGearOptions = getGearFromType(2); } else { - if ($activity[0]["activity_type"] == 9) { - $activityGearOptions = getGearFromType(3); + if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7) { + $activityGearOptions = getGearFromType(1); + } else { + if ($activity["activity_type"] == 8 || $activity["activity_type"] == 9) { + $activityGearOptions = getGearFromType(3); + } } } } @@ -191,44 +197,44 @@
- alt="userPicture" class="rounded-circle" width="55" height="55">
- '; } else { - if ($activity[0]["activity_type"] == 4 || $activity[0]["activity_type"] == 5 || $activity[0]["activity_type"] == 6 || $activity[0]["activity_type"] == 7 || $activity[0]["activity_type"] == 8) { + if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7 || $activity["activity_type"] == 8) { echo ''; } else { - if ($activity[0]["activity_type"] == 9) { + if ($activity["activity_type"] == 9) { echo ''; } } } ?> - format("d/m/y"); ?>@ - format("H:i"); ?> - format("d/m/y"); ?>@ + format("H:i"); ?> + - -
@@ -238,9 +244,9 @@
@@ -305,8 +311,8 @@
diff($endDateTime); if ($interval->h < 1) { @@ -319,27 +325,27 @@ ?>
- +
- m + m - +
- + min/km - +
- + min/100m @@ -348,14 +354,14 @@
- +

- W + W @@ -365,31 +371,31 @@
- m + m

- m + m
- +

- km/h + km/h

- W + W @@ -399,7 +405,7 @@
- m + m
@@ -461,25 +467,25 @@ className: 'bg-danger dot'
- '; } else { - if ($activity[0]["activity_type"] == 4 || $activity[0]["activity_type"] == 5 || $activity[0]["activity_type"] == 6 || $activity[0]["activity_type"] == 7 || $activity[0]["activity_type"] == 8) { + if ($activity["activity_type"] == 4 || $activity["activity_type"] == 5 || $activity["activity_type"] == 6 || $activity["activity_type"] == 7 || $activity["activity_type"] == 8) { echo ''; } else { - if ($activity[0]["activity_type"] == 9) { + if ($activity["activity_type"] == 9) { echo ''; } } } ?> - + - +

- + @@ -497,7 +503,7 @@ className: 'bg-danger dot' aria-label="Close">
&addGearToActivity=1" + action="../activities/activity.php?activityID=&addGearToActivity=1" method="post" enctype="multipart/form-data"> &editGearActivity=1" + action="../activities/activity.php?activityID=&editGearActivity=1" method="post" enctype="multipart/form-data"> @@ -641,7 +647,7 @@ className: 'bg-danger dot'
- +