diff --git a/Dockerfile_backend b/Dockerfile_backend
index 0376960c4..162f2a603 100644
--- a/Dockerfile_backend
+++ b/Dockerfile_backend
@@ -32,6 +32,7 @@ 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"
diff --git a/README.md b/README.md
index f51d3347e..30fd561ef 100644
--- a/README.md
+++ b/README.md
@@ -88,8 +88,9 @@ DB_USER | endurain | Yes | N/A
DB_PASSWORD | changeme | `No` | N/A
DB_DATABASE | endurain | Yes | N/A
SECRET_KEY | changeme | `No` | N/A
-ALGORITHM | HS256 | Yes | N/A
-ACCESS_TOKEN_EXPIRE_MINUTES | 30 | Yes | N/A
+ALGORITHM | HS256 | Yes | Currently only HS256 is supported
+ACCESS_TOKEN_EXPIRE_MINUTES | 15 | Yes | N/A
+REFRESH_TOKEN_EXPIRE_DAYS | 7 | Yes | N/A
STRAVA_CLIENT_ID | changeme | `No` | N/A
STRAVA_CLIENT_SECRET | changeme | `No` | N/A
STRAVA_AUTH_CODE | changeme | `No` | N/A
diff --git a/backend/alembic/versions/ab815ee3beae_remove_photo_path_aux_column_from_users_.py b/backend/alembic/versions/ab815ee3beae_remove_photo_path_aux_column_from_users_.py
new file mode 100644
index 000000000..741cef3e1
--- /dev/null
+++ b/backend/alembic/versions/ab815ee3beae_remove_photo_path_aux_column_from_users_.py
@@ -0,0 +1,55 @@
+"""Remove photo_path_aux column from users table
+
+Revision ID: ab815ee3beae
+Revises: 0ab200a7f196
+Create Date: 2024-06-07 21:40:28.789274
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import mysql
+
+# revision identifiers, used by Alembic.
+revision: str = 'ab815ee3beae'
+down_revision: Union[str, None] = '0ab200a7f196'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('users', 'photo_path_aux')
+
+ # Update admin password
+ new_password = '$2b$12$.b.fWl/Bu/sIx/6ZtIgJPuXaqfqkIi8NwnxeP6SQiQPZB3kKxD5tm' # replace this with the actual new password
+ op.execute(
+ sa.text(
+ """
+ UPDATE users
+ SET password = :new_password
+ WHERE username = 'admin'
+ """
+ ).bindparams(new_password=new_password)
+ )
+
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('users', sa.Column('photo_path_aux', mysql.VARCHAR(length=250), nullable=True, comment='Auxiliary photo path'))
+
+ old_password = '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918' # replace this with the actual old password
+ op.execute(
+ sa.text(
+ """
+ UPDATE users
+ SET password = :old_password
+ WHERE username = 'admin'
+ """
+ ).bindparams(old_password=old_password)
+ )
+
+ # ### end Alembic commands ###
diff --git a/backend/constants.py b/backend/constants.py
index 68e75d624..3617be5fb 100644
--- a/backend/constants.py
+++ b/backend/constants.py
@@ -5,7 +5,8 @@ API_VERSION = "v0.2.1"
# JWT Token constants
JWT_ALGORITHM = os.environ.get("ALGORITHM")
-JWT_EXPIRATION_IN_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES"))
+JWT_ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES"))
+JWT_REFRESH_TOKEN_EXPIRE_DAYS = int(os.environ.get("REFRESH_TOKEN_EXPIRE_DAYS"))
JWT_SECRET_KEY = os.environ.get("SECRET_KEY")
# Scopes definition
diff --git a/backend/crud/crud_users.py b/backend/crud/crud_users.py
index b0531be87..8c9d31cef 100644
--- a/backend/crud/crud_users.py
+++ b/backend/crud/crud_users.py
@@ -34,13 +34,13 @@ def format_user_birthdate(user):
return user
-def authenticate_user(username: str, password: str, db: Session):
+def authenticate_user(username: str, db: Session):
try:
# Get the user from the database
user = (
db.query(models.User)
.filter(
- models.User.username == username, models.User.password == password
+ models.User.username == username
)
.first()
)
diff --git a/backend/dependencies/dependencies_security.py b/backend/dependencies/dependencies_security.py
new file mode 100644
index 000000000..85751e5ab
--- /dev/null
+++ b/backend/dependencies/dependencies_security.py
@@ -0,0 +1,136 @@
+import logging
+
+from typing import Annotated
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+
+# import the jwt module from the joserfc package
+from joserfc import jwt
+from joserfc.jwk import OctKey
+
+from constants import (
+ JWT_ALGORITHM,
+ JWT_SECRET_KEY,
+ ADMIN_ACCESS,
+)
+
+
+# Define the OAuth2 scheme for handling bearer tokens
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+
+# Define a loggger created on main.py
+logger = logging.getLogger("myLogger")
+
+
+def decode_token(token: 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)
+
+ 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 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["id"]
+ 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_access_type(token: Annotated[str, Depends(oauth2_scheme)]):
+ # Decode the token
+ payload = decode_token(token)
+
+ try:
+ # Get the user access_type from the payload and return it
+ return payload.claims["access_type"]
+ except Exception:
+ # Log the error and raise the exception
+ logger.info(
+ "Claim with user access Type 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 access Type not present in token",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+
+def validate_token_admin_access(token: Annotated[str, Depends(oauth2_scheme)]):
+ if get_token_access_type(token) != ADMIN_ACCESS:
+ # Raise an HTTPException with a 403 Forbidden status code
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Unauthorized Access - Admin Access Required",
+ )
+
+
+def create_token(data: dict):
+ # Encode the data and return the token
+ return jwt.encode({"alg": JWT_ALGORITHM}, data.copy(), JWT_SECRET_KEY)
\ No newline at end of file
diff --git a/backend/dependencies/dependencies_session.py b/backend/dependencies/dependencies_session.py
index 9de4ade19..f1d2ec203 100644
--- a/backend/dependencies/dependencies_session.py
+++ b/backend/dependencies/dependencies_session.py
@@ -1,36 +1,99 @@
-from fastapi import Depends, HTTPException, status
-from fastapi.security import OAuth2PasswordBearer
+from fastapi import Depends, HTTPException, status, Request, Security
+from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from sqlalchemy.orm import Session
-from dependencies import dependencies_database
-from schemas import schema_access_tokens, schema_users
+from typing import Annotated
+
+from dependencies import dependencies_database, dependencies_security
+from schemas import schema_users
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
-def validate_token(token: str = Depends(oauth2_scheme), db: Session = Depends(dependencies_database.get_db)):
+
+### 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
- schema_access_tokens.validate_token_expiration(db, token)
+ dependencies_security.validate_token_expiration(access_token)
-def validate_token_and_get_authenticated_user_id(
- token: str = Depends(oauth2_scheme), db: Session = Depends(dependencies_database.get_db)
+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)],
):
- # Validate the token expiration
- schema_access_tokens.validate_token_expiration(db, token)
-
# Return the user ID associated with the token
- return schema_access_tokens.get_token_user_id(token)
+ 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(dependencies_database.get_db)
+ token: str = Depends(oauth2_scheme),
+ db: Session = Depends(dependencies_database.get_db),
):
# Validate the token expiration
- schema_access_tokens.validate_token_expiration(db, token)
+ dependencies_security.validate_token_expiration(token)
# Check if the token has admin access
- schema_access_tokens.validate_token_admin_access(token)
+ dependencies_security.validate_token_admin_access(token)
def validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access(
@@ -39,7 +102,7 @@ def validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_acc
db: Session = Depends(dependencies_database.get_db),
):
# Validate the token expiration
- schema_access_tokens.validate_token_expiration(db, token)
+ dependencies_security.validate_token_expiration(token)
# Check if user_id higher than 0
if not (int(user_id) > 0):
@@ -49,9 +112,9 @@ def validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_acc
)
# Check if token id is different from user id. If yes, check if the token has admin access
- if user_id != schema_access_tokens.get_token_user_id(token):
+ if user_id != dependencies_security.get_token_user_id(token):
# Check if the token has admin access
- schema_access_tokens.validate_token_admin_access(token)
+ dependencies_security.validate_token_admin_access(token)
def validate_token_and_if_user_id_equals_token_user_attributtes_id_if_not_validate_admin_access(
@@ -72,7 +135,7 @@ def validate_token_and_if_user_id_equals_token_user_attributtes_password_id_if_n
def validate_token_user_id_admin_access(db, token, user_id):
# Validate the token expiration
- schema_access_tokens.validate_token_expiration(db, token)
+ dependencies_security.validate_token_expiration(token)
# Check if user_id higher than 0
if not (int(user_id) > 0):
@@ -82,6 +145,26 @@ def validate_token_user_id_admin_access(db, token, user_id):
)
# Check if token id is different from user id. If yes, check if the token has admin access
- if user_id != schema_access_tokens.get_token_user_id(token):
+ if user_id != dependencies_security.get_token_user_id(token):
# Check if the token has admin access
- schema_access_tokens.validate_token_admin_access(token)
\ No newline at end of file
+ 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}"'
+ },
+ )
diff --git a/backend/main.py b/backend/main.py
index b00b8ad8c..6b2ccf657 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -27,7 +27,6 @@ from routers import (
router_strava,
)
from constants import API_VERSION
-from schemas import schema_access_tokens
from database import SessionLocal
from processors import strava_processor, strava_activity_processor
@@ -102,6 +101,7 @@ required_env_vars = [
"SECRET_KEY",
"ALGORITHM",
"ACCESS_TOKEN_EXPIRE_MINUTES",
+ "REFRESH_TOKEN_EXPIRE_DAYS",
"STRAVA_CLIENT_ID",
"STRAVA_CLIENT_SECRET",
"STRAVA_AUTH_CODE",
diff --git a/backend/models.py b/backend/models.py
index 5e17c461e..d783c246a 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -90,9 +90,6 @@ class User(Base):
Integer, nullable=False, comment="User type (one digit)(1 - user, 2 - admin)"
)
photo_path = Column(String(length=250), nullable=True, comment="User photo path")
- photo_path_aux = Column(
- String(length=250), nullable=True, comment="Auxiliary photo path"
- )
is_active = Column(
Integer, nullable=False, comment="Is user active (1 - active, 2 - not active)"
)
diff --git a/backend/routers/router_activities.py b/backend/routers/router_activities.py
index c2cf0ef52..dc2772533 100644
--- a/backend/routers/router_activities.py
+++ b/backend/routers/router_activities.py
@@ -18,6 +18,7 @@ from dependencies import (
dependencies_activities,
dependencies_gear,
dependencies_global,
+ dependencies_security,
)
from processors import gpx_processor, fit_processor
@@ -45,7 +46,9 @@ async def read_activities_useractivities_week(
],
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(
+ dependencies_session.validate_access_token_and_get_authenticated_user_id
+ ),
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -84,7 +87,9 @@ async def read_activities_useractivities_thisweek_distances(
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(
+ dependencies_session.validate_access_token_and_get_authenticated_user_id
+ ),
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -125,7 +130,7 @@ async def read_activities_useractivities_thismonth_distances(
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -165,7 +170,7 @@ async def read_activities_useractivities_thismonth_number(
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -214,7 +219,9 @@ async def read_activities_gearactivities(
db: Session = Depends(dependencies_database.get_db),
):
# Get the activities for the gear
- return crud_activities.get_user_activities_by_gear_id_and_user_id(user_id, gear_id, db)
+ return crud_activities.get_user_activities_by_gear_id_and_user_id(
+ user_id, gear_id, db
+ )
@router.get(
@@ -225,7 +232,7 @@ async def read_activities_gearactivities(
async def read_activities_useractivities_number(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
- validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)],
+ validate_token: Annotated[Callable, Depends(dependencies_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
):
# Get the number of activities for the user
@@ -252,7 +259,7 @@ async def read_activities_useractivities_pagination(
validate_pagination_values: Annotated[
Callable, Depends(dependencies_global.validate_pagination_values)
],
- validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)],
+ validate_token: Annotated[Callable, Depends(dependencies_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
):
# Get the activities for the user with pagination
@@ -281,7 +288,7 @@ async def read_activities_followed_user_activities_pagination(
validate_pagination_values: Annotated[
Callable, Depends(dependencies_global.validate_pagination_values)
],
- validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)],
+ validate_token: Annotated[Callable, Depends(dependencies_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
):
# Get the activities for the following users with pagination
@@ -298,7 +305,7 @@ async def read_activities_followed_user_activities_pagination(
async def read_activities_followed_useractivities_number(
user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
- validate_token: Annotated[Callable, Depends(dependencies_session.validate_token)],
+ validate_token: Annotated[Callable, Depends(dependencies_security.validate_token_expiration)],
db: Session = Depends(dependencies_database.get_db),
):
# Get the number of activities for the following users
@@ -324,12 +331,14 @@ async def read_activities_activity_from_id(
],
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
],
db: Session = Depends(dependencies_database.get_db),
):
# Get the activity from the database and return it
- return crud_activities.get_activity_by_id_from_user_id_or_has_visibility(activity_id, token_user_id, db)
+ return crud_activities.get_activity_by_id_from_user_id_or_has_visibility(
+ activity_id, token_user_id, db
+ )
@router.get(
@@ -341,7 +350,7 @@ async def read_activities_contain_name(
name: str,
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -358,7 +367,7 @@ async def read_activities_contain_name(
async def create_activity_with_uploaded_file(
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
],
file: UploadFile,
db: Session = Depends(dependencies_database.get_db),
@@ -425,7 +434,8 @@ async def create_activity_with_uploaded_file(
) from err
-@router.put("/activities/{activity_id}/addgear/{gear_id}",
+@router.put(
+ "/activities/{activity_id}/addgear/{gear_id}",
tags=["activities"],
)
async def activity_add_gear(
@@ -435,7 +445,9 @@ async def activity_add_gear(
],
gear_id: int,
validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
- token_user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)],
+ 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 user id and gear id
@@ -447,9 +459,11 @@ async def activity_add_gear(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Gear ID {gear_id} for user {token_user_id} not found",
)
-
- # Get the activity by id from user id
- activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db)
+
+ # Get the activity by id from user id
+ activity = crud_activities.get_activity_by_id_from_user_id(
+ activity_id, token_user_id, db
+ )
# Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is
if activity is None:
@@ -460,11 +474,13 @@ async def activity_add_gear(
# Add the gear to the activity
crud_activities.add_gear_to_activity(activity_id, gear_id, db)
-
+
# Return success message
return {"detail": f"Gear ID {gear_id} added to activity successfully"}
-@router.put("/activities/{activity_id}/deletegear",
+
+@router.put(
+ "/activities/{activity_id}/deletegear",
tags=["activities"],
)
async def delete_activity_gear(
@@ -472,11 +488,15 @@ async def delete_activity_gear(
validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id)
],
- token_user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)],
+ 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 activity by id from user id
- activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db)
+ # Get the activity by id from user id
+ activity = crud_activities.get_activity_by_id_from_user_id(
+ activity_id, token_user_id, db
+ )
# Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is
if activity is None:
@@ -487,11 +507,13 @@ async def delete_activity_gear(
# Delete gear from the activity
crud_activities.add_gear_to_activity(activity_id, None, db)
-
+
# Return success message
return {"detail": f"Gear ID {activity.gear_id} deleted from activity successfully"}
-@router.delete("/activities/{activity_id}/delete",
+
+@router.delete(
+ "/activities/{activity_id}/delete",
tags=["activities"],
)
async def delete_activity(
@@ -499,11 +521,15 @@ async def delete_activity(
validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id)
],
- token_user_id: Annotated[int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)],
+ 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 activity by id from user id
- activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db)
+ # Get the activity by id from user id
+ activity = crud_activities.get_activity_by_id_from_user_id(
+ activity_id, token_user_id, db
+ )
# Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is
if activity is None:
@@ -511,10 +537,9 @@ async def delete_activity(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Activity ID {activity_id} for user {token_user_id} not found",
)
-
+
# Delete the activity
crud_activities.delete_activity(activity_id, db)
-
+
# Return success message
return {"detail": f"Activity {activity_id} deleted successfully"}
-
\ No newline at end of file
diff --git a/backend/routers/router_activity_streams.py b/backend/routers/router_activity_streams.py
index b988c03af..3152b46a1 100644
--- a/backend/routers/router_activity_streams.py
+++ b/backend/routers/router_activity_streams.py
@@ -10,7 +10,7 @@ from schemas import schema_activity_streams
from crud import crud_activity_streams
from dependencies import (
dependencies_database,
- dependencies_session,
+ dependencies_security,
dependencies_activities,
dependencies_activity_streams,
)
@@ -36,7 +36,7 @@ async def read_activities_streams_for_activity_all(
Callable, Depends(dependencies_activities.validate_activity_id)
],
validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token)
+ Callable, Depends(dependencies_security.validate_token_expiration)
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -59,7 +59,7 @@ async def read_activities_streams_for_activity_stream_type(
Callable, Depends(dependencies_activity_streams.validate_activity_stream_type)
],
validate_token: Annotated[
- Callable, Depends(dependencies_session.validate_token)
+ Callable, Depends(dependencies_security.validate_token_expiration)
],
db: Session = Depends(dependencies_database.get_db),
):
diff --git a/backend/routers/router_followers.py b/backend/routers/router_followers.py
index d5ccf5c1f..3df4c361d 100644
--- a/backend/routers/router_followers.py
+++ b/backend/routers/router_followers.py
@@ -10,7 +10,7 @@ from schemas import schema_followers
from crud import crud_followers
from dependencies import (
dependencies_database,
- dependencies_session,
+ dependencies_security,
dependencies_users,
)
@@ -32,7 +32,7 @@ logger = logging.getLogger("myLogger")
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_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followers
@@ -47,7 +47,7 @@ async def get_user_follower_all(
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_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followers
@@ -69,7 +69,7 @@ async def get_user_follower_count_all(
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_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followers
@@ -91,7 +91,7 @@ async def get_user_follower_count(
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_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followings
@@ -106,7 +106,7 @@ async def get_user_following_all(
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_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followings
@@ -128,7 +128,7 @@ async def get_user_following_count_all(
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_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return followings
@@ -154,7 +154,7 @@ async def read_followers_user_specific_user(
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
- validate_token: Callable = Depends(dependencies_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Return the follower
@@ -175,7 +175,7 @@ async def create_follow(
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
- validate_token: Callable = Depends(dependencies_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Create the follower
@@ -195,7 +195,7 @@ async def accept_follow(
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
- validate_token: Callable = Depends(dependencies_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Accept the follower
@@ -216,7 +216,7 @@ async def delete_follow(
validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id)
],
- validate_token: Callable = Depends(dependencies_session.validate_token),
+ validate_token: Callable = Depends(dependencies_security.validate_token_expiration),
db: Session = Depends(dependencies_database.get_db),
):
# Delete the follower
diff --git a/backend/routers/router_gear.py b/backend/routers/router_gear.py
index db6665e90..57b893d32 100644
--- a/backend/routers/router_gear.py
+++ b/backend/routers/router_gear.py
@@ -34,7 +34,7 @@ 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_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)],
):
@@ -54,7 +54,7 @@ async def read_gear_user_pagination(
Callable, Depends(dependencies_global.validate_pagination_values)
],
user_id: Annotated[
- int, Depends(dependencies_session.validate_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),
):
@@ -71,7 +71,7 @@ async def read_gear_user_pagination(
)
async def read_gear_user_number(
user_id: Annotated[
- int, Depends(dependencies_session.validate_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),
):
@@ -94,7 +94,7 @@ async def read_gear_user_number(
async def read_gear_user_by_nickname(
nickname: str,
user_id: Annotated[
- int, Depends(dependencies_session.validate_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),
):
@@ -111,7 +111,7 @@ async def read_gear_user_by_type(
gear_type: int,
validate_type: Annotated[Callable, Depends(dependencies_gear.validate_gear_type)],
user_id: Annotated[
- int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)
+ int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
],
db: Session = Depends(dependencies_database.get_db),
):
@@ -127,7 +127,7 @@ async def read_gear_user_by_type(
async def create_gear(
gear: schema_gear.Gear,
user_id: Annotated[
- int, Depends(dependencies_session.validate_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),
):
@@ -144,7 +144,7 @@ async def edit_gear(
validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
gear: schema_gear.Gear,
token_user_id: Annotated[
- int, Depends(dependencies_session.validate_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),
):
@@ -170,7 +170,7 @@ async def delete_user(
gear_id: int,
validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
token_user_id: Annotated[
- int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)
+ int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
],
db: Session = Depends(dependencies_database.get_db),
):
diff --git a/backend/routers/router_session.py b/backend/routers/router_session.py
index e10381e90..dce2c6aec 100644
--- a/backend/routers/router_session.py
+++ b/backend/routers/router_session.py
@@ -1,26 +1,36 @@
import logging
+import bcrypt
-from datetime import datetime, timedelta
-from typing import Annotated, Callable
+from datetime import datetime, timedelta, timezone
+from typing import Annotated
-from fastapi import APIRouter, Depends, HTTPException, status, Security
-from fastapi.security import (
- OAuth2PasswordBearer,
- OAuth2PasswordRequestForm,
- SecurityScopes,
+from fastapi import (
+ APIRouter,
+ Depends,
+ HTTPException,
+ status,
+ Response,
+ Request,
)
+from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
-from crud import crud_user_integrations, crud_users
-from schemas import schema_access_tokens, schema_users
+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,
)
-from dependencies import dependencies_database, dependencies_session
# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(
@@ -35,60 +45,108 @@ router = APIRouter()
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, password, db)
+ user = crud_users.authenticate_user(username, db)
- # Check if the user exists and if the password is correct and if not return False
+ # 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 or password",
+ detail="Incorrect username",
headers={"WWW-Authenticate": "Bearer"},
)
- # Return the user if the password is correct
+ 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 get_current_user(db: Session, user_id: int):
- # Get the user from the database
- user = crud_users.get_user_by_id(user_id, db)
+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
- # If the user does not exist raise the exception
- if user is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials (user not found)",
- headers={"WWW-Authenticate": "Bearer"},
- )
-
- user_integrations = crud_user_integrations.get_user_integrations_by_user_id(
- user.id, db
+ # 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),
+ },
)
- if user_integrations is None:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials (user integrations not found)",
- headers={"WWW-Authenticate": "Bearer"},
- )
+ 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),
+ },
+ )
- if user_integrations.strava_token is None:
- user.is_strava_linked = 0
- else:
- user.is_strava_linked = 1
+ # 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",
+ )
- # Return the user
- return user
+ # 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", response_model=schema_access_tokens.AccessToken, tags=["session"]
-)
+@router.post("/token", tags=["session"])
async def login_for_access_token(
+ response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
- do_not_expire: bool = False,
db: Session = Depends(dependencies_database.get_db),
):
user = authenticate_user(form_data.username, form_data.password, db)
@@ -100,49 +158,33 @@ async def login_for_access_token(
headers={"WWW-Authenticate": "Bearer"},
)
- expire = None
- if do_not_expire:
- expire = datetime.utcnow() + timedelta(days=90)
+ response = create_response_with_tokens(response, user)
- if user.access_type == REGULAR_ACCESS:
- scopes = REGULAR_ACCESS_SCOPES
- else:
- scopes = ADMIN_ACCESS_SCOPES
-
- access_token = schema_access_tokens.create_access_token(
- db,
- data={
- "sub": user.username,
- "scopes": scopes,
- "id": user.id,
- "access_type": user.access_type,
- },
- expires_delta=expire,
- )
-
- return schema_access_tokens.AccessToken(
- access_token=access_token, token_type="bearer"
- )
+ return {"message": "Login successful"}
-@router.get("/users/me", response_model=schema_users.UserMe, tags=["session"])
-async def read_users_me(
- security_scopes: SecurityScopes,
+@router.post("/refresh", tags=["session"])
+async def refresh_token(
+ response: Response,
+ request: Request,
user_id: Annotated[
- int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id)
- ],
- check_scopes: Annotated[
- Callable, Security(schema_access_tokens.check_scopes, scopes=["users:read"])
+ int,
+ Depends(
+ dependencies_session.validate_refresh_token_and_get_authenticated_user_id
+ ),
],
db: Session = Depends(dependencies_database.get_db),
):
- return get_current_user(db, user_id)
+ # 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"},
+ )
-@router.get("/validate_token", tags=["session"])
-async def validate_token(
- validate_token: Callable = Depends(dependencies_session.validate_token),
- db: Session = Depends(dependencies_database.get_db),
-):
- # Return None if the token is valid
- return None
+ response = create_response_with_tokens(response, user)
+
+ return {"message": "Token refreshed successfully"}
diff --git a/backend/routers/router_strava.py b/backend/routers/router_strava.py
index 27577b220..77b1770fa 100644
--- a/backend/routers/router_strava.py
+++ b/backend/routers/router_strava.py
@@ -78,9 +78,7 @@ async def strava_link(
# Redirect to the main page or any other desired page after processing
redirect_url = (
- "https://"
- + os.environ.get("FRONTEND_HOST")
- + "/settings?stravaLinked=1"
+ "https://" + os.environ.get("FRONTEND_HOST") + "/settings?stravaLinked=1"
)
# Return a RedirectResponse to the redirect URL
@@ -104,7 +102,10 @@ async def strava_link(
async def strava_retrieve_activities_days(
days: int,
token_user_id: Annotated[
- int, Depends(dependencies_session.validate_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)],
background_tasks: BackgroundTasks,
@@ -132,7 +133,10 @@ async def strava_retrieve_activities_days(
async def strava_set_user_unique_state(
state: str,
user_id: Annotated[
- int, Depends(dependencies_session.validate_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)],
):
@@ -149,7 +153,7 @@ async def strava_set_user_unique_state(
)
async def strava_unset_user_unique_state(
user_id: Annotated[
- int, Depends(dependencies_session.validate_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)],
):
@@ -163,7 +167,7 @@ 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_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),
):
@@ -183,7 +187,7 @@ 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_token_and_get_authenticated_user_id)
+ int, Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id)
],
background_tasks: BackgroundTasks,
):
diff --git a/backend/routers/router_users.py b/backend/routers/router_users.py
index d6c43e633..ea8c16282 100644
--- a/backend/routers/router_users.py
+++ b/backend/routers/router_users.py
@@ -3,7 +3,7 @@ import logging
from typing import Annotated, Callable
-from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
+from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, Security
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
@@ -16,6 +16,7 @@ from dependencies import (
dependencies_session,
dependencies_global,
dependencies_users,
+ dependencies_security,
)
# Define the OAuth2 scheme for handling bearer tokens
@@ -28,12 +29,65 @@ router = APIRouter()
logger = logging.getLogger("myLogger")
+@router.get("/users/me", response_model=schema_users.UserMe, tags=["users"])
+async def read_users_me(
+ 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"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
+):
+ # Get the user from the database
+ user = crud_users.get_user_by_id(token_user_id, db)
+
+ # If the user does not exist raise the exception
+ if user is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials (user not found)",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ user_integrations = crud_user_integrations.get_user_integrations_by_user_id(
+ user.id, db
+ )
+
+ if user_integrations is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials (user integrations not found)",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ if user_integrations.strava_token is None:
+ user.is_strava_linked = 0
+ else:
+ user.is_strava_linked = 1
+
+ # Return the user
+ return user
+
+
@router.get("/users/number", response_model=int, tags=["users"])
async def read_users_number(
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
return crud_users.get_users_number(db)
@@ -49,10 +103,16 @@ async def read_users_all_pagination(
validate_pagination_values: Annotated[
Callable, Depends(dependencies_global.validate_pagination_values)
],
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
# Get the users from the database with pagination
return crud_users.get_users_with_pagination(
@@ -67,10 +127,16 @@ async def read_users_all_pagination(
)
async def read_users_contain_username(
username: str,
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
# Get the users from the database by username
return crud_users.get_user_if_contains_username(username=username, db=db)
@@ -83,10 +149,16 @@ async def read_users_contain_username(
)
async def read_users_username(
username: str,
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
# Get the user from the database by username
return crud_users.get_user_by_username(username=username, db=db)
@@ -96,8 +168,16 @@ async def read_users_username(
async def read_users_id(
user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
- validate_token: Callable = Depends(dependencies_session.validate_token),
- db: Session = Depends(dependencies_database.get_db),
+ validate_access_token: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
):
# Get the users from the database by id
return crud_users.get_user_by_id(user_id=user_id, db=db)
@@ -106,10 +186,16 @@ async def read_users_id(
@router.get("/users/{username}/id", response_model=int, tags=["users"])
async def read_users_username_id(
username: str,
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
# Get the users from the database by username
return crud_users.get_user_id_by_username(username, db)
@@ -119,37 +205,34 @@ async def read_users_username_id(
async def read_users_id_photo_path(
user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:read"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
# Get the photo_path from the database by id
return crud_users.get_user_photo_path_by_id(user_id, db)
-@router.get(
- "/users/{user_id}/photo_path_aux", response_model=str | None, tags=["users"]
-)
-async def read_users_id_photo_path_aux(
- user_id: int,
- validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
- ],
- db: Session = Depends(dependencies_database.get_db),
-):
- # Get the photo_path_aux from the database by id
- return crud_users.get_user_photo_path_aux_by_id(user_id, db)
-
-
@router.post("/users/create", response_model=int, status_code=201, tags=["users"])
async def create_user(
user: schema_users.UserCreate,
- validate_token_validate_admin_access: Annotated[
- Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
+ validate_access_token_and_validate_admin_access: Annotated[
+ Callable, Depends(dependencies_session.validate_access_token_and_validate_admin_access)
+ ],
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:write"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
],
- db: Session = Depends(dependencies_database.get_db),
):
# Create the user in the database
created_user = crud_users.create_user(user, db)
@@ -171,15 +254,23 @@ async def upload_user_image(
user_id: int,
token_user_id: Annotated[
Callable,
- Depends(dependencies_session.validate_token_and_get_authenticated_user_id),
+ Depends(
+ dependencies_session.validate_access_token_and_get_authenticated_user_id
+ ),
],
file: UploadFile,
- db: Session = Depends(dependencies_database.get_db),
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
):
try:
upload_dir = "user_images"
os.makedirs(upload_dir, exist_ok=True)
-
+
# Get file extension
_, file_extension = os.path.splitext(file.filename)
filename = f"{user_id}{file_extension}"
@@ -188,13 +279,11 @@ 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)
except Exception as err:
# Log the exception
- logger.error(
- f"Error in upload_user_image: {err}", exc_info=True
- )
+ logger.error(f"Error in upload_user_image: {err}", exc_info=True)
# Remove the file after processing
if os.path.exists(file_path_to_save):
@@ -216,7 +305,13 @@ async def edit_user(
dependencies_session.validate_token_and_if_user_id_equals_token_user_attributtes_id_if_not_validate_admin_access
),
],
- db: Session = Depends(dependencies_database.get_db),
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
):
# Update the user in the database
crud_users.edit_user(user_attributtes, db)
@@ -234,7 +329,13 @@ async def edit_user_password(
dependencies_session.validate_token_and_if_user_id_equals_token_user_attributtes_password_id_if_not_validate_admin_access
),
],
- db: Session = Depends(dependencies_database.get_db),
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
):
# Update the user password in the database
crud_users.edit_user_password(user_attributtes.id, user_attributtes.password, db)
@@ -252,7 +353,13 @@ async def delete_user_photo(
dependencies_session.validate_token_and_if_user_id_equals_token_user_id_if_not_validate_admin_access
),
],
- db: Session = Depends(dependencies_database.get_db),
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:edit"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
):
# Update the user photo_path in the database
crud_users.delete_user_photo(user_id, db)
@@ -268,7 +375,13 @@ async def delete_user(
validate_token_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_token_and_validate_admin_access)
],
- db: Session = Depends(dependencies_database.get_db),
+ check_scopes: Annotated[
+ Callable, Security(dependencies_session.check_scopes, scopes=["users:write"])
+ ],
+ db: Annotated[
+ Session,
+ Depends(dependencies_database.get_db),
+ ],
):
# Delete the user in the database
crud_users.delete_user(user_id, db)
diff --git a/backend/schemas/schema_access_tokens.py b/backend/schemas/schema_access_tokens.py
deleted file mode 100644
index 5cab10052..000000000
--- a/backend/schemas/schema_access_tokens.py
+++ /dev/null
@@ -1,167 +0,0 @@
-import logging
-
-from pydantic import BaseModel
-from datetime import datetime, timedelta, timezone
-from fastapi import Depends, HTTPException, status, Security
-from fastapi.security import OAuth2PasswordBearer, SecurityScopes
-
-# from jose import JWTError, jwt
-from joserfc import jwt
-from joserfc.jwk import OctKey
-from sqlalchemy.orm import Session
-
-from constants import (
- JWT_EXPIRATION_IN_MINUTES,
- JWT_ALGORITHM,
- JWT_SECRET_KEY,
- ADMIN_ACCESS,
-)
-
-
-class AccessToken(BaseModel):
- """Access token schema"""
-
- access_token: str
- token_type: str
-
-
-class Token(BaseModel):
- """Token schema"""
-
- user_id: int | None = None
- expires_at: str | None = None
-
-
-class TokenData(Token):
- """Token data schema"""
-
- access_type: int | None = None
-
-
-class CreateToken(Token):
- """Create token schema"""
-
- token: str
-
-
-# Define the OAuth2 scheme for handling bearer tokens
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
-
-# Define a loggger created on main.py
-logger = logging.getLogger("myLogger")
-
-
-def decode_token(token: str = Depends(oauth2_scheme)):
- # Decode the token and return the payload
- return jwt.decode(token, OctKey.import_key(JWT_SECRET_KEY))
-
-
-def validate_token_expiration(db: Session, token: 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: str = Depends(oauth2_scheme)):
- # Decode the token
- payload = decode_token(token)
-
- # Get the user id from the payload
- user_id = payload.claims["id"]
-
- if user_id is None:
- # If the user id is None raise an HTTPException with a 401 Unauthorized status code
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials",
- headers={"WWW-Authenticate": "Bearer"},
- )
-
- # Return the user id
- return user_id
-
-
-def get_token_access_type(token: str = Depends(oauth2_scheme)):
- # Decode the token
- payload = decode_token(token)
-
- # Get the admin access from the payload
- access_type = payload.claims["access_type"]
-
- if access_type is None:
- # If the access type is None raise an HTTPException with a 401 Unauthorized status code
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials",
- headers={"WWW-Authenticate": "Bearer"},
- )
-
- # Return the access type
- return access_type
-
-
-def validate_token_admin_access(token: str = Depends(oauth2_scheme)):
- if get_token_access_type(token) != ADMIN_ACCESS:
- # Raise an HTTPException with a 403 Forbidden status code
- raise HTTPException(
- status_code=status.HTTP_403_FORBIDDEN,
- detail="Unauthorized Access - Admin Access Required",
- )
-
-
-def check_scopes(security_scopes: SecurityScopes, token: str = Security(oauth2_scheme)):
- # Decode the token
- payload = decode_token(token)
-
- # Get the scopes from the payload
- scopes = payload.claims["scopes"]
-
- if not any(scope in scopes for scope in security_scopes.scopes):
- raise HTTPException(
- status_code=status.HTTP_403_FORBIDDEN,
- detail="Unauthorized Access - Not enough permissions",
- headers={"WWW-Authenticate": f'Bearer scope="{security_scopes.scope_str}"'},
- )
-
-
-def create_access_token(
- db: Session, data: dict, expires_delta: timedelta | None = None
-):
- # Create a copy of the data to encode
- to_encode = data.copy()
-
- # If an expiration time is provided, calculate the expiration time
- if expires_delta:
- expire = datetime.now(timezone.utc) + expires_delta
- else:
- expire = datetime.now(timezone.utc) + timedelta(
- minutes=JWT_EXPIRATION_IN_MINUTES
- )
-
- # Add the expiration time to the data to encode
- to_encode.update({"exp": expire})
-
- # Encode the data and return the token
- # encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
- encoded_jwt = jwt.encode({"alg": JWT_ALGORITHM}, to_encode, JWT_SECRET_KEY)
-
- # Return the token
- return encoded_jwt
diff --git a/backend/schemas/schema_users.py b/backend/schemas/schema_users.py
index 6c10b0cec..fbcabc0c7 100644
--- a/backend/schemas/schema_users.py
+++ b/backend/schemas/schema_users.py
@@ -12,7 +12,6 @@ class User(BaseModel):
gender: int
access_type: int
photo_path: str | None = None
- photo_path_aux: str | None = None
is_active: int
class Config:
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 2767c0b34..af29f2713 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -9,7 +9,7 @@ import UserView from '../views/UserView.vue'
import SettingsView from '../views/SettingsView.vue';
import NotFoundView from '../views/NotFoundView.vue';
-import { auth } from '@/services/auth';
+//import { auth } from '@/services/auth';
//import { useAuthStore } from '@/stores/auth';
@@ -59,7 +59,7 @@ const router = createRouter({
]
})
-router.beforeEach((to, from, next) => {
+/* router.beforeEach((to, from, next) => {
const accessToken = localStorage.getItem('accessToken');
const tokenType = localStorage.getItem('tokenType');
@@ -79,6 +79,6 @@ router.beforeEach((to, from, next) => {
} else {
next();
}
-});
+}); */
export default router;
\ No newline at end of file
diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js
index b1e23319a..f2a2ea08d 100644
--- a/frontend/src/services/auth.js
+++ b/frontend/src/services/auth.js
@@ -1,4 +1,4 @@
-import { fetchGetRequestTokenAsParameter, fetchPostFormUrlEncoded } from '@/utils/serviceUtils';
+import { fetchGetRequest, fetchPostFormUrlEncoded } from '@/utils/serviceUtils';
import { useUserStore } from '@/stores/user';
export const auth = {
@@ -13,9 +13,7 @@ export const auth = {
return exp > currentTime;
},
- storeLoggedUser(token, userMe) {
- localStorage.setItem('accessToken', token.access_token);
- localStorage.setItem('tokenType', token.token_type);
+ storeLoggedUser(userMe) {
localStorage.setItem('userMe', JSON.stringify(userMe));
},
removeLoggedUser() {
@@ -28,6 +26,6 @@ export const auth = {
return fetchPostFormUrlEncoded('token', formData);
},
getUserMe(token) {
- return fetchGetRequestTokenAsParameter('users/me', token);
+ return fetchGetRequest('users/me', token);
},
};
\ No newline at end of file
diff --git a/frontend/src/services/session.js b/frontend/src/services/session.js
new file mode 100644
index 000000000..5116882d1
--- /dev/null
+++ b/frontend/src/services/session.js
@@ -0,0 +1,7 @@
+import { fetchGetRequest, fetchPostFormUrlEncoded } from '@/utils/serviceUtils';
+
+export const session = {
+ getToken(formData) {
+ return fetchPostFormUrlEncoded('token', formData);
+ },
+};
\ No newline at end of file
diff --git a/frontend/src/services/user.js b/frontend/src/services/user.js
index bfef4f20d..88a6a588b 100644
--- a/frontend/src/services/user.js
+++ b/frontend/src/services/user.js
@@ -1,6 +1,9 @@
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest, fetchPostFileRequest } from '@/utils/serviceUtils';
export const users = {
+ getUserMe() {
+ return fetchGetRequest('users/me');
+ },
getUsersWithPagination(pageNumber, numRecords) {
return fetchGetRequest(`users/all/page_number/${pageNumber}/num_records/${numRecords}`);
},
diff --git a/frontend/src/stores/session.js b/frontend/src/stores/session.js
new file mode 100644
index 000000000..14f33c88b
--- /dev/null
+++ b/frontend/src/stores/session.js
@@ -0,0 +1,39 @@
+import { defineStore } from 'pinia';
+
+export const useSessionStore = defineStore('session', {
+ state: () => ({
+ id: null,
+ name: '',
+ username: '',
+ email: '',
+ city: null,
+ birthdate: null,
+ preferred_language: '',
+ gender: null,
+ access_type: null,
+ photo_path: '',
+ photo_path_aux: null,
+ is_active: null,
+ is_strava_linked: null,
+ }),
+ actions: {
+ setUser(user) {
+ this.id = user.id;
+ this.name = user.name;
+ this.username = user.username;
+ this.email = user.email;
+ this.city = user.city;
+ this.birthdate = user.birthdate;
+ this.preferred_language = user.preferred_language;
+ this.gender = user.gender;
+ this.access_type = user.access_type;
+ this.photo_path = user.photo_path;
+ this.photo_path_aux = user.photo_path_aux;
+ this.is_active = user.is_active;
+ this.is_strava_linked = user.is_strava_linked;
+ },
+ resetUser() {
+ this.$reset();
+ }
+ }
+});
\ No newline at end of file
diff --git a/frontend/src/utils/serviceUtils.js b/frontend/src/utils/serviceUtils.js
index 1128ccad2..b6b32f67e 100644
--- a/frontend/src/utils/serviceUtils.js
+++ b/frontend/src/utils/serviceUtils.js
@@ -13,9 +13,9 @@ export async function fetchGetRequest(url) {
// Send the GET request
const response = await fetch(fullUrl, {
method: 'GET',
+ credentials: 'include',
headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
},
});
// If the response status is not ok, throw an error
@@ -96,10 +96,13 @@ export async function fetchPostFileRequest(url, formData) {
export async function fetchPostFormUrlEncoded(url, formData) {
// Create the full URL by combining the API URL with the provided URL
const fullUrl = `${API_URL}${url}`;
+ // Ensure formData is a URLSearchParams object for URL-encoded data
+ const urlEncodedData = new URLSearchParams(formData);
// Send the POST request
const response = await fetch(fullUrl, {
method: 'POST',
- body: formData,
+ body: urlEncodedData,
+ credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
index 725f58a84..f6d06c8ef 100644
--- a/frontend/src/views/LoginView.vue
+++ b/frontend/src/views/LoginView.vue
@@ -21,11 +21,11 @@
-