Added poetry and simplified dependencies

[backend] bump python from 3.11 to 3.12
[backend] Added poetry for dependency management
[backend] adjusted docker image to use poetry
[backend] changed folder structure
[backend] continued implementation of scopes logic (frontend home, login and gears api calls handled)
[backend] simplified verify token dependencies
[frontend] simplifies toast management
[frontend] Added pagination component
[frontend] updated auth pinia store to better handle authentication
[frontend] removed infinite scroll from gears view and added pagination
This commit is contained in:
João Vitória Silva
2024-07-11 17:31:57 +01:00
parent 1518f797ce
commit 00184e7b28
187 changed files with 4795 additions and 2059 deletions

75
.gitignore vendored
View File

@@ -1,49 +1,52 @@
# General
docker-compose.yml
# Python
backend/__pycache__/
backend/*/__pycache__/
backend/*/*/__pycache__/
backend/*.pyc
backend/app/__pycache__/
backend/app/*/__pycache__/
backend/app/*/*/__pycache__/
backend/app/*.pyc
# Logs
backend/logs/*.log
backend/*.log
backend/app/logs/*.log
backend/app/*.log
# user image folder images
backend/user_images/*.jpeg
backend/user_images/*.png
backend/user_images/*.jpg
backend/app/user_images/*.jpeg
backend/app/user_images/*.png
backend/app/user_images/*.jpg
# Frontend
frontend/img/users_img/*.*
frontend/app/img/users_img/*.*
# Logs
frontend/.gitignore
frontend/logs
frontend/*.log
frontend/npm-debug.log*
frontend/yarn-debug.log*
frontend/yarn-error.log*
frontend/pnpm-debug.log*
frontend/lerna-debug.log*
frontend/app/.gitignore
frontend/app/logs
frontend/app/*.log
frontend/app/npm-debug.log*
frontend/app/yarn-debug.log*
frontend/app/yarn-error.log*
frontend/app/pnpm-debug.log*
frontend/app/lerna-debug.log*
frontend/node_modules
frontend/.DS_Store
frontend/dist
frontend/dist-ssr
frontend/coverage
frontend/*.local
frontend/README.md
frontend/app/node_modules
frontend/app/.DS_Store
frontend/app/dist
frontend/app/dist-ssr
frontend/app/coverage
frontend/app/*.local
frontend/app/README.md
frontend/cypress/videos/
frontend/cypress/screenshots/
frontend/app/cypress/videos/
frontend/app/cypress/screenshots/
# Editor directories and files
frontend/.vscode/*
frontend/!.vscode/extensions.json
frontend/.idea
frontend/*.suo
frontend/*.ntvs*
frontend/*.njsproj
frontend/*.sln
frontend/*.sw?
frontend/app/.vscode/*
frontend/app/!.vscode/extensions.json
frontend/app/.idea
frontend/app/*.suo
frontend/app/*.ntvs*
frontend/app/*.njsproj
frontend/app/*.sln
frontend/app/*.sw?
frontend/*.tsbuildinfo
frontend/app/*.tsbuildinfo

View File

@@ -1,50 +0,0 @@
FROM python:3.11
# Links Docker image with repository
LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/endurain
# Set the working directory
WORKDIR /app
# Copy the requirements.txt file to /app
COPY requirements.txt /app
# Install any needed packages specified in requirements.txt and then remove requirements.txt file
# Remove any temporary files or directories created during the build process
# Cache Cleanup
RUN pip install --no-cache-dir --upgrade -r requirements.txt \
&& rm requirements.txt \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Copy the directory backend contents to /app
COPY backend /app
# Make port 80 available to the world outside this container
EXPOSE 80
# Define environment variables
ENV DB_HOST="mariadb"
ENV DB_PORT=3306
ENV DB_USER="endurain"
ENV DB_PASSWORD="changeme"
ENV DB_DATABASE="endurain"
ENV SECRET_KEY="changeme"
ENV ALGORITHM="HS256"
ENV ACCESS_TOKEN_EXPIRE_MINUTES=30
ENV REFRESH_TOKEN_EXPIRE_DAYS=7
ENV STRAVA_CLIENT_ID="changeme"
ENV STRAVA_CLIENT_SECRET="changeme"
ENV STRAVA_AUTH_CODE="changeme"
ENV JAEGER_ENABLED="false"
ENV JAEGER_HOST="jaeger"
ENV JAEGER_PROTOCOL="http"
ENV JAGGER_PORT=4317
ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30
ENV FRONTEND_PROTOCOL="http"
ENV FRONTEND_HOST="frontend"
ENV FRONTEND_PORT=8080
ENV GEOCODES_MAPS_API="changeme"
# Run main.py when the container launches
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

119
backend/Dockerfile Normal file
View File

@@ -0,0 +1,119 @@
# FROM python:3.12
# # Links Docker image with repository
# LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/endurain
# # Set the working directory
# WORKDIR /app
# # Copy pyproject.toml and poetry.lock* files
# COPY pyproject.toml poetry.lock* /app/
# # Install Poetry
# RUN pip install poetry
# # Install dependencies using poetry
# # Remove any temporary files or directories created during the build process
# # Cache Cleanup
# RUN poetry config virtualenvs.create false \
# && poetry install --only main --no-root --no-directory --no-interaction --no-ansi \
# && apt-get clean \
# && rm -rf /var/lib/apt/lists/* \
# && rm -rf /tmp/poetry_cache
# # Copy the directory app contents to /app
# COPY app /app
# # Make port 80 available to the world outside this container
# EXPOSE 80
# # Define environment variables
# ENV DB_HOST="mariadb"
# ENV DB_PORT=3306
# ENV DB_USER="endurain"
# ENV DB_PASSWORD="changeme"
# ENV DB_DATABASE="endurain"
# ENV SECRET_KEY="changeme"
# ENV ALGORITHM="HS256"
# ENV ACCESS_TOKEN_EXPIRE_MINUTES=30
# ENV REFRESH_TOKEN_EXPIRE_DAYS=7
# ENV STRAVA_CLIENT_ID="changeme"
# ENV STRAVA_CLIENT_SECRET="changeme"
# ENV STRAVA_AUTH_CODE="changeme"
# ENV JAEGER_ENABLED="false"
# ENV JAEGER_HOST="jaeger"
# ENV JAEGER_PROTOCOL="http"
# ENV JAGGER_PORT=4317
# ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30
# ENV FRONTEND_PROTOCOL="http"
# ENV FRONTEND_HOST="frontend"
# ENV FRONTEND_PORT=8080
# ENV GEOCODES_MAPS_API="changeme"
# # Run main.py when the container launches
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
# Set the base image to Python 3.11 and create a new stage
FROM python:3.11 as requirements-stage
# Set the working directory
WORKDIR /tmp
# Install Poetry
RUN pip install poetry
# Copy pyproject.toml and poetry.lock* files
COPY ./pyproject.toml ./poetry.lock* /tmp/
# Install dependencies using poetry and export them to requirements.txt
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
# Set the base image to Python 3.11
FROM python:3.11
# Links Docker image with repository
LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/endurain
# Set the working directory
WORKDIR /app
# Copy requirements.txt from requirements-stage to /app
COPY --from=requirements-stage /tmp/requirements.txt /app/requirements.txt
# Install dependencies
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
# Copy the directory app contents to /app
COPY app /app
# Make port 80 available to the world outside this container
EXPOSE 80
# Define environment variables
ENV DB_HOST="mariadb"
ENV DB_PORT=3306
ENV DB_USER="endurain"
ENV DB_PASSWORD="changeme"
ENV DB_DATABASE="endurain"
ENV SECRET_KEY="changeme"
ENV ALGORITHM="HS256"
ENV ACCESS_TOKEN_EXPIRE_MINUTES=30
ENV REFRESH_TOKEN_EXPIRE_DAYS=7
ENV STRAVA_CLIENT_ID="changeme"
ENV STRAVA_CLIENT_SECRET="changeme"
ENV STRAVA_AUTH_CODE="changeme"
ENV JAEGER_ENABLED="false"
ENV JAEGER_HOST="jaeger"
ENV JAEGER_PROTOCOL="http"
ENV JAGGER_PORT=4317
ENV STRAVA_DAYS_ACTIVITIES_ONLINK=30
ENV FRONTEND_PROTOCOL="http"
ENV FRONTEND_HOST="frontend"
ENV FRONTEND_PORT=8080
ENV GEOCODES_MAPS_API="changeme"
# Run main.py when the container launches
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
#
#CMD ["fastapi", "run", "main.py", "--port", "80"]

View File

@@ -8,7 +8,8 @@ from sqlalchemy.orm import Session, joinedload
from urllib.parse import unquote
import models
from schemas import schema_activities
import activities.schema as activities_schema
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@@ -188,7 +189,7 @@ def get_user_following_activities_with_pagination(
.filter(
and_(
models.Follower.follower_id == user_id,
models.Follower.is_accepted == True,
models.Follower.is_accepted,
),
models.Activity.visibility.in_([0, 1]),
)
@@ -232,7 +233,7 @@ def get_user_following_activities(user_id, db):
.filter(
and_(
models.Follower.follower_id == user_id,
models.Follower.is_accepted == True,
models.Follower.is_accepted,
),
models.Activity.visibility.in_([0, 1]),
)
@@ -336,7 +337,9 @@ def get_activity_by_id_from_user_id_or_has_visibility(
) from err
def get_activity_by_id_from_user_id(activity_id: int, user_id: int, db: Session) -> schema_activities.Activity:
def get_activity_by_id_from_user_id(
activity_id: int, user_id: int, db: Session
) -> activities_schema.Activity:
try:
# Get the activities from the database
activity = (
@@ -405,9 +408,7 @@ def get_activity_by_strava_id_from_user_id(
) from err
def get_activities_if_contains_name(
name: str, user_id: int, db: Session
):
def get_activities_if_contains_name(name: str, user_id: int, db: Session):
try:
# Define a search term
partial_name = unquote(name).replace("+", " ")
@@ -445,7 +446,7 @@ def get_activities_if_contains_name(
) from err
def create_activity(activity: schema_activities.Activity, db: Session):
def create_activity(activity: activities_schema.Activity, db: Session):
try:
# Create a new activity
db_activity = models.Activity(
@@ -476,8 +477,13 @@ def create_activity(activity: schema_activities.Activity, db: Session):
db.commit()
db.refresh(db_activity)
# Return the user
return db_activity
activity.id = db_activity.id
activity.created_at = db_activity.created_at.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 activity
return activity
except Exception as err:
# Rollback the transaction
db.rollback()
@@ -514,12 +520,14 @@ def add_gear_to_activity(activity_id: int, gear_id: int, db: Session):
) from err
def edit_multiple_activities_gear_id(activities: [schema_activities.Activity], user_id: int, db: Session):
def edit_multiple_activities_gear_id(
activities: list[activities_schema.Activity], user_id: int, db: Session
):
try:
for activity in activities:
# Get the activity from the database
db_activity = get_activity_by_id_from_user_id(activity.id, user_id, db)
# Update the activity
db_activity.gear_id = activity.gear_id
@@ -577,7 +585,7 @@ def delete_all_strava_activities_for_user(user_id: int, db: Session):
db.query(models.Activity)
.filter(
models.Activity.user_id == user_id,
models.Activity.strava_activity_id != None,
models.Activity.strava_activity_id is not None,
)
.delete()
)

View File

@@ -4,26 +4,36 @@ 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 fastapi import APIRouter, Depends, HTTPException, status, UploadFile, Security
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
import activities.schema as activities_schema
import activities.utils as activies_utils
import activities.crud as activities_crud
import activities.dependencies as activities_dependencies
import session.security as session_security
import dependencies.dependencies_global as dependencies_global
import gears.crud as gears_crud
import gears.dependencies as gears_dependencies
import users.dependencies as users_dependencies
import activity_streams.crud as activity_streams_crud
import gpx.utils as gpx_utils
import database
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,
dependencies_security,
# dependencies_security,
)
from processors import gpx_processor, fit_processor
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
from processors import fit_processor
# Define the API router
router = APIRouter()
@@ -33,24 +43,27 @@ logger = logging.getLogger("myLogger")
@router.get(
"/activities/user/{user_id}/week/{week_number}",
response_model=list[schema_activities.Activity] | None,
tags=["activities"],
"/user/{user_id}/week/{week_number}",
response_model=list[activities_schema.Activity] | None,
)
async def read_activities_useractivities_week(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
week_number: int,
validate_week_number: Annotated[
Callable, Depends(dependencies_activities.validate_week_number)
Callable, Depends(activities_dependencies.validate_week_number)
],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
Session,
Depends(database.get_db),
],
db: Session = Depends(dependencies_database.get_db),
):
# Calculate the start of the requested week
today = datetime.utcnow().date()
@@ -59,12 +72,12 @@ async def read_activities_useractivities_week(
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(
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 = crud_activities.get_user_following_activities_per_timeframe(
activities = activities_crud.get_user_following_activities_per_timeframe(
user_id, start_of_week, end_of_week, db
)
@@ -78,20 +91,23 @@ async def read_activities_useractivities_week(
@router.get(
"/activities/user/{user_id}/thisweek/distances",
response_model=schema_activities.ActivityDistances | None,
tags=["activities"],
"/user/{user_id}/thisweek/distances",
response_model=activities_schema.ActivityDistances | None,
)
async def read_activities_useractivities_thisweek_distances(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
Session,
Depends(database.get_db),
],
db: Session = Depends(dependencies_database.get_db),
):
# Calculate the start of the current week
today = datetime.utcnow().date()
@@ -102,12 +118,12 @@ async def read_activities_useractivities_thisweek_distances(
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(
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 = crud_activities.get_user_following_activities_per_timeframe(
activities = activities_crud.get_user_following_activities_per_timeframe(
user_id, start_of_week, end_of_week, db
)
@@ -117,22 +133,27 @@ async def read_activities_useractivities_thisweek_distances(
return None
# Return the activities distances for this week
return schema_activities.calculate_activity_distances(activities)
return activies_utils.calculate_activity_distances(activities)
@router.get(
"/activities/user/{user_id}/thismonth/distances",
response_model=schema_activities.ActivityDistances | None,
tags=["activities"],
"/user/{user_id}/thismonth/distances",
response_model=activities_schema.ActivityDistances | None,
)
async def read_activities_useractivities_thismonth_distances(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
Session,
Depends(database.get_db),
],
db: Session = Depends(dependencies_database.get_db),
):
# Calculate the start of the current month
today = datetime.utcnow().date()
@@ -143,12 +164,12 @@ async def read_activities_useractivities_thismonth_distances(
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(
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 = crud_activities.get_user_following_activities_per_timeframe(
activities = activities_crud.get_user_following_activities_per_timeframe(
user_id, start_of_month, end_of_month, db
)
@@ -157,22 +178,29 @@ async def read_activities_useractivities_thismonth_distances(
return None
# Return the activities distances for this month
return schema_activities.calculate_activity_distances(activities)
return activies_utils.calculate_activity_distances(activities)
@router.get(
"/activities/user/{user_id}/thismonth/number",
"/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)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
Depends(
session_security.get_user_id_from_access_token
),
],
db: Annotated[
Session,
Depends(database.get_db),
],
db: Session = Depends(dependencies_database.get_db),
):
# Calculate the start of the current month
today = datetime.utcnow().date()
@@ -183,12 +211,12 @@ async def read_activities_useractivities_thismonth_number(
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(
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 = crud_activities.get_user_following_activities_per_timeframe(
activities = activities_crud.get_user_following_activities_per_timeframe(
user_id, start_of_month, end_of_month, db
)
@@ -201,42 +229,48 @@ async def read_activities_useractivities_thismonth_number(
@router.get(
"/activities/user/{user_id}/gear/{gear_id}",
response_model=list[schema_activities.Activity] | None,
tags=["activities"],
"/user/{user_id}/gear/{gear_id}",
response_model=list[activities_schema.Activity] | None,
)
async def read_activities_gearactivities(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
gear_id: int,
validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
validate_gear_id: Annotated[Callable, Depends(gears_dependencies.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),
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the activities for the gear
return crud_activities.get_user_activities_by_gear_id_and_user_id(
return activities_crud.get_user_activities_by_gear_id_and_user_id(
user_id, gear_id, db
)
@router.get(
"/activities/user/{user_id}/number",
"/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_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the number of activities for the user
activities = crud_activities.get_user_activities(user_id, db)
activities = activities_crud.get_user_activities(user_id, db)
# Check if activities is None and return 0 if it is
if activities is None:
@@ -247,23 +281,27 @@ async def read_activities_useractivities_number(
@router.get(
"/activities/user/{user_id}/page_number/{page_number}/num_records/{num_records}",
response_model=list[schema_activities.Activity] | None,
tags=["activities"],
"/user/{user_id}/page_number/{page_number}/num_records/{num_records}",
response_model=list[activities_schema.Activity] | None,
)
async def read_activities_useractivities_pagination(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.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_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the activities for the user with pagination
activities = crud_activities.get_user_activities_with_pagination(
activities = activities_crud.get_user_activities_with_pagination(
user_id, db, page_number, num_records
)
@@ -276,40 +314,48 @@ async def read_activities_useractivities_pagination(
@router.get(
"/activities/user/{user_id}/followed/page_number/{page_number}/num_records/{num_records}",
response_model=list[schema_activities.Activity] | None,
tags=["activities"],
"/user/{user_id}/followed/page_number/{page_number}/num_records/{num_records}",
response_model=list[activities_schema.Activity] | None,
)
async def read_activities_followed_user_activities_pagination(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_user_id: Annotated[Callable, Depends(users_dependencies.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_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
validate_token: Annotated[
Callable, Depends(session_security.validate_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the activities for the following users with pagination
return crud_activities.get_user_following_activities_with_pagination(
return activities_crud.get_user_following_activities_with_pagination(
user_id, page_number, num_records, db
)
@router.get(
"/activities/user/{user_id}/followed/number",
"/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_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[
Callable, Depends(session_security.validate_token_expiration)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the number of activities for the following users
activities = crud_activities.get_user_following_activities(user_id, db)
activities = activities_crud.get_user_following_activities(user_id, db)
# Check if activities is None and return 0 if it is
if activities is None:
@@ -320,57 +366,63 @@ async def read_activities_followed_useractivities_number(
@router.get(
"/activities/{activity_id}",
response_model=schema_activities.Activity | None,
tags=["activities"],
"/{activity_id}",
response_model=activities_schema.Activity | None,
)
async def read_activities_activity_from_id(
activity_id: int,
validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id)
Callable, Depends(activities_dependencies.validate_activity_id)
],
token_user_id: Annotated[
Callable,
Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
Session,
Depends(database.get_db),
],
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(
return activities_crud.get_activity_by_id_from_user_id_or_has_visibility(
activity_id, token_user_id, db
)
@router.get(
"/activities/name/contains/{name}",
response_model=list[schema_activities.Activity] | None,
tags=["activities"],
"/name/contains/{name}",
response_model=list[activities_schema.Activity] | None,
)
async def read_activities_contain_name(
name: str,
token_user_id: Annotated[
Callable,
Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
Session,
Depends(database.get_db),
],
db: Session = Depends(dependencies_database.get_db),
):
# Get the activities from the database by name
return crud_activities.get_activities_if_contains_name(name, token_user_id, db)
return activities_crud.get_activities_if_contains_name(name, token_user_id, db)
@router.post(
"/activities/create/upload",
"/create/upload",
status_code=201,
response_model=int,
tags=["activities"],
response_model=activities_schema.Activity,
)
async def create_activity_with_uploaded_file(
token_user_id: Annotated[
Callable,
Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
Depends(session_security.get_user_id_from_access_token),
],
file: UploadFile,
db: Session = Depends(dependencies_database.get_db),
db: Annotated[
Session,
Depends(database.get_db),
],
):
try:
# Ensure the 'uploads' directory exists
@@ -387,7 +439,7 @@ async def create_activity_with_uploaded_file(
# 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, token_user_id)
parsed_info = gpx_utils.parse_gpx_file(file.filename, token_user_id)
elif file_extension.lower() == ".fit":
# Parse the FIT file
parsed_info = fit_processor.parse_fit_file(file.filename, token_user_id)
@@ -399,7 +451,7 @@ async def create_activity_with_uploaded_file(
)
# create the activity in the database
created_activity = crud_activities.create_activity(parsed_info["activity"], db)
created_activity = activities_crud.create_activity(parsed_info["activity"], db)
# Check if created_activity is None
if created_activity is None:
@@ -410,18 +462,18 @@ async def create_activity_with_uploaded_file(
)
# Parse the activity streams from the parsed info
activity_streams = gpx_processor.parse_activity_streams_from_gpx_file(
activity_streams = gpx_utils.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)
activity_streams_crud.create_activity_streams(activity_streams, db)
# Remove the file after processing
os.remove(file.filename)
# Return activity ID
return created_activity.id
return created_activity
except Exception as err:
# Log the exception
logger.error(
@@ -435,23 +487,28 @@ async def create_activity_with_uploaded_file(
@router.put(
"/activities/{activity_id}/addgear/{gear_id}",
tags=["activities"],
"/{activity_id}/addgear/{gear_id}",
)
async def activity_add_gear(
activity_id: int,
validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id)
Callable, Depends(activities_dependencies.validate_activity_id)
],
gear_id: int,
validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
validate_gear_id: Annotated[Callable, Depends(gears_dependencies.validate_gear_id)],
token_user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
int,
Depends(
session_security.get_user_id_from_access_token
),
],
db: Annotated[
Session,
Depends(database.get_db),
],
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)
gear = gears_crud.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:
@@ -461,7 +518,7 @@ async def activity_add_gear(
)
# Get the activity by id from user id
activity = crud_activities.get_activity_by_id_from_user_id(
activity = activities_crud.get_activity_by_id_from_user_id(
activity_id, token_user_id, db
)
@@ -473,28 +530,33 @@ async def activity_add_gear(
)
# Add the gear to the activity
crud_activities.add_gear_to_activity(activity_id, gear_id, db)
activities_crud.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"],
"/{activity_id}/deletegear",
)
async def delete_activity_gear(
activity_id: int,
validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id)
Callable, Depends(activities_dependencies.validate_activity_id)
],
token_user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
int,
Depends(
session_security.get_user_id_from_access_token
),
],
db: Annotated[
Session,
Depends(database.get_db),
],
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 = activities_crud.get_activity_by_id_from_user_id(
activity_id, token_user_id, db
)
@@ -506,28 +568,33 @@ async def delete_activity_gear(
)
# Delete gear from the activity
crud_activities.add_gear_to_activity(activity_id, None, db)
activities_crud.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",
tags=["activities"],
"/{activity_id}/delete",
)
async def delete_activity(
activity_id: int,
validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id)
Callable, Depends(activities_dependencies.validate_activity_id)
],
token_user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
int,
Depends(
session_security.get_user_id_from_access_token
),
],
db: Annotated[
Session,
Depends(database.get_db),
],
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 = activities_crud.get_activity_by_id_from_user_id(
activity_id, token_user_id, db
)
@@ -539,7 +606,7 @@ async def delete_activity(
)
# Delete the activity
crud_activities.delete_activity(activity_id, db)
activities_crud.delete_activity(activity_id, db)
# Return success message
return {"detail": f"Activity {activity_id} deleted successfully"}

View File

@@ -32,22 +32,4 @@ class Activity(BaseModel):
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]:
bike += activity.distance
elif activity.activity_type in [8, 9]:
swim += activity.distance
# Return the distances
return ActivityDistances(run=run, bike=bike, swim=swim)
run: float

View File

@@ -7,12 +7,32 @@ from datetime import datetime
from urllib.parse import urlencode
from statistics import mean
import activities.schema as activities_schema
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
def calculate_activity_distances(activities: list[activities_schema.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]:
bike += activity.distance
elif activity.activity_type in [8, 9]:
swim += activity.distance
# Return the distances
return activities_schema.ActivityDistances(run=run, bike=bike, swim=swim)
def location_based_on_coordinates(latitude, longitude) -> dict | None:
if latitude is None or longitude is None:
if latitude is None or longitude is None:
return None
# Create a dictionary with the parameters for the request

View File

@@ -3,7 +3,8 @@ import logging
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from schemas import schema_activity_streams
import activity_streams.schema as activity_streams_schema
import models
# Define a loggger created on main.py
@@ -66,7 +67,7 @@ def get_activity_stream_by_type(activity_id: int, stream_type: int, db: Session)
def create_activity_streams(
activity_streams: [schema_activity_streams.ActivityStreams], db: Session
activity_streams: list[activity_streams_schema.ActivityStreams], db: Session
):
try:
# Create a list to store the ActivityStreams objects

View File

@@ -0,0 +1,74 @@
import logging
from typing import Annotated, Callable
from fastapi import APIRouter, Depends, Security
from sqlalchemy.orm import Session
import activity_streams.schema as activity_streams_schema
import activity_streams.crud as activity_streams_crud
import activity_streams.dependencies as activity_streams_dependencies
import activities.dependencies as activities_dependencies
import session.security as session_security
import database
#from dependencies import (
# dependencies_security,
#)
# Define the API router
router = APIRouter()
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@router.get(
"/activity_id/{activity_id}/all",
response_model=list[activity_streams_schema.ActivityStreams] | None,
)
async def read_activities_streams_for_activity_all(
activity_id: int,
validate_id: Annotated[
Callable, Depends(activities_dependencies.validate_activity_id)
],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the activity streams from the database and return them
return activity_streams_crud.get_activity_streams(activity_id, db)
@router.get(
"/activity_id/{activity_id}/stream_type/{stream_type}",
response_model=activity_streams_schema.ActivityStreams | None,
)
async def read_activities_streams_for_activity_stream_type(
activity_id: int,
validate_activity_id: Annotated[
Callable, Depends(activities_dependencies.validate_activity_id)
],
stream_type: int,
validate_activity_stream_type: Annotated[
Callable, Depends(activity_streams_dependencies.validate_activity_stream_type)
],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the activity stream from the database and return them
return activity_streams_crud.get_activity_stream_by_type(
activity_id, stream_type, db
)

2
backend/app/config.py Normal file
View File

@@ -0,0 +1,2 @@
# Constant related to version
API_VERSION = "v0.3.0"

View File

@@ -4,6 +4,19 @@ from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.engine.url import URL
def get_db():
# Create a new database session and return it
db = SessionLocal()
try:
# Yield the database session
yield db
finally:
# Close the database session
db.close()
# Define the database connection URL using environment variables
db_url = URL.create(
drivername="mysql",
@@ -23,4 +36,4 @@ engine = create_engine(
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create a base class for declarative models
Base = declarative_base()
Base = declarative_base()

View File

@@ -4,8 +4,11 @@ from sqlalchemy.orm import Session
from typing import Annotated
from dependencies import dependencies_database, dependencies_security
from schemas import schema_users
from session import dependencies_security
import users.schema as users_schema
import database
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@@ -13,6 +16,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
### Dependencies for access token validation
def validate_access_token(request: Request):
# Extract the access token from the cookies
access_token = request.cookies.get("endurain_access_token")
@@ -87,7 +91,7 @@ def validate_refresh_token_and_get_authenticated_user_id(
def validate_token_and_validate_admin_access(
token: str = Depends(oauth2_scheme),
db: Session = Depends(dependencies_database.get_db),
db: Session = Depends(database.get_db),
):
# Validate the token expiration
dependencies_security.validate_token_expiration(token)
@@ -99,7 +103,7 @@ def validate_token_and_validate_admin_access(
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),
db: Session = Depends(database.get_db),
):
# Validate the token expiration
dependencies_security.validate_token_expiration(token)
@@ -118,17 +122,17 @@ def validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_acc
def validate_token_and_if_user_id_equals_token_user_attributtes_id_if_not_validate_admin_access(
user_attributtes: schema_users.User,
user_attributtes: users_schema.User,
token: str = Depends(oauth2_scheme),
db: Session = Depends(dependencies_database.get_db),
db: Session = Depends(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,
user_attributtes: users_schema.UserEditPassword,
token: str = Depends(oauth2_scheme),
db: Session = Depends(dependencies_database.get_db),
db: Session = Depends(database.get_db),
):
validate_token_user_id_admin_access(db, token, user_attributtes.id)

View File

@@ -1,13 +1,9 @@
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")
@@ -45,7 +41,7 @@ def get_accepted_followers_by_user_id(user_id: int, db: Session):
db.query(models.Follower)
.filter(
(models.Follower.follower_id == user_id)
& (models.Follower.is_accepted == True)
& (models.Follower.is_accepted)
)
.all()
)
@@ -100,7 +96,7 @@ def get_accepted_following_by_user_id(user_id: int, db: Session):
db.query(models.Follower)
.filter(
(models.Follower.following_id == user_id)
& (models.Follower.is_accepted == True)
& (models.Follower.is_accepted)
)
.all()
)

View File

@@ -0,0 +1,257 @@
import logging
from typing import Annotated, Callable
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
import followers.schema as followers_schema
import followers.crud as followers_crud
import users.dependencies as users_dependencies
import session.security as session_security
import database
# 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(
"/followers/user/{user_id}/followers/all",
response_model=list[followers_schema.Follower] | None,
tags=["followers"],
)
async def get_user_follower_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return followers
return followers_crud.get_all_following_by_user_id(user_id, db)
@router.get(
"/followers/user/{user_id}/followers/count/all",
response_model=int,
tags=["followers"],
)
async def get_user_follower_count_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return followers
followers = followers_crud.get_all_followers_by_user_id(user_id, db)
# Check if followers is None and return 0 if it is
if followers is None:
return 0
# Return the number of followers
return len(followers)
@router.get(
"/followers/user/{user_id}/followers/count/accepted",
response_model=int,
tags=["followers"],
)
async def get_user_follower_count(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return followers
followers = followers_crud.get_accepted_followers_by_user_id(user_id, db)
# Check if followers is None and return 0 if it is
if followers is None:
return 0
# Return the number of followers
return len(followers)
@router.get(
"/followers/user/{user_id}/following/all",
response_model=list[followers_schema.Follower] | None,
tags=["followers"],
)
async def get_user_following_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return followings
return followers_crud.get_all_followers_by_user_id(user_id, db)
@router.get(
"/followers/user/{user_id}/following/count/all",
response_model=int,
tags=["followers"],
)
async def get_user_following_count_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return followings
followings = followers_crud.get_all_following_by_user_id(user_id, db)
# Check if followings is None and return 0 if it is
if followings is None:
return 0
# Return the number of followings
return len(followings)
@router.get(
"/followers/user/{user_id}/following/count/accepted",
response_model=int,
tags=["followers"],
)
async def get_user_following_count(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return followings
followings = followers_crud.get_accepted_following_by_user_id(user_id, db)
# Check if followings is None and return 0 if it is
if followings is None:
return 0
# Return the number of followings
return len(followings)
@router.get(
"/followers/user/{user_id}/targetUser/{target_user_id}",
response_model=followers_schema.Follower | None,
tags=["followers"],
)
async def read_followers_user_specific_user(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(users_dependencies.validate_target_user_id)
],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return the follower
return followers_crud.get_follower_for_user_id_and_target_user_id(
user_id, target_user_id, db
)
@router.post(
"/followers/create/user/{user_id}/targetUser/{target_user_id}",
status_code=201,
tags=["followers"],
)
async def create_follow(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(users_dependencies.validate_target_user_id)
],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Create the follower
#new_follow = followers_crud.create_follower(user_id, target_user_id, db)
# Return the ID of the gear created
return {"detail": "Follower record created successfully"}
@router.put("/followers/accept/user/{user_id}/targetUser/{target_user_id}",
tags=["followers"],
)
async def accept_follow(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(users_dependencies.validate_target_user_id)
],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Accept the follower
followers_crud.accept_follower(user_id, target_user_id, db)
# Return success message
return {"detail": "Follower record accepted successfully"}
@router.delete(
"/followers/delete/user/{user_id}/targetUser/{target_user_id}",
tags=["followers"],
)
async def delete_follow(
user_id: int,
validate_user_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(users_dependencies.validate_target_user_id)
],
validate_token: Annotated[Callable, Depends(session_security.validate_token_expiration)],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Delete the follower
followers_crud.delete_follower(user_id, target_user_id, db)
# Return success message
return {"detail": "Follower record deleted successfully"}

View File

@@ -7,4 +7,4 @@ class Follower(BaseModel):
is_accepted: bool
class Config:
orm_mode = True
from_attributes = True

View File

@@ -1,19 +1,21 @@
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
import gears.schema as gears_schema
import gears.utils as gears_utils
# 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) -> schema_gear.Gear | None:
def get_gear_user_by_id(user_id: int, gear_id: int, db: Session) -> gears_schema.Gear | None:
try:
gear = (
db.query(models.Gear)
@@ -41,7 +43,7 @@ def get_gear_user_by_id(user_id: int, gear_id: int, db: Session) -> schema_gear.
def get_gear_users_with_pagination(
user_id: int, db: Session, page_number: int = 1, num_records: int = 5
) -> list[schema_gear.Gear] | None:
) -> list[gears_schema.Gear] | None:
try:
# Get the gear by user ID from the database
gear = (
@@ -73,7 +75,7 @@ def get_gear_users_with_pagination(
) from err
def get_gear_user(user_id: int, db: Session) -> list[schema_gear.Gear] | None:
def get_gear_user(user_id: int, db: Session) -> list[gears_schema.Gear] | None:
try:
# Get the gear by user ID from the database
gears = db.query(models.Gear).filter(models.Gear.user_id == user_id).all()
@@ -98,7 +100,7 @@ def get_gear_user(user_id: int, db: Session) -> list[schema_gear.Gear] | None:
) from err
def get_gear_user_by_nickname(user_id: int, nickname: str, db: Session) -> list[schema_gear.Gear] | None:
def get_gear_user_by_nickname(user_id: int, nickname: str, db: Session) -> list[gears_schema.Gear] | None:
try:
# Unquote the nickname and change "+" to whitespace
parsed_nickname = unquote(nickname).replace("+", " ")
@@ -166,7 +168,7 @@ def get_gear_by_type_and_user(gear_type: int, user_id: int, db: Session):
def get_gear_by_strava_id_from_user_id(
gear_strava_id: str, user_id: int, db: Session
) -> schema_gear.Gear | None:
) -> gears_schema.Gear | None:
try:
# Get the gear from the database
gear = (
@@ -197,43 +199,14 @@ def get_gear_by_strava_id_from_user_id(
) from err
def transform_schema_gear_to_model_gear(
gear: schema_gear.Gear, user_id: int
) -> models.Gear:
# 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
new_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,
strava_gear_id=gear.strava_gear_id,
)
return new_gear
def create_multiple_gears(gears: [schema_gear.Gear], user_id: int, db: Session):
def create_multiple_gears(gears: list[gears_schema.Gear], user_id: int, db: Session):
try:
# Filter out None values from the gears list
valid_gears = [gear for gear in gears if gear is not None]
# Create a list of gear objects
new_gears = [
transform_schema_gear_to_model_gear(gear, user_id) for gear in valid_gears
gears_utils.transform_schema_gear_to_model_gear(gear, user_id) for gear in valid_gears
]
# Add the gears to the database
@@ -264,9 +237,9 @@ def create_multiple_gears(gears: [schema_gear.Gear], user_id: int, db: Session):
) from err
def create_gear(gear: schema_gear.Gear, user_id: int, db: Session):
def create_gear(gear: gears_schema.Gear, user_id: int, db: Session):
try:
new_gear = transform_schema_gear_to_model_gear(gear, user_id)
new_gear = gears_utils.transform_schema_gear_to_model_gear(gear, user_id)
# Add the gear to the database
db.add(new_gear)
@@ -299,7 +272,7 @@ def create_gear(gear: schema_gear.Gear, user_id: int, db: Session):
) from err
def edit_gear(gear_id: int, gear: schema_gear.Gear, db: Session):
def edit_gear(gear_id: int, gear: gears_schema.Gear, db: Session):
try:
# Get the gear from the database
db_gear = db.query(models.Gear).filter(models.Gear.id == gear_id).first()
@@ -370,7 +343,7 @@ def delete_all_strava_gear_for_user(user_id: int, db: Session):
# Delete the gear records with strava_gear_id not null for the user
num_deleted = (
db.query(models.Gear)
.filter(models.Gear.user_id == user_id, models.Gear.strava_gear_id != None)
.filter(models.Gear.user_id == user_id, models.Gear.strava_gear_id is not None)
.delete()
)

227
backend/app/gears/router.py Normal file
View File

@@ -0,0 +1,227 @@
import logging
from typing import Annotated, Callable
from fastapi import APIRouter, Depends, HTTPException, status, Security
from sqlalchemy.orm import Session
import session.security as session_security
import gears.schema as gears_schema
import gears.crud as gears_crud
import gears.dependencies as gears_dependencies
import database
from dependencies import (
dependencies_session,
dependencies_global,
)
# Define the API router
router = APIRouter()
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@router.get(
"/id/{gear_id}",
response_model=gears_schema.Gear | None,
)
async def read_gear_id(
gear_id: int,
validate_gear_id: Annotated[Callable, Depends(gears_dependencies.validate_gear_id)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:read"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[Session, Depends(database.get_db)],
):
# Return the gear
return gears_crud.get_gear_user_by_id(token_user_id, gear_id, db)
@router.get(
"/page_number/{page_number}/num_records/{num_records}",
response_model=list[gears_schema.Gear] | None,
)
async def read_gear_user_pagination(
page_number: int,
num_records: int,
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:read"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return the gear
return gears_crud.get_gear_users_with_pagination(
token_user_id, db, page_number, num_records
)
@router.get(
"/number",
response_model=int,
)
async def read_gear_user_number(
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:read"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the gear
gear = gears_crud.get_gear_user(token_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(
"/nickname/{nickname}",
response_model=list[gears_schema.Gear] | None,
)
async def read_gear_user_by_nickname(
nickname: str,
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:read"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return the gear
return gears_crud.get_gear_user_by_nickname(token_user_id, nickname, db)
@router.get(
"/type/{gear_type}",
response_model=list[gears_schema.Gear] | None,
)
async def read_gear_user_by_type(
gear_type: int,
validate_type: Annotated[Callable, Depends(gears_dependencies.validate_gear_type)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:read"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Return the gear
return gears_crud.get_gear_by_type_and_user(gear_type, token_user_id, db)
@router.post(
"/create",
status_code=201,
)
async def create_gear(
gear: gears_schema.Gear,
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:write"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Create the gear
gear_created = gears_crud.create_gear(gear, token_user_id, db)
# Return the ID of the gear created
return gear_created.id
@router.put("/{gear_id}/edit")
async def edit_gear(
gear_id: int,
validate_id: Annotated[Callable, Depends(gears_dependencies.validate_gear_id)],
gear: gears_schema.Gear,
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:write"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the gear by id
gear_db = gears_crud.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_db is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Gear ID {gear_id} for user {token_user_id} not found",
)
# Edit the gear
gears_crud.edit_gear(gear_id, gear, db)
# Return success message
return {"detail": f"Gear ID {gear_id} edited successfully"}
@router.delete("/{gear_id}/delete", tags=["gear"])
async def delete_user(
gear_id: int,
validate_id: Annotated[Callable, Depends(gears_dependencies.validate_gear_id)],
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["gears:write"])
],
token_user_id: Annotated[
int, Depends(session_security.get_user_id_from_access_token)
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Get the gear by id
gear = gears_crud.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
gears_crud.delete_gear(gear_id, db)
# Return success message
return {"detail": f"Gear ID {gear_id} deleted successfully"}

View File

@@ -0,0 +1,35 @@
from sqlalchemy import func
from urllib.parse import unquote
import models
import gears.schema as gear_schema
def transform_schema_gear_to_model_gear(
gear: gear_schema.Gear, user_id: int
) -> models.Gear:
# 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
new_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,
strava_gear_id=gear.strava_gear_id,
)
return new_gear

View File

@@ -4,8 +4,11 @@ import logging
from fastapi import HTTPException, status
from processors import activity_processor
from schemas import schema_activities, schema_activity_streams
import activities.utils as activities_utils
import activities.schema as activities_schema
import activity_streams.schema as activity_streams_schema
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@@ -37,7 +40,7 @@ def parse_activity_streams_from_gpx_file(parsed_info: dict, activity_id: int):
# Return activity streams as a list of ActivityStreams objects
return [
schema_activity_streams.ActivityStreams(
activity_streams_schema.ActivityStreams(
activity_id=activity_id,
stream_type=stream_type,
stream_waypoints=waypoints,
@@ -108,7 +111,7 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
# Calculate distance between waypoints
if prev_latitude is not None and prev_longitude is not None:
distance += activity_processor.calculate_distance(
distance += activities_utils.calculate_distance(
prev_latitude, prev_longitude, latitude, longitude
)
@@ -123,7 +126,7 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
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(
location_data = activities_utils.location_based_on_coordinates(
latitude, longitude
)
city = location_data["city"]
@@ -164,7 +167,7 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
is_power_set = True
# Calculate instant speed, pace, and update waypoint arrays
instant_speed = activity_processor.calculate_instant_speed(
instant_speed = activities_utils.calculate_instant_speed(
last_waypoint_time,
time,
latitude,
@@ -233,25 +236,25 @@ def parse_gpx_file(file: str, user_id: int) -> dict:
)
# Calculate elevation gain/loss, pace, average speed, and average power
elevation_data = activity_processor.calculate_elevation_gain_loss(ele_waypoints)
elevation_data = activities_utils.calculate_elevation_gain_loss(ele_waypoints)
elevation_gain = elevation_data["elevation_gain"]
elevation_loss = elevation_data["elevation_loss"]
pace = activity_processor.calculate_pace(
pace = activities_utils.calculate_pace(
distance, first_waypoint_time, last_waypoint_time
)
average_speed = activity_processor.calculate_average_speed(
average_speed = activities_utils.calculate_average_speed(
distance, first_waypoint_time, last_waypoint_time
)
average_power = activity_processor.calculate_average_power(power_waypoints)
average_power = activities_utils.calculate_average_power(power_waypoints)
# Create an Activity object with parsed data
activity = schema_activities.Activity(
activity = activities_schema.Activity(
user_id=user_id,
name=activity_name,
distance=distance,
activity_type=activity_processor.define_activity_type(activity_type),
activity_type=activities_utils.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,

View File

@@ -1,7 +1,7 @@
import logging
import os
from fastapi import FastAPI
from fastapi import FastAPI, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
@@ -17,16 +17,26 @@ from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
# from session import router as session_router
# from users import router as users_router
# from activities import router as activities_router
# from activity_streams import router as activity_streams_router
# from gears import router as gears_router
# from followers import router as followers_router
import session.router as session_router
import session.security as session_security
import users.router as users_router
import activities.router as activities_router
import activity_streams.router as activity_streams_router
import gears.router as gears_router
import followers.router as followers_router
from routers import (
router_session,
router_users,
router_activities,
router_activity_streams,
router_gear,
router_followers,
router_strava,
)
from constants import API_VERSION
from config import API_VERSION
from database import SessionLocal
from processors import strava_processor, strava_activity_processor
@@ -162,13 +172,36 @@ app.add_middleware(
)
# Router files
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)
app.include_router(router_followers.router)
app.include_router(router_strava.router)
app.include_router(
session_router.router,
tags=["session"],
)
app.include_router(
users_router.router,
prefix="/users",
tags=["users"],
dependencies=[Depends(session_security.validate_access_token)],
)
app.include_router(
activities_router.router,
prefix="/activities",
tags=["activities"],
dependencies=[Depends(session_security.validate_access_token)],
)
app.include_router(
activity_streams_router.router,
prefix="/activities/streams",
tags=["activity_streams"],
dependencies=[Depends(session_security.validate_access_token)],
)
app.include_router(
gears_router.router,
prefix="/gears",
tags=["gears"],
dependencies=[Depends(session_security.validate_access_token)],
)
app.include_router(followers_router.router, tags=["followers"])
app.include_router(router_strava.router, tags=["strava"])
# Check if Jaeger tracing is enabled using the 'JAEGER_ENABLED' environment variable
if os.environ.get("JAEGER_ENABLED") == "true":

View File

@@ -120,6 +120,7 @@ class User(Base):
cascade="all, delete-orphan",
foreign_keys=[Follower.following_id],
)
# Establish a one-to-many relationship between User and Followers
following = relationship(
"Follower",

View File

View File

@@ -5,15 +5,21 @@ from sqlalchemy.orm import Session
from stravalib.client import Client
from pint import Quantity
from schemas import schema_activities, schema_activity_streams, schema_user_integrations
from crud import (
crud_activities,
crud_activity_streams,
crud_users,
crud_gear,
)
import activities.schema as activities_schema
import activities.crud as activities_crud
import activities.utils as activities_utils
import activity_streams.schema as activity_streams_schema
import activity_streams.crud as activity_streams_crud
import user_integrations.schema as user_integrations_schema
import users.crud as users_crud
import gears.crud as gears_crud
from database import SessionLocal
from processors import activity_processor, strava_processor
from processors import strava_processor
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@@ -23,7 +29,7 @@ def fetch_and_process_activities(
strava_client: Client,
start_date: datetime,
user_id: int,
user_integrations: schema_user_integrations.UserIntegrations,
user_integrations: user_integrations_schema.UserIntegrations,
db: Session,
) -> int:
# Fetch Strava activities after the specified start date
@@ -46,7 +52,13 @@ def fetch_and_process_activities(
return len(strava_activities)
def parse_activity(activity, user_id: int, strava_client: Client, user_integrations: schema_user_integrations.UserIntegrations, db: Session) -> dict:
def parse_activity(
activity,
user_id: int,
strava_client: Client,
user_integrations: user_integrations_schema.UserIntegrations,
db: Session,
) -> dict:
# Parse start and end dates
start_date_parsed = activity.start_date
@@ -69,7 +81,7 @@ def parse_activity(activity, user_id: int, strava_client: Client, user_integrati
# Initialize location variables
city, town, country = None, None, None
parsed_location = activity_processor.location_based_on_coordinates(
parsed_location = activities_utils.location_based_on_coordinates(
latitude, longitude
)
@@ -206,14 +218,16 @@ def parse_activity(activity, user_id: int, strava_client: Client, user_integrati
if user_integrations.strava_sync_gear:
# set the gear id for the activity
gear = crud_gear.get_gear_by_strava_id_from_user_id(activity.gear_id, user_id, db)
gear = gears_crud.get_gear_by_strava_id_from_user_id(
activity.gear_id, user_id, db
)
# set the gear id for the activity
if gear is not None:
gear_id = gear.id
# Create the activity object
activity_to_store = schema_activities.Activity(
activity_to_store = activities_schema.Activity(
user_id=user_id,
name=activity.name,
distance=(
@@ -222,7 +236,7 @@ def parse_activity(activity, user_id: int, strava_client: Client, user_integrati
else round(activity.distance)
),
description=activity.description,
activity_type=activity_processor.define_activity_type(activity.sport_type),
activity_type=activities_utils.define_activity_type(activity.sport_type),
start_time=start_date_parsed.strftime("%Y-%m-%dT%H:%M:%S"),
end_time=end_date_parsed.strftime("%Y-%m-%dT%H:%M:%S"),
city=city,
@@ -245,10 +259,10 @@ def parse_activity(activity, user_id: int, strava_client: Client, user_integrati
def save_activity_and_streams(
activity: schema_activities.Activity, stream_data: list, db: Session
activity: activities_schema.Activity, stream_data: list, db: Session
):
# Create the activity and get the ID
created_activity = crud_activities.create_activity(activity, db)
created_activity = activities_crud.create_activity(activity, db)
# Create the empty array of activity streams
activity_streams = []
@@ -257,7 +271,7 @@ def save_activity_and_streams(
for is_set, stream_type, waypoints in stream_data:
if is_set:
activity_streams.append(
schema_activity_streams.ActivityStreams(
activity_streams_schema.ActivityStreams(
activity_id=created_activity.id,
stream_type=stream_type,
stream_waypoints=waypoints,
@@ -266,14 +280,14 @@ def save_activity_and_streams(
)
# Create the activity streams in the database
crud_activity_streams.create_activity_streams(activity_streams, db)
activity_streams_crud.create_activity_streams(activity_streams, db)
def process_activity(
activity,
user_id: int,
strava_client: Client,
user_integrations: schema_user_integrations.UserIntegrations,
user_integrations: user_integrations_schema.UserIntegrations,
db: Session,
):
# Get the activity by Strava ID from the user
@@ -287,7 +301,9 @@ def process_activity(
logger.info(f"User {user_id}: Strava activity {activity.id} will be processed")
# Parse the activity and streams
parsed_activity = parse_activity(activity, user_id, strava_client, user_integrations, db)
parsed_activity = parse_activity(
activity, user_id, strava_client, user_integrations, db
)
# Save the activity and streams to the database
save_activity_and_streams(
@@ -301,7 +317,7 @@ def retrieve_strava_users_activities_for_days(days: int):
try:
# Get all users
users = crud_users.get_all_users(db)
users = users_crud.get_all_users(db)
finally:
# Ensure the session is closed after use
db.close()
@@ -325,9 +341,7 @@ def get_user_strava_activities_by_days(start_date: datetime, user_id: int):
)
if user_integrations is None:
logger.info(
f"User {user_id}: Strava not linked"
)
logger.info(f"User {user_id}: Strava not linked")
return None
# Log the start of the activities processing

View File

@@ -4,12 +4,14 @@ from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from stravalib.client import Client
from schemas import schema_gear, schema_activities
from crud import (
crud_user_integrations,
crud_gear,
crud_activities,
)
import gears.schema as gears_schema
import gears.crud as gears_crud
import activities.schema as activities_schema
import activities.crud as activities_crud
import user_integrations.crud as user_integrations_crud
from database import SessionLocal
from processors import strava_processor, strava_athlete_processor
@@ -62,9 +64,9 @@ def fetch_and_process_gear(strava_client: Client, user_id: int, db: Session) ->
def process_gear(
gear, type: str, user_id: int, strava_client: Client, db: Session
) -> schema_gear.Gear | None:
) -> gears_schema.Gear | None:
# Get the gear by strava id from user id
gear_db = crud_gear.get_gear_by_strava_id_from_user_id(gear.id, user_id, db)
gear_db = gears_crud.get_gear_by_strava_id_from_user_id(gear.id, user_id, db)
# Skip existing gear
if gear_db:
@@ -73,7 +75,7 @@ def process_gear(
# Get the gear from Strava
strava_gear = get_strava_gear(gear.id, strava_client)
new_gear = schema_gear.Gear(
new_gear = gears_schema.Gear(
brand=strava_gear.brand_name,
model=strava_gear.model_name,
nickname=strava_gear.name,
@@ -86,13 +88,17 @@ def process_gear(
return new_gear
def save_gears(gears: [schema_gear.Gear], user_id: int, db: Session):
def save_gears(gears: [gears_schema.Gear], user_id: int, db: Session):
# Save the gear to the database
crud_gear.create_multiple_gears(gears, user_id, db)
gears_crud.create_multiple_gears(gears, user_id, db)
def iterate_over_activities_and_set_gear(
activity: schema_activities.Activity, gears: [schema_gear.Gear], counter: int, user_id: int, db: Session
activity: activities_schema.Activity,
gears: [gears_schema.Gear],
counter: int,
user_id: int,
db: Session,
) -> dict:
# Iterate over gears and set gear if applicable
@@ -109,14 +115,14 @@ def iterate_over_activities_and_set_gear(
def set_activities_gear(user_id: int, db: Session) -> int:
# Get user activities
activities = crud_activities.get_user_activities(user_id, db)
activities = activities_crud.get_user_activities(user_id, db)
# Skip if no activities
if activities is None:
return 0
# Get user gears
gears = crud_gear.get_gear_user(user_id, db)
gears = gears_crud.get_gear_user(user_id, db)
# Skip if no gears
if gears is None:
@@ -130,11 +136,13 @@ def set_activities_gear(user_id: int, db: Session) -> int:
# iterate over activities and set gear if applicable
for activity in activities:
parsed_activity = iterate_over_activities_and_set_gear(activity, gears, counter, user_id, db)
parsed_activity = iterate_over_activities_and_set_gear(
activity, gears, counter, user_id, db
)
counter = parsed_activity["counter"]
activities_parsed.append(parsed_activity["activity"])
crud_activities.edit_multiple_activities_gear_id(activities_parsed, user_id, db)
activities_crud.edit_multiple_activities_gear_id(activities_parsed, user_id, db)
return counter
@@ -156,7 +164,7 @@ def get_user_gear(user_id: int):
strava_client = strava_processor.create_strava_client(user_integrations)
# Set the user's gear to sync to True
crud_user_integrations.set_user_strava_sync_gear(user_id, True, db)
user_integrations_crud.set_user_strava_sync_gear(user_id, True, db)
# Fetch Strava activities after the specified start date
num_strava_gear_processed = fetch_and_process_gear(strava_client, user_id, db)

View File

@@ -7,8 +7,13 @@ from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from stravalib.client import Client
from schemas import schema_activities, schema_user_integrations
from crud import crud_user_integrations, crud_activities, crud_users
import activities.schema as activities_schema
import activities.crud as activities_crud
import user_integrations.schema as user_integrations_schema
import user_integrations.crud as user_integrations_crud
import users.crud as users_crud
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@@ -16,12 +21,12 @@ logger = logging.getLogger("myLogger")
def refresh_strava_tokens(db: Session):
# Get all users
users = crud_users.get_all_users(db)
users = users_crud.get_all_users(db)
# Iterate through all users
for user in users:
# Get the user integrations by user ID
user_integrations = crud_user_integrations.get_user_integrations_by_user_id(
user_integrations = user_integrations_crud.get_user_integrations_by_user_id(
user.id, db
)
@@ -65,7 +70,7 @@ def refresh_strava_tokens(db: Session):
) from err
finally:
# Update the user integrations with the tokens
crud_user_integrations.link_strava_account(
user_integrations_crud.link_strava_account(
user_integrations, tokens, db
)
@@ -77,9 +82,9 @@ def refresh_strava_tokens(db: Session):
def fetch_and_validate_activity(
activity_id: int, user_id: int, db: Session
) -> schema_activities.Activity | None:
) -> activities_schema.Activity | None:
# Get the activity by Strava ID from the user
activity_db = crud_activities.get_activity_by_strava_id_from_user_id(
activity_db = activities_crud.get_activity_by_strava_id_from_user_id(
activity_id, user_id, db
)
@@ -98,9 +103,9 @@ def fetch_and_validate_activity(
def fetch_user_integrations_and_validate_token(
user_id: int, db: Session
) -> schema_user_integrations.UserIntegrations | None:
) -> user_integrations_schema.UserIntegrations | None:
# Get the user integrations by user ID
user_integrations = crud_user_integrations.get_user_integrations_by_user_id(
user_integrations = user_integrations_crud.get_user_integrations_by_user_id(
user_id, db
)
@@ -120,7 +125,7 @@ def fetch_user_integrations_and_validate_token(
def create_strava_client(
user_integrations: schema_user_integrations.UserIntegrations,
user_integrations: user_integrations_schema.UserIntegrations,
) -> Client:
# Create a Strava client with the user's access token and return it
return Client(access_token=user_integrations.strava_token)

View File

View File

@@ -9,13 +9,19 @@ from fastapi.security import OAuth2PasswordBearer
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from crud import crud_user_integrations, crud_gear, crud_activities
import user_integrations.crud as user_integrations_crud
import gears.crud as gears_crud
import activities.crud as activities_crud
from processors import strava_activity_processor, strava_gear_processor
from dependencies import (
dependencies_database,
dependencies_session,
)
import database
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@@ -33,7 +39,7 @@ logger = logging.getLogger("myLogger")
async def strava_link(
state: str,
code: str,
db: Session = Depends(dependencies_database.get_db),
db: Session = Depends(database.get_db),
):
# Define the token URL
token_url = "https://www.strava.com/oauth/token"
@@ -63,7 +69,7 @@ async def strava_link(
# Get the user integrations by the state
user_integrations = (
crud_user_integrations.get_user_integrations_by_strava_state(state, db)
user_integrations_crud.get_user_integrations_by_strava_state(state, db)
)
# Check if user integrations is None
@@ -74,7 +80,7 @@ async def strava_link(
)
# Update the user integrations with the tokens
crud_user_integrations.link_strava_account(user_integrations, tokens, db)
user_integrations_crud.link_strava_account(user_integrations, tokens, db)
# Redirect to the main page or any other desired page after processing
redirect_url = (
@@ -107,7 +113,7 @@ async def strava_retrieve_activities_days(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
],
# db: Annotated[Session, Depends(dependencies_database.get_db)],
# db: Annotated[Session, Depends(database.get_db)],
background_tasks: BackgroundTasks,
):
# Process strava activities in the background
@@ -138,10 +144,10 @@ async def strava_set_user_unique_state(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
],
db: Annotated[Session, Depends(dependencies_database.get_db)],
db: Annotated[Session, Depends(database.get_db)],
):
# Set the user Strava state
crud_user_integrations.set_user_strava_state(user_id, state, db)
user_integrations_crud.set_user_strava_state(user_id, state, db)
# Return success message
return {"detail": f"Strava state for user {user_id} edited successfully"}
@@ -153,12 +159,15 @@ async def strava_set_user_unique_state(
)
async def strava_unset_user_unique_state(
user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
int,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
],
db: Annotated[Session, Depends(dependencies_database.get_db)],
db: Annotated[Session, Depends(database.get_db)],
):
# Set the user Strava state
crud_user_integrations.set_user_strava_state(user_id, None, db)
user_integrations_crud.set_user_strava_state(user_id, None, db)
# Return success message
return {"detail": f"Strava state for user {user_id} removed successfully"}
@@ -167,18 +176,21 @@ async def strava_unset_user_unique_state(
@router.delete("/strava/unlink", tags=["strava"])
async def strava_unlink(
token_user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
int,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
],
db: Session = Depends(dependencies_database.get_db),
db: Session = Depends(database.get_db),
):
# delete all strava gear for user
crud_gear.delete_all_strava_gear_for_user(token_user_id, db)
gears_crud.delete_all_strava_gear_for_user(token_user_id, db)
# delete all strava activities for user
crud_activities.delete_all_strava_activities_for_user(token_user_id, db)
activities_crud.delete_all_strava_activities_for_user(token_user_id, db)
# unlink strava account
crud_user_integrations.unlink_strava_account(token_user_id, db)
user_integrations_crud.unlink_strava_account(token_user_id, db)
# Return success message
return {"detail": f"Strava unlinked for user {token_user_id} successfully"}
@@ -187,7 +199,10 @@ async def strava_unlink(
@router.get("/strava/gear", status_code=202, tags=["strava"])
async def strava_retrieve_gear(
token_user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
int,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
],
background_tasks: BackgroundTasks,
):

View File

View File

@@ -1,8 +1,5 @@
import os
# Constant related to version
API_VERSION = "v0.2.1"
# JWT Token constants
JWT_ALGORITHM = os.environ.get("ALGORITHM")
JWT_ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES"))
@@ -10,28 +7,26 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS = int(os.environ.get("REFRESH_TOKEN_EXPIRE_DAYS"))
JWT_SECRET_KEY = os.environ.get("SECRET_KEY")
# Scopes definition
USERS_ADMIN_SCOPES = ["users:read", "users:edit", "users:write"]
USERS_REGULAR_SCOPES = ["users:read", "users:edit"]
GEARS_SCOPES = ["gears:read", "gears:edit", "gears:write"]
ACTIVITIES_SCOPES = ["activities:read", "activities:edit", "activities:write"]
USERS_REGULAR_SCOPES = ["profile", "users:read"]
USERS_ADMIN_SCOPES = ["users:write"]
GEARS_SCOPES = ["gears:read", "gears:write"]
ACTIVITIES_SCOPES = ["activities:read", "activities:write"]
SCOPES_DICT = {
"profile": "Privileges over user's own profile",
"users:read": "Read privileges over users",
"users:write": "Create privileges over users",
"users:edit": "Edit privileges over users",
"gears:read": "Read privileges over gears",
"gears:write": "Create privileges over gears",
"gears:edit": "Edit privileges over gears",
"activities:read": "Read privileges over activities",
"activities:write": "Create privileges over activities",
"activities:edit": "Edit privileges over activities",
}
# Constants related to user access types
REGULAR_ACCESS = 1
REGULAR_ACCESS_SCOPES = USERS_REGULAR_SCOPES + GEARS_SCOPES + ACTIVITIES_SCOPES
ADMIN_ACCESS = 2
ADMIN_ACCESS_SCOPES = USERS_ADMIN_SCOPES + GEARS_SCOPES + ACTIVITIES_SCOPES
ADMIN_ACCESS_SCOPES = USERS_REGULAR_SCOPES + USERS_ADMIN_SCOPES + GEARS_SCOPES + ACTIVITIES_SCOPES
# Constants related to user active status
USER_ACTIVE = 1
USER_NOT_ACTIVE = 2
USER_NOT_ACTIVE = 2

View File

@@ -9,11 +9,14 @@ from fastapi.security import OAuth2PasswordBearer
from joserfc import jwt
from joserfc.jwk import OctKey
from constants import (
JWT_ALGORITHM,
JWT_SECRET_KEY,
ADMIN_ACCESS,
)
import session.security as session_security
import session.constants as session_constants
# from constants import (
# JWT_ALGORITHM,
# JWT_SECRET_KEY,
# ADMIN_ACCESS,
# )
# Define the OAuth2 scheme for handling bearer tokens
@@ -23,49 +26,9 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
logger = logging.getLogger("myLogger")
def decode_token(token: Annotated[str, Depends(oauth2_scheme)]):
try:
# Decode the token and return the payload
return jwt.decode(token, OctKey.import_key(JWT_SECRET_KEY))
except Exception:
# Log the error and raise the exception
logger.info("Unable to decode token | Returning 401 response")
# Raise an HTTPException with a 401 Unauthorized status code
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unable to decode token",
headers={"WWW-Authenticate": "Bearer"},
)
def validate_token_expiration(token: Annotated[str, Depends(oauth2_scheme)]):
# Try to decode the token and check if it is expired
try:
# Decode the token
# Mark exp claim as required
claims_requests = jwt.JWTClaimsRegistry(exp={"essential": True})
# decodes the token
payload = decode_token(token)
# Validate token exp
claims_requests.validate(payload.claims)
except Exception:
# Log the error and raise the exception
logger.info("Token expired during validation | Returning 401 response")
# 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"},
)
def get_token_scopes(token: Annotated[str, Depends(oauth2_scheme)]):
# Decode the token
payload = decode_token(token)
payload = session_security.decode_token(token)
try:
# Get the scopes from the payload and return it
@@ -84,7 +47,7 @@ def get_token_scopes(token: Annotated[str, Depends(oauth2_scheme)]):
def get_token_user_id(token: Annotated[str, Depends(oauth2_scheme)]):
# Decode the token
payload = decode_token(token)
payload = session_security.decode_token(token)
try:
# Get the user id from the payload and return it
@@ -103,7 +66,7 @@ def get_token_user_id(token: Annotated[str, Depends(oauth2_scheme)]):
def get_token_access_type(token: Annotated[str, Depends(oauth2_scheme)]):
# Decode the token
payload = decode_token(token)
payload = session_security.decode_token(token)
try:
# Get the user access_type from the payload and return it
@@ -123,7 +86,7 @@ def get_token_access_type(token: Annotated[str, Depends(oauth2_scheme)]):
def validate_token_admin_access(token: Annotated[str, Depends(oauth2_scheme)]):
if get_token_access_type(token) != ADMIN_ACCESS:
if get_token_access_type(token) != session_constants.ADMIN_ACCESS:
# Raise an HTTPException with a 403 Forbidden status code
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -133,4 +96,8 @@ def validate_token_admin_access(token: Annotated[str, Depends(oauth2_scheme)]):
def create_token(data: dict):
# Encode the data and return the token
return jwt.encode({"alg": JWT_ALGORITHM}, data.copy(), JWT_SECRET_KEY)
return jwt.encode(
{"alg": session_constants.JWT_ALGORITHM},
data.copy(),
session_constants.JWT_SECRET_KEY,
)

View File

@@ -0,0 +1,93 @@
import logging
from typing import Annotated
from fastapi import (
APIRouter,
Depends,
HTTPException,
status,
Response,
)
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
import session.utils as session_utils
import session.security as session_security
import session.constants as session_constants
import users.crud as users_crud
import database
# from constants import (
# USER_NOT_ACTIVE,
# )
# Define the API router
router = APIRouter()
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@router.post("/token")
async def login_for_access_token(
response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[
Session,
Depends(database.get_db),
],
):
user = session_utils.authenticate_user(form_data.username, form_data.password, db)
if user.is_active == session_constants.USER_NOT_ACTIVE:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
headers={"WWW-Authenticate": "Bearer"},
)
response = session_utils.create_response_with_tokens(response, user)
return {"message": "Login successful"}
@router.post("/refresh")
async def refresh_token(
response: Response,
user_id: Annotated[
int,
Depends(session_security.validate_refresh_token_and_get_authenticated_user_id),
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# get user
user = users_crud.get_user_by_id(user_id, db)
if user.is_active == session_constants.USER_NOT_ACTIVE:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
headers={"WWW-Authenticate": "Bearer"},
)
response = session_utils.create_response_with_tokens(response, user)
return {"message": "Token refreshed successfully"}
@router.post("/logout")
async def logout(
response: Response,
):
# Clear the cookies by setting their expiration to the past
response.delete_cookie(key="endurain_access_token", path="/")
response.delete_cookie(key="endurain_refresh_token", path="/")
# response.delete_cookie(key="ctr_csrf_token", path="/")
return {"message": "Logout successful"}

View File

@@ -0,0 +1,210 @@
import bcrypt
import logging
from typing import Annotated, Callable
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
# import the jwt module from the joserfc package
from joserfc import jwt
from joserfc.jwk import OctKey
import session.constants as session_constants
# from constants import (
# JWT_ALGORITHM,
# JWT_SECRET_KEY,
# ADMIN_ACCESS,
# )
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes=session_constants.SCOPES_DICT,
)
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
def hash_password(password: str):
# Hash the password and return it
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
def verify_password(plain_password: str, hashed_password: str):
# Check if the password is equal to the hashed password
return bcrypt.checkpw(
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
)
def decode_token(token: Annotated[str, Depends(oauth2_scheme)]):
try:
# Decode the token and return the payload
return jwt.decode(token, OctKey.import_key(session_constants.JWT_SECRET_KEY))
except Exception:
# Log the error and raise the exception
logger.info("Unable to decode token | Returning 401 response")
# Raise an HTTPException with a 401 Unauthorized status code
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unable to decode token",
headers={"WWW-Authenticate": "Bearer"},
)
def validate_token_expiration(token: Annotated[str, Depends(oauth2_scheme)]):
# Try to decode the token and check if it is expired
try:
# Decode the token
# Mark exp claim as required
claims_requests = jwt.JWTClaimsRegistry(exp={"essential": True})
# decodes the token
payload = decode_token(token)
# Validate token exp
claims_requests.validate(payload.claims)
except Exception:
# Log the error and raise the exception
logger.info("Token expired during validation | Returning 401 response")
# 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"},
)
def get_token_user_id(token: Annotated[str, Depends(oauth2_scheme)]):
# Decode the token
payload = decode_token(token)
try:
# Get the user id from the payload and return it
return payload.claims["sub"]
except Exception:
# Log the error and raise the exception
logger.info("Claim with user ID not present in token | Returning 401 response")
# Raise an HTTPException with a 401 Unauthorized status code
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Claim with user ID not present in token",
headers={"WWW-Authenticate": "Bearer"},
)
def get_token_scopes(token: Annotated[str, Depends(oauth2_scheme)]):
# Decode the token
payload = decode_token(token)
try:
# Get the scopes from the payload and return it
return payload.claims["scopes"]
except Exception:
# Log the error and raise the exception
logger.info("Scopes not present in token | Returning 401 response")
# Raise an HTTPException with a 401 Unauthorized status code
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Scopes not present in token",
headers={"WWW-Authenticate": "Bearer"},
)
def create_token(data: dict):
# Encode the data and return the token
return jwt.encode(
{"alg": session_constants.JWT_ALGORITHM},
data.copy(),
session_constants.JWT_SECRET_KEY,
)
## ACCESS TOKEN VALIDATION
def get_access_token_from_cookies(request: Request):
# Extract the access token from the cookies
access_token = request.cookies.get("endurain_access_token")
# Check if the token is missing
if not access_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access token missing",
headers={"WWW-Authenticate": "Bearer"},
)
# Return token
return access_token
def validate_access_token(
access_token: Annotated[str, Depends(get_access_token_from_cookies)]
):
# Validate the token expiration
validate_token_expiration(access_token)
def get_user_id_from_access_token(
access_token: Annotated[str, Depends(get_access_token_from_cookies)]
):
# Return the user ID associated with the token
return get_token_user_id(access_token)
def get_and_return_access_token(
access_token: Annotated[str, Depends(get_access_token_from_cookies)],
):
# Return token
return access_token
## REFRESH TOKEN VALIDATION
def validate_token_and_return_refresh_token(request: Request):
# Extract the refresh token from the cookies
refresh_token = request.cookies.get("endurain_refresh_token")
if not refresh_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Refresh token missing",
headers={"WWW-Authenticate": "Bearer"},
)
# Validate the token expiration
validate_token_expiration(refresh_token)
# Return token
return refresh_token
def validate_refresh_token_and_get_authenticated_user_id(
refresh_token: Annotated[str, Depends(validate_token_and_return_refresh_token)]
):
# Return the user ID associated with the token
return get_token_user_id(refresh_token)
def check_scopes(
access_token: Annotated[
str, Depends(get_access_token_from_cookies)
],
security_scopes: SecurityScopes,
):
# Get the scopes from the token
scopes = get_token_scopes(access_token)
# Check if the token has the required scopes
for scope in security_scopes.scopes:
if scope not in scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Unauthorized Access - Not enough permissions - scope={security_scopes.scopes}",
headers={
"WWW-Authenticate": f'Bearer scope="{security_scopes.scopes}"'
},
)

View File

@@ -0,0 +1,104 @@
from datetime import datetime, timedelta, timezone
from fastapi import (
HTTPException,
status,
Response,
)
from sqlalchemy.orm import Session
import session.security as session_security
import session.constants as session_constants
import users.crud as users_crud
import users.schema as users_schema
# from constants import (
# REGULAR_ACCESS,
# REGULAR_ACCESS_SCOPES,
# ADMIN_ACCESS_SCOPES,
# JWT_ACCESS_TOKEN_EXPIRE_MINUTES,
# JWT_REFRESH_TOKEN_EXPIRE_DAYS,
# )
def authenticate_user(username: str, password: str, db: Session):
# Get the user from the database
user = users_crud.authenticate_user(username, db)
# Check if the user exists and if the hashed_password is correct and if not return False
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username",
headers={"WWW-Authenticate": "Bearer"},
)
if not session_security.verify_password(password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect password",
headers={"WWW-Authenticate": "Bearer"},
)
# Return the user if the hashed_password is correct
return user
def create_response_with_tokens(response: Response, user: users_schema.User):
# Check user access level and set scopes accordingly
if user.access_type == session_constants.REGULAR_ACCESS:
scopes = session_constants.REGULAR_ACCESS_SCOPES
else:
scopes = session_constants.ADMIN_ACCESS_SCOPES
# Create the access and refresh tokens
access_token = session_security.create_token(
data={
"sub": user.id,
"scopes": scopes,
"exp": datetime.now(timezone.utc)
+ timedelta(minutes=session_constants.JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
},
)
refresh_token = session_security.create_token(
data={
"sub": user.id,
"scopes": "scopes",
"exp": datetime.now(timezone.utc)
+ timedelta(days=session_constants.JWT_REFRESH_TOKEN_EXPIRE_DAYS),
},
)
# Set the cookies with the tokens
response.set_cookie(
key="endurain_access_token",
value=access_token,
expires=datetime.now(timezone.utc)
+ timedelta(minutes=session_constants.JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
httponly=True,
path="/",
secure=False,
samesite="None",
)
response.set_cookie(
key="endurain_refresh_token",
value=refresh_token,
expires=datetime.now(timezone.utc)
+ timedelta(days=session_constants.JWT_REFRESH_TOKEN_EXPIRE_DAYS),
httponly=True,
path="/",
secure=False,
samesite="None",
)
# Set the user id in a cookie
# response.set_cookie(
# key="endurain_logged_user_id",
# value=user.id,
# httponly=False,
# )
# Return the response
return response

View File

View File

View File

@@ -4,7 +4,7 @@ from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from datetime import datetime
from schemas import schema_user_integrations
import user_integrations.schema as user_integrations_schema
import models
# Define a loggger created on main.py
@@ -13,17 +13,22 @@ logger = logging.getLogger("myLogger")
def get_user_integrations_by_user_id(user_id: int, db: Session):
try:
# Get the user integrations by the user id
user_integrations = (
db.query(models.UserIntegrations)
.filter(models.UserIntegrations.user_id == user_id)
.first()
)
# Check if user_integrations is None and return None if it is
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 the user integrations
return user_integrations
except Exception as err:
# Log the exception
@@ -89,7 +94,7 @@ def create_user_integrations(user_id: int, db: Session):
def link_strava_account(
user_integrations: schema_user_integrations.UserIntegrations,
user_integrations: user_integrations_schema.UserIntegrations,
tokens: dict,
db: Session,
):

View File

@@ -11,4 +11,4 @@ class UserIntegrations(BaseModel):
strava_sync_gear: bool
class Config:
orm_mode = True
from_attributes = True

View File

View File

@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from urllib.parse import unquote
from schemas import schema_users
import users.schema as users_schema
import models
# Define a loggger created on main.py
@@ -272,7 +272,7 @@ def get_user_photo_path_aux_by_id(user_id: int, db: Session):
) from err
def create_user(user: schema_users.UserCreate, db: Session):
def create_user(user: users_schema.UserCreate, db: Session):
try:
# Create a new user
db_user = models.User(
@@ -320,7 +320,7 @@ def create_user(user: schema_users.UserCreate, db: Session):
) from err
def edit_user(user: schema_users.User, db: Session):
def edit_user(user: users_schema.User, db: Session):
try:
# Get the user from the database
db_user = db.query(models.User).filter(models.User.id == user.id).first()

View File

@@ -4,24 +4,26 @@ import logging
from typing import Annotated, Callable
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, Security
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
import shutil
from schemas import schema_users
from crud import crud_user_integrations, crud_users
import users.schema as users_schema
import users.crud as users_crud
import users.dependencies as users_dependencies
import user_integrations.crud as user_integrations_crud
import session.security as session_security
import database
from dependencies import (
dependencies_database,
dependencies_session,
dependencies_global,
dependencies_users,
dependencies_security,
# dependencies_security,
)
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Define the API router
router = APIRouter()
@@ -29,24 +31,22 @@ router = APIRouter()
logger = logging.getLogger("myLogger")
@router.get("/users/me", response_model=schema_users.UserMe, tags=["users"])
@router.get("/me", response_model=users_schema.UserMe)
async def read_users_me(
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["profile"])
],
token_user_id: Annotated[
int,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the user from the database
user = crud_users.get_user_by_id(token_user_id, db)
user = users_crud.get_user_by_id(token_user_id, db)
# If the user does not exist raise the exception
if user is None:
@@ -56,7 +56,7 @@ async def read_users_me(
headers={"WWW-Authenticate": "Bearer"},
)
user_integrations = crud_user_integrations.get_user_integrations_by_user_id(
user_integrations = user_integrations_crud.get_user_integrations_by_user_id(
user.id, db
)
@@ -67,34 +67,32 @@ async def read_users_me(
headers={"WWW-Authenticate": "Bearer"},
)
if user_integrations.strava_token is None:
user.is_strava_linked = 0
else:
user.is_strava_linked = 1
user.is_strava_linked = 1 if user_integrations.strava_token else 0
# Return the user
return user
@router.get("/users/number", response_model=int, tags=["users"])
@router.get("/number", response_model=int)
async def read_users_number(
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
return crud_users.get_users_number(db)
return users_crud.get_users_number(db)
@router.get(
"/users/all/page_number/{page_number}/num_records/{num_records}",
response_model=list[schema_users.User] | None,
"/all/page_number/{page_number}/num_records/{num_records}",
response_model=list[users_schema.User] | None,
tags=["users"],
)
async def read_users_all_pagination(
@@ -104,148 +102,151 @@ async def read_users_all_pagination(
Callable, Depends(dependencies_global.validate_pagination_values)
],
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the users from the database with pagination
return crud_users.get_users_with_pagination(
return users_crud.get_users_with_pagination(
db=db, page_number=page_number, num_records=num_records
)
@router.get(
"/users/username/contains/{username}",
response_model=list[schema_users.User] | None,
"/username/contains/{username}",
response_model=list[users_schema.User] | None,
tags=["users"],
)
async def read_users_contain_username(
username: str,
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the users from the database by username
return crud_users.get_user_if_contains_username(username=username, db=db)
return users_crud.get_user_if_contains_username(username=username, db=db)
@router.get(
"/users/username/{username}",
response_model=schema_users.User | None,
"/username/{username}",
response_model=users_schema.User | None,
tags=["users"],
)
async def read_users_username(
username: str,
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the user from the database by username
return crud_users.get_user_by_username(username=username, db=db)
return users_crud.get_user_by_username(username=username, db=db)
@router.get("/users/id/{user_id}", response_model=schema_users.User, tags=["users"])
@router.get("/id/{user_id}", response_model=users_schema.User)
async def read_users_id(
user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_access_token: Annotated[
Callable, Depends(dependencies_session.validate_access_token)
],
validate_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the users from the database by id
return crud_users.get_user_by_id(user_id=user_id, db=db)
return users_crud.get_user_by_id(user_id=user_id, db=db)
@router.get("/users/{username}/id", response_model=int, tags=["users"])
@router.get("/{username}/id", response_model=int)
async def read_users_username_id(
username: str,
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the users from the database by username
return crud_users.get_user_id_by_username(username, db)
return users_crud.get_user_id_by_username(username, db)
@router.get("/users/{user_id}/photo_path", response_model=str | None, tags=["users"])
@router.get("/{user_id}/photo_path", response_model=str | None)
async def read_users_id_photo_path(
user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
Callable, Security(session_security.check_scopes, scopes=["users:read"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Get the photo_path from the database by id
return crud_users.get_user_photo_path_by_id(user_id, db)
return users_crud.get_user_photo_path_by_id(user_id, db)
@router.post("/users/create", response_model=int, status_code=201, tags=["users"])
@router.post("/create", response_model=int, status_code=201)
async def create_user(
user: schema_users.UserCreate,
user: users_schema.UserCreate,
validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
Callable,
Depends(dependencies_session.validate_access_token_and_validate_admin_access),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:write"])
Callable, Security(session_security.check_scopes, scopes=["users:write"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Create the user in the database
created_user = crud_users.create_user(user, db)
created_user = users_crud.create_user(user, db)
# Create the user integrations in the database
crud_user_integrations.create_user_integrations(created_user.id, db)
user_integrations_crud.create_user_integrations(created_user.id, db)
# Return the user id
return created_user.id
@router.post(
"/users/{user_id}/upload/image",
"/{user_id}/upload/image",
status_code=201,
response_model=str | None,
tags=["users"],
@@ -260,11 +261,11 @@ async def upload_user_image(
],
file: UploadFile,
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
Callable, Security(session_security.check_scopes, scopes=["users:edit"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
try:
@@ -280,7 +281,7 @@ async def upload_user_image(
with open(file_path_to_save, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return crud_users.edit_user_photo_path(user_id, file_path_to_save, db)
return users_crud.edit_user_photo_path(user_id, file_path_to_save, db)
except Exception as err:
# Log the exception
logger.error(f"Error in upload_user_image: {err}", exc_info=True)
@@ -296,9 +297,9 @@ async def upload_user_image(
) from err
@router.put("/users/edit", tags=["users"])
@router.put("/edit")
async def edit_user(
user_attributtes: schema_users.User,
user_attributtes: users_schema.User,
validate_token_user_id: Annotated[
Callable,
Depends(
@@ -306,23 +307,23 @@ async def edit_user(
),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
Callable, Security(session_security.check_scopes, scopes=["users:edit"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Update the user in the database
crud_users.edit_user(user_attributtes, db)
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"])
@router.put("/edit/password")
async def edit_user_password(
user_attributtes: schema_users.UserEditPassword,
user_attributtes: users_schema.UserEditPassword,
validate_token_user_id: Annotated[
Callable,
Depends(
@@ -330,21 +331,21 @@ async def edit_user_password(
),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
Callable, Security(session_security.check_scopes, scopes=["users:edit"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Update the user password in the database
crud_users.edit_user_password(user_attributtes.id, user_attributtes.password, db)
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"])
@router.put("/{user_id}/delete-photo")
async def delete_user_photo(
user_id: int,
validate_token_user_id: Annotated[
@@ -354,37 +355,37 @@ async def delete_user_photo(
),
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
Callable, Security(session_security.check_scopes, scopes=["users:edit"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Update the user photo_path in the database
crud_users.delete_user_photo(user_id, db)
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"])
@router.delete("/{user_id}/delete")
async def delete_user(
user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_id: Annotated[Callable, Depends(users_dependencies.validate_user_id)],
validate_token_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
],
check_scopes: Annotated[
Callable, Security(dependencies_session.check_scopes, scopes=["users:write"])
Callable, Security(session_security.check_scopes, scopes=["users:write"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
Depends(database.get_db),
],
):
# Delete the user in the database
crud_users.delete_user(user_id, db)
users_crud.delete_user(user_id, db)
# Return success message
return {"detail": f"User ID {user_id} deleted successfully"}

View File

@@ -23,7 +23,6 @@ class UserCreate(User):
class UserMe(User):
id: int
is_strava_linked: int | None = None
class UserEditPassword(BaseModel):

View File

@@ -1,12 +0,0 @@
from database import SessionLocal
def get_db():
# Create a new database session and return it
db = SessionLocal()
try:
# Yield the database session
yield db
finally:
# Close the database session
db.close()

2226
backend/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

31
backend/pyproject.toml Normal file
View File

@@ -0,0 +1,31 @@
[tool.poetry]
name = "endurain"
version = "0.3.0"
description = "Endurain API for the Endurain app"
authors = ["João Vitória Silva <8648976+joaovitoriasilva@users.noreply.github.com>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.111.0"
uvicorn = "^0.30.1"
python-dotenv = "^1.0.1"
sqlalchemy = "^2.0.31"
apscheduler = "^3.10.4"
requests = "^2.32.3"
stravalib = "^1.7"
opentelemetry-sdk = "^1.25.0"
opentelemetry-instrumentation-fastapi = "^0.46b0"
opentelemetry-exporter-otlp = "^1.25.0"
python-multipart = "^0.0.9"
gpxpy = "^1.6.2"
alembic = "^1.13.2"
joserfc = "^0.12.0"
bcrypt = "^4.1.3"
mysqlclient = "^2.2.4"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,69 +0,0 @@
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_security,
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_security.validate_token_expiration)
],
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_security.validate_token_expiration)
],
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
)

View File

@@ -1,226 +0,0 @@
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_followers
from crud import crud_followers
from dependencies import (
dependencies_database,
dependencies_security,
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(
"/followers/user/{user_id}/followers/all",
response_model=list[schema_followers.Follower] | None,
tags=["followers"],
)
async def get_user_follower_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followers
return crud_followers.get_all_following_by_user_id(user_id, db)
@router.get(
"/followers/user/{user_id}/followers/count/all",
response_model=int,
tags=["followers"],
)
async def get_user_follower_count_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followers
followers = crud_followers.get_all_followers_by_user_id(user_id, db)
# Check if followers is None and return 0 if it is
if followers is None:
return 0
# Return the number of followers
return len(followers)
@router.get(
"/followers/user/{user_id}/followers/count/accepted",
response_model=int,
tags=["followers"],
)
async def get_user_follower_count(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followers
followers = crud_followers.get_accepted_followers_by_user_id(user_id, db)
# Check if followers is None and return 0 if it is
if followers is None:
return 0
# Return the number of followers
return len(followers)
@router.get(
"/followers/user/{user_id}/following/all",
response_model=list[schema_followers.Follower] | None,
tags=["followers"],
)
async def get_user_following_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followings
return crud_followers.get_all_followers_by_user_id(user_id, db)
@router.get(
"/followers/user/{user_id}/following/count/all",
response_model=int,
tags=["followers"],
)
async def get_user_following_count_all(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followings
followings = crud_followers.get_all_following_by_user_id(user_id, db)
# Check if followings is None and return 0 if it is
if followings is None:
return 0
# Return the number of followings
return len(followings)
@router.get(
"/followers/user/{user_id}/following/count/accepted",
response_model=int,
tags=["followers"],
)
async def get_user_following_count(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followings
followings = crud_followers.get_accepted_following_by_user_id(user_id, db)
# Check if followings is None and return 0 if it is
if followings is None:
return 0
# Return the number of followings
return len(followings)
@router.get(
"/followers/user/{user_id}/targetUser/{target_user_id}",
response_model=schema_followers.Follower | None,
tags=["followers"],
)
async def read_followers_user_specific_user(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return the follower
return crud_followers.get_follower_for_user_id_and_target_user_id(
user_id, target_user_id, db
)
@router.post(
"/followers/create/user/{user_id}/targetUser/{target_user_id}",
status_code=201,
tags=["followers"],
)
async def create_follow(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Create the follower
new_follow = crud_followers.create_follower(user_id, target_user_id, db)
# Return the ID of the gear created
return {"detail": "Follower record created successfully"}
@router.put("/followers/accept/user/{user_id}/targetUser/{target_user_id}",
tags=["followers"],
)
async def accept_follow(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Accept the follower
crud_followers.accept_follower(user_id, target_user_id, db)
# Return success message
return {"detail": "Follower record accepted successfully"}
@router.delete(
"/followers/delete/user/{user_id}/targetUser/{target_user_id}",
tags=["followers"],
)
async def delete_follow(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
target_user_id: int,
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Delete the follower
crud_followers.delete_follower(user_id, target_user_id, db)
# Return success message
return {"detail": "Follower record deleted successfully"}

View File

@@ -1,191 +0,0 @@
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_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
user_id: Annotated[
int, Depends(dependencies_session.validate_access_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_access_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_access_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_access_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_access_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_access_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.put("/gear/{gear_id}/edit", tags=["gear"])
async def edit_gear(
gear_id: int,
validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
gear: schema_gear.Gear,
token_user_id: Annotated[
int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
],
db: Session = Depends(dependencies_database.get_db),
):
# Get the gear by id
gear_db = 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_db is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Gear ID {gear_id} for user {token_user_id} not found",
)
# Edit the gear
crud_gear.edit_gear(gear_id, gear, db)
# Return success message
return {"detail": f"Gear ID {gear_id} edited successfully"}
@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_access_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"}

View File

@@ -1,195 +0,0 @@
import logging
import bcrypt
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import (
APIRouter,
Depends,
HTTPException,
status,
Response,
Request,
)
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from crud import crud_users
from schemas import schema_users
from constants import (
USER_NOT_ACTIVE,
REGULAR_ACCESS,
REGULAR_ACCESS_SCOPES,
ADMIN_ACCESS_SCOPES,
SCOPES_DICT,
JWT_ACCESS_TOKEN_EXPIRE_MINUTES,
JWT_REFRESH_TOKEN_EXPIRE_DAYS,
)
from dependencies import (
dependencies_database,
dependencies_session,
dependencies_security,
)
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes=SCOPES_DICT,
)
# Define the API router
router = APIRouter()
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
def hash_password(password: str):
# Hash the password and return it
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
def verify_password(plain_password: str, hashed_password: str):
# Check if the password is equal to the hashed password
return bcrypt.checkpw(
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
)
def authenticate_user(username: str, password: str, db: Session):
# Get the user from the database
user = crud_users.authenticate_user(username, db)
# Check if the user exists and if the hashed_password is correct and if not return False
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect password",
headers={"WWW-Authenticate": "Bearer"},
)
# Return the user if the hashed_password is correct
return user
def create_response_with_tokens(response: Response, user: schema_users.User):
# Check user access level and set scopes accordingly
if user.access_type == REGULAR_ACCESS:
scopes = REGULAR_ACCESS_SCOPES
else:
scopes = ADMIN_ACCESS_SCOPES
# Create the access and refresh tokens
access_token = dependencies_security.create_token(
data={
"sub": user.username,
"scopes": scopes,
"id": user.id,
"access_type": user.access_type,
"exp": datetime.now(timezone.utc)
+ timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
},
)
refresh_token = dependencies_security.create_token(
data={
"sub": user.username,
"scopes": "scopes",
"id": user.id,
"access_type": user.access_type,
"exp": datetime.now(timezone.utc)
+ timedelta(days=JWT_REFRESH_TOKEN_EXPIRE_DAYS),
},
)
# Set the cookies with the tokens
response.set_cookie(
key="endurain_access_token",
value=access_token,
expires=datetime.now(timezone.utc)
+ timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
httponly=True,
path="/",
secure=False,
samesite="None",
)
response.set_cookie(
key="endurain_refresh_token",
value=refresh_token,
expires=datetime.now(timezone.utc)
+ timedelta(days=JWT_REFRESH_TOKEN_EXPIRE_DAYS),
httponly=True,
path="/",
secure=False,
samesite="None",
)
# Set the user id in a cookie
#response.set_cookie(
# key="endurain_logged_user_id",
# value=user.id,
# httponly=False,
#)
# Return the response
return response
@router.post("/token", tags=["session"])
async def login_for_access_token(
response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
],
):
user = authenticate_user(form_data.username, form_data.password, db)
if user.is_active == USER_NOT_ACTIVE:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
headers={"WWW-Authenticate": "Bearer"},
)
response = create_response_with_tokens(response, user)
return {"message": "Login successful"}
@router.post("/refresh", tags=["session"])
async def refresh_token(
response: Response,
user_id: Annotated[
int,
Depends(
dependencies_session.validate_refresh_token_and_get_authenticated_user_id
),
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
],
):
# get user
user = crud_users.get_user_by_id(user_id, db)
if user.is_active == USER_NOT_ACTIVE:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user",
headers={"WWW-Authenticate": "Bearer"},
)
response = create_response_with_tokens(response, user)
return {"message": "Token refreshed successfully"}

View File

@@ -1,2 +0,0 @@
VITE_BACKEND_PROTOCOL=MY_APP_BACKEND_PROTOCOL
VITE_BACKEND_HOST=MY_APP_BACKEND_HOST

33
frontend/.gitignore vendored
View File

@@ -1,33 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# Sentry Config File
.env.sentry-build-plugin

View File

@@ -8,11 +8,11 @@ LABEL org.opencontainers.image.source https://github.com/joaovitoriasilva/endura
WORKDIR /app
# Copy package.json and package-lock.json
COPY frontend/package*.json ./
COPY app/package*.json ./
RUN npm install
# Copy the current directory contents into the container at /app
COPY frontend ./
COPY app ./
# Build the app
RUN npm run build

3
frontend/app/.env Normal file
View File

@@ -0,0 +1,3 @@
VITE_BACKEND_PROTOCOL=http
VITE_BACKEND_HOST=localhost
VITE_BACKEND_PORT=98

View File

@@ -4312,9 +4312,9 @@
"dev": true
},
"node_modules/ws": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"engines": {
"node": ">=10.0.0"

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,11 +1,13 @@
<script setup>
import { RouterView } from 'vue-router'
import NavbarComponent from './components/NavbarComponent.vue'
import ToastContainerComponent from './components/Toasts/ToastContainerComponent.vue'
import NavbarComponent from './components/Navbar/NavbarComponent.vue'
import FooterComponent from './components/FooterComponent.vue'
</script>
<template>
<NavbarComponent />
<ToastContainerComponent />
<main class="container py-4 bg-body">
<RouterView />
</main>

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -12,7 +12,7 @@
<script>
import { ref, onMounted, watchEffect, nextTick } from 'vue';
import { activityStreams } from '@/services/activityStreams';
import LoadingComponent from '@/components/LoadingComponent.vue';
import LoadingComponent from '@/components/GeneralComponents/LoadingComponent.vue';
import L from 'leaflet';
export default {

View File

@@ -186,14 +186,14 @@ import { ref, onMounted, watchEffect, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
// Importing the components
import LoadingComponent from '@/components/LoadingComponent.vue';
import LoadingComponent from '@/components/GeneralComponents/LoadingComponent.vue';
import ErrorToastComponent from '@/components/Toasts/ErrorToastComponent.vue';
import UserAvatarComponent from '@/components/Users/UserAvatarComponent.vue';
// Importing the stores
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
// Importing the services
import { users } from '@/services/user';
import { activities } from '@/services/activities';
import { users } from '@/services/usersService';
import { activities } from '@/services/activitiesService';
import { formatDate, formatTime, calculateTimeDifference } from '@/utils/dateTimeUtils';
import { formatPace } from '@/utils/activityUtils';

Some files were not shown because too many files have changed in this diff Show More