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
75
.gitignore
vendored
@@ -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
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
74
backend/app/activity_streams/router.py
Normal 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
@@ -0,0 +1,2 @@
|
||||
# Constant related to version
|
||||
API_VERSION = "v0.3.0"
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
257
backend/app/followers/router.py
Normal 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"}
|
||||
@@ -7,4 +7,4 @@ class Follower(BaseModel):
|
||||
is_accepted: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
from_attributes = True
|
||||
@@ -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
@@ -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"}
|
||||
35
backend/app/gears/utils.py
Normal 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
|
||||
@@ -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,
|
||||
@@ -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":
|
||||
@@ -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",
|
||||
0
backend/app/processors/__init__.py
Normal 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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
0
backend/app/routers/__init__.py
Normal 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,
|
||||
):
|
||||
0
backend/app/session/__init__.py
Normal 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
|
||||
@@ -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,
|
||||
)
|
||||
93
backend/app/session/router.py
Normal 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"}
|
||||
210
backend/app/session/security.py
Normal 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}"'
|
||||
},
|
||||
)
|
||||
104
backend/app/session/utils.py
Normal 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
|
||||
0
backend/app/uploads/__init__.py
Normal file
0
backend/app/user_images/__init__.py
Normal file
0
backend/app/user_integrations/__init__.py
Normal 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,
|
||||
):
|
||||
@@ -11,4 +11,4 @@ class UserIntegrations(BaseModel):
|
||||
strava_sync_gear: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
from_attributes = True
|
||||
0
backend/app/users/__init__.py
Normal 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()
|
||||
@@ -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"}
|
||||
@@ -23,7 +23,6 @@ class UserCreate(User):
|
||||
|
||||
|
||||
class UserMe(User):
|
||||
id: int
|
||||
is_strava_linked: int | None = None
|
||||
|
||||
class UserEditPassword(BaseModel):
|
||||
@@ -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
31
backend/pyproject.toml
Normal 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"
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -1,2 +0,0 @@
|
||||
VITE_BACKEND_PROTOCOL=MY_APP_BACKEND_PROTOCOL
|
||||
VITE_BACKEND_HOST=MY_APP_BACKEND_HOST
|
||||
33
frontend/.gitignore
vendored
@@ -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
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
VITE_BACKEND_PROTOCOL=http
|
||||
VITE_BACKEND_HOST=localhost
|
||||
VITE_BACKEND_PORT=98
|
||||
@@ -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"
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
@@ -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>
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -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 {
|
||||
@@ -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';
|
||||
|
||||