Finished backend structure migration

[backend] finished backend structure migration
[backend] logic to edit activity added
[backend] added 404 error if user or activity not found on edit logic
This commit is contained in:
João Vitória Silva
2024-07-18 10:01:58 +01:00
parent bb10e637a0
commit 75c3b19d99
16 changed files with 152 additions and 250 deletions

View File

@@ -497,6 +497,49 @@ def create_activity(activity: activities_schema.Activity, db: Session):
) from err
def edit_activity(user_id: int, activity: activities_schema.ActivityEdit, db: Session):
try:
# Get the activity from the database
db_activity = (
db.query(models.Activity)
.filter(
models.Activity.user_id == user_id,
models.Activity.id == activity.id,
)
.first()
)
if db_activity is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Activity not found",
headers={"WWW-Authenticate": "Bearer"},
)
# Update the activity
if activity.description is not None:
db_activity.description = activity.description
if activity.name is not None:
db_activity.name = activity.name
if activity.visibility is not None:
db_activity.visibility = activity.visibility
# Commit the transaction
db.commit()
except Exception as err:
# Rollback the transaction
db.rollback()
# Log the exception
logger.error(f"Error in edit_activity: {err}", exc_info=True)
# Raise an HTTPException with a 500 Internal Server Error status code
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal Server Error",
) from err
def add_gear_to_activity(activity_id: int, gear_id: int, db: Session):
try:
# Get the activity from the database

View File

