Backend optimization
7
.gitignore
vendored
@@ -4,5 +4,8 @@ backend/*/__pycache__/
|
|||||||
backend/*.pyc
|
backend/*.pyc
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
backend/logs/
|
backend/logs/*.log
|
||||||
backend/*.log
|
backend/*.log
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
frontend/img/users_img/*.*
|
||||||
@@ -91,6 +91,9 @@ JAEGER_PROTOCOL | http | Yes
|
|||||||
JAEGER_HOST | jaeger | Yes
|
JAEGER_HOST | jaeger | Yes
|
||||||
JAGGER_PORT | 4317 | Yes
|
JAGGER_PORT | 4317 | Yes
|
||||||
STRAVA_DAYS_ACTIVITIES_ONLINK | 30 | Yes
|
STRAVA_DAYS_ACTIVITIES_ONLINK | 30 | Yes
|
||||||
|
API_ENDPOINT* | changeme | Yes
|
||||||
|
|
||||||
|
*API_ENDPOINT needs to be set if you want to enable Strava integration
|
||||||
|
|
||||||
Table bellow shows the obligatory environemnt variables for mariadb container. You should set them based on what was also set for backend container.
|
Table bellow shows the obligatory environemnt variables for mariadb container. You should set them based on what was also set for backend container.
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ JAEGER_ENABLED=true
|
|||||||
JAEGER_PROTOCOL=http
|
JAEGER_PROTOCOL=http
|
||||||
JAEGER_HOST=jaeger
|
JAEGER_HOST=jaeger
|
||||||
JAGGER_PORT=4317
|
JAGGER_PORT=4317
|
||||||
STRAVA_DAYS_ACTIVITIES_ONLINK=30
|
STRAVA_DAYS_ACTIVITIES_ONLINK=30
|
||||||
|
API_ENDPOINT=changeme
|
||||||
@@ -416,7 +416,15 @@ async def read_activities_useractivities_thisweek_distances(
|
|||||||
distances = calculate_activity_distances(activity_records)
|
distances = calculate_activity_distances(activity_records)
|
||||||
|
|
||||||
# Return the queried values using JSONResponse
|
# Return the queried values using JSONResponse
|
||||||
return JSONResponse(content=distances)
|
#return JSONResponse(content=distances)
|
||||||
|
|
||||||
|
# Include metadata in the response
|
||||||
|
metadata = {"total_records": 1}
|
||||||
|
|
||||||
|
# Return the queried values using JSONResponse
|
||||||
|
return JSONResponse(
|
||||||
|
content={"metadata": metadata, "content": distances}
|
||||||
|
)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
# Return an error response if the user is not authenticated
|
# Return an error response if the user is not authenticated
|
||||||
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
|
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
|
||||||
@@ -479,7 +487,15 @@ async def read_activities_useractivities_thismonth_distances(
|
|||||||
distances = calculate_activity_distances(activity_records)
|
distances = calculate_activity_distances(activity_records)
|
||||||
|
|
||||||
# Return the queried values using JSONResponse
|
# Return the queried values using JSONResponse
|
||||||
return JSONResponse(content=distances)
|
#return JSONResponse(content=distances)
|
||||||
|
|
||||||
|
# Include metadata in the response
|
||||||
|
metadata = {"total_records": 1}
|
||||||
|
|
||||||
|
# Return the queried values using JSONResponse
|
||||||
|
return JSONResponse(
|
||||||
|
content={"metadata": metadata, "content": distances}
|
||||||
|
)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
# Return an error response if the user is not authenticated
|
# Return an error response if the user is not authenticated
|
||||||
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
|
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Logger:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from . import sessionController
|
from . import sessionController
|
||||||
from jose import JWTError
|
from jose import JWTError
|
||||||
|
|||||||
@@ -1,58 +1,224 @@
|
|||||||
import os
|
"""
|
||||||
import logging
|
Authentication and User Management Module
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from jose import jwt, JWTError
|
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from db.db import (
|
|
||||||
User,
|
|
||||||
AccessToken,
|
|
||||||
) # Import your SQLAlchemy session management from db.db and models
|
|
||||||
from controllers.userController import UserResponse
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from dependencies import get_db_session
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Define the API router
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# Define a loggger created on main.py
|
||||||
logger = logging.getLogger("myLogger")
|
logger = logging.getLogger("myLogger")
|
||||||
|
|
||||||
|
# Define the OAuth2 scheme for handling bearer tokens
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
|
|
||||||
|
|
||||||
def get_user_id_from_token(token: str):
|
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:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token,
|
token,
|
||||||
os.environ.get("SECRET_KEY"),
|
os.environ.get("SECRET_KEY"),
|
||||||
algorithms=[os.environ.get("ALGORITHM")],
|
algorithms=[os.environ.get("ALGORITHM")],
|
||||||
)
|
)
|
||||||
return payload.get("id")
|
return payload
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
# 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)):
|
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:
|
try:
|
||||||
payload = jwt.decode(
|
return decode_token(token).get("exp")
|
||||||
token,
|
|
||||||
os.environ.get("SECRET_KEY"),
|
|
||||||
algorithms=[os.environ.get("ALGORITHM")],
|
|
||||||
)
|
|
||||||
return payload.get("exp")
|
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
# 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)):
|
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:
|
try:
|
||||||
payload = jwt.decode(
|
return decode_token(token).get("access_type")
|
||||||
token,
|
|
||||||
os.environ.get("SECRET_KEY"),
|
|
||||||
algorithms=[os.environ.get("ALGORITHM")],
|
|
||||||
)
|
|
||||||
return payload.get("access_type")
|
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
# 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(
|
async def authenticate_user(
|
||||||
@@ -61,6 +227,26 @@ async def authenticate_user(
|
|||||||
neverExpires: bool,
|
neverExpires: bool,
|
||||||
db_session: Session,
|
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:
|
try:
|
||||||
# Use SQLAlchemy ORM to query the database
|
# Use SQLAlchemy ORM to query the database
|
||||||
user = (
|
user = (
|
||||||
@@ -69,8 +255,8 @@ async def authenticate_user(
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
return create_error_response(
|
||||||
status_code=400, detail="Incorrect username or password"
|
"BAD_REQUEST", "Incorrect username or password", 400
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if there is an existing access token for the user
|
# Check if there is an existing access token for the user
|
||||||
@@ -93,9 +279,12 @@ async def authenticate_user(
|
|||||||
|
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as err:
|
||||||
logger.error(e)
|
# Log the error and return an error response
|
||||||
return False
|
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(
|
async def create_access_token(
|
||||||
@@ -104,34 +293,79 @@ async def create_access_token(
|
|||||||
db_session: Session,
|
db_session: Session,
|
||||||
expires_delta: timedelta = None,
|
expires_delta: timedelta = None,
|
||||||
):
|
):
|
||||||
to_encode = data.copy()
|
"""
|
||||||
if never_expire:
|
Create and store a new access token.
|
||||||
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})
|
Parameters:
|
||||||
encoded_jwt = jwt.encode(
|
- data (dict): The payload data to be encoded in the access token.
|
||||||
to_encode, os.environ.get("SECRET_KEY"), algorithm=os.environ.get("ALGORITHM")
|
- 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.
|
||||||
|
|
||||||
# Insert the access token into the database using SQLAlchemy
|
Returns:
|
||||||
access_token = AccessToken(
|
- Union[str, Tuple[str, str, int]]: The generated access token,
|
||||||
token=encoded_jwt,
|
or a tuple representing an error response if token creation fails.
|
||||||
user_id=data["id"],
|
|
||||||
created_at=datetime.utcnow(),
|
|
||||||
expires_at=expire,
|
|
||||||
)
|
|
||||||
|
|
||||||
db_session.add(access_token)
|
This function creates a new access token by encoding the provided payload data.
|
||||||
db_session.commit()
|
The token is then stored in the database, and the generated token string is returned.
|
||||||
|
|
||||||
return encoded_jwt
|
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):
|
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:
|
try:
|
||||||
# Calculate the expiration time
|
# Calculate the expiration time
|
||||||
expiration_time = datetime.utcnow() - timedelta(
|
expiration_time = datetime.utcnow() - timedelta(
|
||||||
@@ -148,24 +382,45 @@ def remove_expired_tokens(db_session: Session):
|
|||||||
|
|
||||||
logger.info(f"{rows_deleted} access tokens deleted from the database")
|
logger.info(f"{rows_deleted} access tokens deleted from the database")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
# Log the error and return an error response
|
||||||
logger.error(f"Error in remove_expired_tokens: {err}", exc_info=True)
|
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)):
|
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:
|
try:
|
||||||
validate_token(db_session=db_session, token=token)
|
validate_token(db_session=db_session, token=token)
|
||||||
|
|
||||||
user_id = get_user_id_from_token(token)
|
user_id = get_user_id_from_token(token)
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
raise HTTPException(
|
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
|
||||||
status_code=401, detail="Invalid authentication credentials"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Retrieve the user details from the database using the user ID
|
# Retrieve the user details from the database using the user ID
|
||||||
user = db_session.query(User).filter(User.id == user_id).first()
|
user = db_session.query(User).filter(User.id == user_id).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
return create_error_response("NOT_FOUND", "User not found", 404)
|
||||||
|
|
||||||
if user.strava_token is None:
|
if user.strava_token is None:
|
||||||
is_strava_linked = 0
|
is_strava_linked = 0
|
||||||
@@ -191,10 +446,35 @@ def get_user_data(db_session: Session, token: str = Depends(oauth2_scheme)):
|
|||||||
|
|
||||||
return user_data
|
return user_data
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
# 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):
|
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:
|
try:
|
||||||
user_id = get_user_id_from_token(token)
|
user_id = get_user_id_from_token(token)
|
||||||
|
|
||||||
@@ -206,56 +486,159 @@ def validate_token(db_session: Session, token: str):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
if access_token:
|
if not access_token or datetime.utcnow() > datetime.fromtimestamp(exp):
|
||||||
expiration_datetime = datetime.fromtimestamp(exp)
|
|
||||||
current_time = datetime.utcnow()
|
|
||||||
if current_time > expiration_datetime:
|
|
||||||
raise JWTError("Token expired")
|
|
||||||
else:
|
|
||||||
return {"message": "Token is valid"}
|
|
||||||
else:
|
|
||||||
raise JWTError("Token expired")
|
raise JWTError("Token expired")
|
||||||
|
else:
|
||||||
# if 'exp' not in decoded_token:
|
return {"message": "Token is valid"}
|
||||||
# return {"message": "Token is valid"}
|
except JWTError as jwt_error:
|
||||||
# else:
|
raise jwt_error
|
||||||
except JWTError:
|
except Exception as err:
|
||||||
raise JWTError("Invalid token")
|
logger.error(f"Error in token validation: {err}", exc_info=True)
|
||||||
|
raise JWTError("Token validation failed")
|
||||||
|
|
||||||
|
|
||||||
def validate_admin_access(token: str):
|
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:
|
try:
|
||||||
user_access_type = get_access_type_from_token(token)
|
user_access_type = get_access_type_from_token(token)
|
||||||
if user_access_type != 2:
|
if user_access_type != 2:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise JWTError("Invalid token")
|
raise JWTError("Invalid token")
|
||||||
|
|
||||||
|
|
||||||
class CreateTokenRequest(BaseModel):
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
neverExpires: bool
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/token")
|
@router.post("/token")
|
||||||
async def login_for_access_token(
|
async def login_for_access_token(
|
||||||
token: CreateTokenRequest, db_session: Session = Depends(get_db_session)
|
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(
|
access_token = await authenticate_user(
|
||||||
token.username, token.password, token.neverExpires, db_session
|
token.username, token.password, token.neverExpires, db_session
|
||||||
)
|
)
|
||||||
if not access_token:
|
if not access_token:
|
||||||
raise HTTPException(status_code=400, detail="Unable to retrieve access token")
|
return create_error_response(
|
||||||
|
"BAD_REQUEST", "Unable to retrieve access token", 400
|
||||||
|
)
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
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}")
|
@router.delete("/logout/{user_id}")
|
||||||
async def logout(
|
async def logout(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
token: str = Depends(oauth2_scheme),
|
token: str = Depends(oauth2_scheme),
|
||||||
db_session: Session = Depends(get_db_session),
|
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:
|
try:
|
||||||
access_token = (
|
access_token = (
|
||||||
db_session.query(AccessToken)
|
db_session.query(AccessToken)
|
||||||
@@ -267,24 +650,11 @@ async def logout(
|
|||||||
db_session.commit()
|
db_session.commit()
|
||||||
return {"message": "Logged out successfully"}
|
return {"message": "Logged out successfully"}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=404, detail="Token not found")
|
return create_error_response("NOT_FOUND", "Token not found", 404)
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
raise HTTPException(status_code=500, detail="Failed to invalidate access token")
|
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
@router.get("/validate_token")
|
# Log the error and return an error response
|
||||||
async def check_validate_token(
|
logger.error(f"Error in logout: {err}", exc_info=True)
|
||||||
token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session)
|
return create_error_response(
|
||||||
):
|
"INTERNAL_SERVER_ERROR", "Internal Server Error", 500
|
||||||
try:
|
)
|
||||||
return validate_token(db_session, token)
|
|
||||||
except JWTError:
|
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/me", response_model=UserResponse)
|
|
||||||
async def read_users_me(
|
|
||||||
token: str = Depends(oauth2_scheme), db_session: Session = Depends(get_db_session)
|
|
||||||
):
|
|
||||||
return get_user_data(db_session, token)
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Logger:
|
|||||||
- Logger named "myLogger" for logging errors and exceptions.
|
- Logger named "myLogger" for logging errors and exceptions.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def configure_logger():
|
|||||||
Returns:
|
Returns:
|
||||||
- logging.Logger: Configured logger instance.
|
- logging.Logger: Configured logger instance.
|
||||||
"""
|
"""
|
||||||
logging.config.fileConfig('logging_config.ini')
|
logging.config.fileConfig('logs/logging_config.ini')
|
||||||
return logging.getLogger('myLogger')
|
return logging.getLogger('myLogger')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
backend/logs/__init__.py
Normal file
@@ -62,6 +62,8 @@ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|||||||
# Import OS module for handling environment variables
|
# Import OS module for handling environment variables
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
# Import database-related functions and dependencies
|
# Import database-related functions and dependencies
|
||||||
from db.db import create_database_tables
|
from db.db import create_database_tables
|
||||||
from dependencies import get_db_session, configure_logger
|
from dependencies import get_db_session, configure_logger
|
||||||
@@ -70,10 +72,20 @@ from dependencies import get_db_session, configure_logger
|
|||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# Create loggger
|
# Create loggger
|
||||||
logger = configure_logger()
|
#logger = configure_logger()
|
||||||
|
logger = logging.getLogger("myLogger")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler("app.log")
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
# Check for required environment variables
|
# 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"]
|
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:
|
for var in required_env_vars:
|
||||||
if var not in os.environ:
|
if var not in os.environ:
|
||||||
@@ -96,6 +108,8 @@ def startup_event():
|
|||||||
logger.info("Backend startup event")
|
logger.info("Backend startup event")
|
||||||
# Create the database and tables if they don't exist
|
# Create the database and tables if they don't exist
|
||||||
create_database_tables()
|
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():
|
def shutdown_event():
|
||||||
|
|||||||
0
frontend/.env
Normal file → Executable file
0
frontend/activities/activity.php
Normal file → Executable file
0
frontend/gear/gear.php
Normal file → Executable file
0
frontend/gear/gears.php
Normal file → Executable file
0
frontend/img/avatar/bicycle1.png
Normal file → Executable file
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
0
frontend/img/avatar/bicycle2.png
Normal file → Executable file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
frontend/img/avatar/female1.png
Normal file → Executable file
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
0
frontend/img/avatar/male1.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
0
frontend/img/avatar/running_shoe1.png
Normal file → Executable file
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
0
frontend/img/avatar/running_shoe2.png
Normal file → Executable file
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
0
frontend/img/avatar/wetsuit1.png
Normal file → Executable file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
frontend/img/avatar/wetsuit2.png
Normal file → Executable file
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
0
frontend/img/logo/logo.png
Normal file → Executable file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
0
frontend/img/strava/api_logo_cptblWith_strava_horiz_light.png
Normal file → Executable file
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
0
frontend/img/strava/api_logo_cptblWith_strava_stack_light.png
Normal file → Executable file
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
0
frontend/img/strava/btn_strava_connectwith_orange.png
Normal file → Executable file
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -55,7 +55,7 @@ function getUserActivitiesThisWeekDistances($userID)
|
|||||||
return -1;
|
return -1;
|
||||||
} else {
|
} else {
|
||||||
if ($response[1] === 200) {
|
if ($response[1] === 200) {
|
||||||
return json_decode($response[0], true);
|
return json_decode($response[0], true)["content"];
|
||||||
} else {
|
} else {
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ function getUserActivitiesThisMonthDistances($userID)
|
|||||||
return -1;
|
return -1;
|
||||||
} else {
|
} else {
|
||||||
if ($response[1] === 200) {
|
if ($response[1] === 200) {
|
||||||
return json_decode($response[0], true);
|
return json_decode($response[0], true)["content"];
|
||||||
} else {
|
} else {
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ function getActivitiesPagination($pageNumber, $numRecords)
|
|||||||
return -1;
|
return -1;
|
||||||
} else {
|
} else {
|
||||||
if ($response[1] === 200) {
|
if ($response[1] === 200) {
|
||||||
return json_decode($response[0], true);
|
return json_decode($response[0], true)["content"];
|
||||||
} else {
|
} else {
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|||||||