@@ -24,11 +24,11 @@ import activity_streams.crud as activity_streams_crud
import gpx.utils as gpx_utils
import fit.utils as fit_utils
import database
import dependencies_global
from processors import fit_processor
# Define the API router
router = APIRouter()
@@ -51,7 +51,7 @@ async def read_activities_useractivities_week(
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -95,7 +95,7 @@ async def read_activities_useractivities_thisweek_distances(
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -141,7 +141,7 @@ async def read_activities_useractivities_thismonth_distances(
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -373,7 +373,7 @@ async def read_activities_activity_from_id(
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -397,7 +397,7 @@ async def read_activities_contain_name(
Callable, Security(session_security.check_scopes, scopes=["activities:read"])
],
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -416,7 +416,7 @@ async def read_activities_contain_name(
)
async def create_activity_with_uploaded_file(
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
file: UploadFile,
@@ -446,7 +446,7 @@ async def create_activity_with_uploaded_file(
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)
parsed_info = fit_utils.parse_fit_file(file.filename, token_user_id)
else:
# file extension not supported raise an HTTPException with a 406 Not Acceptable status code
raise HTTPException(
@@ -490,6 +490,30 @@ async def create_activity_with_uploaded_file(
) from err
@router.put(
"/edit",
)
async def edit_activity(
token_user_id: Annotated[
int,
Depends(session_security.get_user_id_from_access_token),
],
activity_attributes: activities_schema.ActivityEdit,
check_scopes: Annotated[
Callable, Security(session_security.check_scopes, scopes=["activities:write"])
],
db: Annotated[
Session,
Depends(database.get_db),
],
):
# Update the activity in the database
activities_crud.edit_activity(token_user_id, activity_attributes, db)
# Return success message
return {f"Activity ID {activity_attributes.id} updated successfully"}
@router.put(
"/{activity_id}/addgear/{gear_id}",
)

View File

@@ -32,4 +32,11 @@ class Activity(BaseModel):
class ActivityDistances(BaseModel):
swim: float
bike: float
run: float
run: float
class ActivityEdit(BaseModel):
id: int
description: str | None = None
name: str
visibility: int | None = None

View File

@@ -1,174 +0,0 @@
from fastapi import Depends, HTTPException, status, Request, Security
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from sqlalchemy.orm import Session
from typing import Annotated
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")
### 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")
if not access_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access token missing",
headers={"WWW-Authenticate": "Bearer"},
)
# Validate the token expiration
dependencies_security.validate_token_expiration(access_token)
def validate_token_and_return_access_token(request: Request):
# Extract the access token from the cookies
access_token = request.cookies.get("endurain_access_token")
if not access_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access token missing",
headers={"WWW-Authenticate": "Bearer"},
)
# Validate the token expiration
dependencies_security.validate_token_expiration(access_token)
# Return token
return access_token
def validate_access_token_and_get_authenticated_user_id(
access_token: Annotated[str, Depends(validate_token_and_return_access_token)],
):
# Return the user ID associated with the token
return dependencies_security.get_token_user_id(access_token)
def validate_access_token_and_validate_admin_access(
access_token: Annotated[str, Depends(validate_token_and_return_access_token)],
):
# Check if the token has admin access
dependencies_security.validate_token_admin_access(access_token)
### Dependencies for 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
dependencies_security.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 dependencies_security.get_token_user_id(refresh_token)
def validate_token_and_validate_admin_access(
token: str = Depends(oauth2_scheme),
db: Session = Depends(database.get_db),
):
# Validate the token expiration
dependencies_security.validate_token_expiration(token)
# Check if the token has admin access
dependencies_security.validate_token_admin_access(token)
def validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access(
user_id: int | None,
token: str = Depends(oauth2_scheme),
db: Session = Depends(database.get_db),
):
# Validate the token expiration
dependencies_security.validate_token_expiration(token)
# Check if user_id higher than 0
if not (int(user_id) > 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid user ID",
)
# Check if token id is different from user id. If yes, check if the token has admin access
if user_id != dependencies_security.get_token_user_id(token):
# Check if the token has admin access
dependencies_security.validate_token_admin_access(token)
def validate_token_and_if_user_id_equals_token_user_attributtes_id_if_not_validate_admin_access(
user_attributtes: users_schema.User,
token: str = Depends(oauth2_scheme),
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: users_schema.UserEditPassword,
token: str = Depends(oauth2_scheme),
db: Session = Depends(database.get_db),
):
validate_token_user_id_admin_access(db, token, user_attributtes.id)
def validate_token_user_id_admin_access(db, token, user_id):
# Validate the token expiration
dependencies_security.validate_token_expiration(token)
# Check if user_id higher than 0
if not (int(user_id) > 0):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Invalid user ID",
)
# Check if token id is different from user id. If yes, check if the token has admin access
if user_id != dependencies_security.get_token_user_id(token):
# Check if the token has admin access
dependencies_security.validate_token_admin_access(token)
def check_scopes(
security_scopes: SecurityScopes,
# scopes: Annotated[list[str], Depends(dependencies_security.get_token_scopes)],
access_token: Annotated[str, Depends(validate_token_and_return_access_token)],
):
# Get the scopes from the token
scopes = dependencies_security.get_token_scopes(access_token)
# Check if the token has the required scopes
for scope in security_scopes.scopes:
if scope not in scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Unauthorized Access - Not enough permissions - scope={security_scopes.scopes}",
headers={
"WWW-Authenticate": f'Bearer scope="{security_scopes.scopes}"'
},
)

View File

@@ -1,7 +1,7 @@
import logging
import os
from fastapi import FastAPI, Request, Depends, Security
from fastapi import FastAPI, Depends, Security
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
@@ -32,14 +32,12 @@ 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_strava,
)
import strava.router as strava_router
import strava.utils as strava_utils
import strava.activity_utils as strava_activity_utils
from config import API_VERSION
from database import SessionLocal
from processors import strava_processor, strava_activity_processor
def startup_event():
@@ -79,7 +77,7 @@ def refresh_strava_tokens_job():
db = SessionLocal()
try:
# Refresh Strava tokens
strava_processor.refresh_strava_tokens(db=db)
strava_utils.refresh_strava_tokens(db=db)
finally:
# Ensure the session is closed after use
db.close()
@@ -87,7 +85,7 @@ def refresh_strava_tokens_job():
def retrieve_strava_user_activities_for_last_day():
# Get last day users Strava activities
strava_activity_processor.retrieve_strava_users_activities_for_days(1)
strava_activity_utils.retrieve_strava_users_activities_for_days(1)
# Create loggger
@@ -216,7 +214,15 @@ app.include_router(
tags=["followers"],
dependencies=[Depends(session_security.validate_access_token)],
)
app.include_router(router_strava.router, tags=["strava"])
app.include_router(
strava_router.router,
prefix="/strava",
tags=["strava"],
dependencies=[
Depends(session_security.validate_access_token),
Security(session_security.check_scopes, scopes=["profile"]),
],
)
# Check if Jaeger tracing is enabled using the 'JAEGER_ENABLED' environment variable
if os.environ.get("JAEGER_ENABLED") == "true":

View File

@@ -1,6 +1,6 @@
import logging
from typing import Annotated, Callable
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
from sqlalchemy.orm import Session
@@ -69,7 +69,7 @@ async def read_users_me(
async def upload_profile_image(
file: UploadFile,
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -84,7 +84,7 @@ async def upload_profile_image(
async def edit_user(
user_attributtes: users_schema.User,
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -103,7 +103,7 @@ async def edit_user(
async def edit_profile_password(
user_attributtes: users_schema.UserEditPassword,
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[
@@ -121,7 +121,7 @@ async def edit_profile_password(
@router.put("/delete-photo")
async def delete_profile_photo(
token_user_id: Annotated[
Callable,
int,
Depends(session_security.get_user_id_from_access_token),
],
db: Annotated[

View File

@@ -18,8 +18,9 @@ import users.crud as users_crud
import gears.crud as gears_crud
import strava.utils as strava_utils
from database import SessionLocal
from processors import strava_processor
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@@ -291,7 +292,7 @@ def process_activity(
db: Session,
):
# Get the activity by Strava ID from the user
activity_db = strava_processor.fetch_and_validate_activity(activity.id, user_id, db)
activity_db = strava_utils.fetch_and_validate_activity(activity.id, user_id, db)
# Check if activity is None and return None if it is
if activity_db is not None:
@@ -336,7 +337,7 @@ def get_user_strava_activities_by_days(start_date: datetime, user_id: int):
try:
# Get the user integrations by user ID
user_integrations = strava_processor.fetch_user_integrations_and_validate_token(
user_integrations = strava_utils.fetch_user_integrations_and_validate_token(
user_id, db
)
@@ -348,7 +349,7 @@ def get_user_strava_activities_by_days(start_date: datetime, user_id: int):
logger.info(f"User {user_id}: Started Strava activities processing")
# Create a Strava client with the user's access token
strava_client = strava_processor.create_strava_client(user_integrations)
strava_client = strava_utils.create_strava_client(user_integrations)
# Fetch Strava activities after the specified start date
num_strava_activities_processed = fetch_and_process_activities(

View File

@@ -1,9 +1,10 @@
import logging
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from stravalib.client import Client
import strava.utils as strava_utils
import gears.schema as gears_schema
import gears.crud as gears_crud
@@ -12,8 +13,9 @@ import activities.crud as activities_crud
import user_integrations.crud as user_integrations_crud
import strava.athlete_utils as strava_athlete_utils
from database import SessionLocal
from processors import strava_processor, strava_athlete_processor
# Define a loggger created on main.py
logger = logging.getLogger("myLogger")
@@ -34,7 +36,7 @@ def get_strava_gear(gear_id: str, strava_client: Client):
def fetch_and_process_gear(strava_client: Client, user_id: int, db: Session) -> int:
# Fetch Strava athlete
strava_athlete = strava_athlete_processor.get_strava_athlete(strava_client)
strava_athlete = strava_athlete_utils.get_strava_athlete(strava_client)
# Initialize an empty list for results
strava_gear = []
@@ -88,14 +90,14 @@ def process_gear(
return new_gear
def save_gears(gears: [gears_schema.Gear], user_id: int, db: Session):
def save_gears(gears: list[gears_schema.Gear], user_id: int, db: Session):
# Save the gear to the database
gears_crud.create_multiple_gears(gears, user_id, db)
def iterate_over_activities_and_set_gear(
activity: activities_schema.Activity,
gears: [gears_schema.Gear],
gears: list[gears_schema.Gear],
counter: int,
user_id: int,
db: Session,
@@ -153,7 +155,7 @@ def get_user_gear(user_id: int):
try:
# Get the user integrations by user ID
user_integrations = strava_processor.fetch_user_integrations_and_validate_token(
user_integrations = strava_utils.fetch_user_integrations_and_validate_token(
user_id, db
)
@@ -161,7 +163,7 @@ def get_user_gear(user_id: int):
logger.info(f"User {user_id}: Started Strava gear processing")
# Create a Strava client with the user's access token
strava_client = strava_processor.create_strava_client(user_integrations)
strava_client = strava_utils.create_strava_client(user_integrations)
# Set the user's gear to sync to True
user_integrations_crud.set_user_strava_sync_gear(user_id, True, db)

View File

@@ -2,29 +2,25 @@ import logging
import requests
import os
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from fastapi.security import OAuth2PasswordBearer
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
import session.security as session_security
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_session,
)
import strava.gear_utils as strava_gear_utils
import strava.activity_utils as strava_activity_utils
import database
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Define the API router
router = APIRouter()
@@ -33,7 +29,7 @@ logger = logging.getLogger("myLogger")
@router.get(
"/strava/link",
"/link",
tags=["strava"],
)
async def strava_link(
@@ -101,7 +97,7 @@ async def strava_link(
@router.get(
"/strava/activities/days/{days}",
"/activities/days/{days}",
status_code=202,
tags=["strava"],
)
@@ -109,17 +105,15 @@ async def strava_retrieve_activities_days(
days: int,
token_user_id: Annotated[
int,
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)],
background_tasks: BackgroundTasks,
):
# Process strava activities in the background
background_tasks.add_task(
strava_activity_processor.get_user_strava_activities_by_days,
(datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S"),
strava_activity_utils.get_user_strava_activities_by_days,
(datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S"),
token_user_id,
)
@@ -133,53 +127,47 @@ async def strava_retrieve_activities_days(
@router.put(
"/strava/set-user-unique-state/{state}",
"/set-user-unique-state/{state}",
tags=["strava"],
)
async def strava_set_user_unique_state(
state: str,
user_id: Annotated[
token_user_id: Annotated[
int,
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)],
):
# Set the user Strava state
user_integrations_crud.set_user_strava_state(user_id, state, db)
user_integrations_crud.set_user_strava_state(token_user_id, state, db)
# Return success message
return {"detail": f"Strava state for user {user_id} edited successfully"}
return {f"Strava state for user {token_user_id} edited successfully"}
@router.put(
"/strava/unset-user-unique-state",
"/unset-user-unique-state",
tags=["strava"],
)
async def strava_unset_user_unique_state(
user_id: Annotated[
token_user_id: Annotated[
int,
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)],
):
# Set the user Strava state
user_integrations_crud.set_user_strava_state(user_id, None, db)
user_integrations_crud.set_user_strava_state(token_user_id, None, db)
# Return success message
return {"detail": f"Strava state for user {user_id} removed successfully"}
return {f"Strava state for user {token_user_id} removed successfully"}
@router.delete("/strava/unlink", tags=["strava"])
@router.delete("/unlink", tags=["strava"])
async def strava_unlink(
token_user_id: Annotated[
int,
Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
Depends(session_security.get_user_id_from_access_token),
],
db: Session = Depends(database.get_db),
):
@@ -196,19 +184,17 @@ async def strava_unlink(
return {"detail": f"Strava unlinked for user {token_user_id} successfully"}
@router.get("/strava/gear", status_code=202, tags=["strava"])
@router.get("/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
),
Depends(session_security.get_user_id_from_access_token),
],
background_tasks: BackgroundTasks,
):
# Process strava activities in the background
background_tasks.add_task(
strava_gear_processor.get_user_gear,
strava_gear_utils.get_user_gear,
token_user_id,
)

View File

@@ -2,7 +2,7 @@ import logging
import os
import requests
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from stravalib.client import Client
@@ -36,7 +36,7 @@ def refresh_strava_tokens(db: Session):
minutes=60
)
if datetime.utcnow() > refresh_time:
if datetime.now(timezone.utc) > refresh_time:
# Strava token refresh endpoint
token_url = "https://www.strava.com/oauth/token"
# Parameters for the token refresh request

View File

@@ -301,6 +301,13 @@ def edit_user(user_id: int, user: users_schema.User, db: Session):
# Get the user from the database
db_user = db.query(models.User).filter(models.User.id == user_id).first()
if db_user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
headers={"WWW-Authenticate": "Bearer"},
)
# Update the user
if user.name is not None:
db_user.name = user.name