Started change of login secrets handling

[backend] Created new alembic migration to remove unused photo_path_aux column from users table. This new migration also changes default admin password to new hashing method
[backend] Moved password hashing on login to backend
[backend] Added env variable for expire token in minutes
[backend] Rename file schema_session/schema_access_token to dependencies_security
[backend] Started changing session dependencies functions to handle token in the request cookie
[frontend] Started updating api calls logic
[frontend] Moved login store logic to pinea store instead of local storage
[README] Updated README with new env variable for backend
This commit is contained in:
João Vitória Silva
2024-06-07 22:52:48 +01:00
parent 7a90182e72
commit 3d0507528d
26 changed files with 761 additions and 414 deletions

View File

@@ -32,6 +32,7 @@ ENV DB_DATABASE="endurain"
ENV SECRET_KEY="changeme" ENV SECRET_KEY="changeme"
ENV ALGORITHM="HS256" ENV ALGORITHM="HS256"
ENV ACCESS_TOKEN_EXPIRE_MINUTES=30 ENV ACCESS_TOKEN_EXPIRE_MINUTES=30
ENV REFRESH_TOKEN_EXPIRE_DAYS=7
ENV STRAVA_CLIENT_ID="changeme" ENV STRAVA_CLIENT_ID="changeme"
ENV STRAVA_CLIENT_SECRET="changeme" ENV STRAVA_CLIENT_SECRET="changeme"
ENV STRAVA_AUTH_CODE="changeme" ENV STRAVA_AUTH_CODE="changeme"

View File

@@ -88,8 +88,9 @@ DB_USER | endurain | Yes | N/A
DB_PASSWORD | changeme | `No` | N/A DB_PASSWORD | changeme | `No` | N/A
DB_DATABASE | endurain | Yes | N/A DB_DATABASE | endurain | Yes | N/A
SECRET_KEY | changeme | `No` | N/A SECRET_KEY | changeme | `No` | N/A
ALGORITHM | HS256 | Yes | N/A ALGORITHM | HS256 | Yes | Currently only HS256 is supported
ACCESS_TOKEN_EXPIRE_MINUTES | 30 | Yes | N/A 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_ID | changeme | `No` | N/A
STRAVA_CLIENT_SECRET | changeme | `No` | N/A STRAVA_CLIENT_SECRET | changeme | `No` | N/A
STRAVA_AUTH_CODE | changeme | `No` | N/A STRAVA_AUTH_CODE | changeme | `No` | N/A

View File

@@ -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 ###

View File

@@ -5,7 +5,8 @@ API_VERSION = "v0.2.1"
# JWT Token constants # JWT Token constants
JWT_ALGORITHM = os.environ.get("ALGORITHM") 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") JWT_SECRET_KEY = os.environ.get("SECRET_KEY")
# Scopes definition # Scopes definition

View File

@@ -34,13 +34,13 @@ def format_user_birthdate(user):
return user return user
def authenticate_user(username: str, password: str, db: Session): def authenticate_user(username: str, db: Session):
try: try:
# Get the user from the database # Get the user from the database
user = ( user = (
db.query(models.User) db.query(models.User)
.filter( .filter(
models.User.username == username, models.User.password == password models.User.username == username
) )
.first() .first()
) )

View File

@@ -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)

View File

@@ -1,36 +1,99 @@
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status, Request, Security
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from dependencies import dependencies_database from typing import Annotated
from schemas import schema_access_tokens, schema_users
from dependencies import dependencies_database, dependencies_security
from schemas import schema_users
# Define the OAuth2 scheme for handling bearer tokens # Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 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 # 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( def validate_token_and_return_access_token(request: Request):
token: str = Depends(oauth2_scheme), db: Session = Depends(dependencies_database.get_db) # 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 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( 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 # 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 # 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( 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Validate the token expiration # 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 # Check if user_id higher than 0
if not (int(user_id) > 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 # 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 # 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( 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): def validate_token_user_id_admin_access(db, token, user_id):
# Validate the token expiration # 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 # Check if user_id higher than 0
if not (int(user_id) > 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 # 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 # Check if the token has admin access
schema_access_tokens.validate_token_admin_access(token) 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

@@ -27,7 +27,6 @@ from routers import (
router_strava, router_strava,
) )
from constants import API_VERSION from constants import API_VERSION
from schemas import schema_access_tokens
from database import SessionLocal from database import SessionLocal
from processors import strava_processor, strava_activity_processor from processors import strava_processor, strava_activity_processor
@@ -102,6 +101,7 @@ required_env_vars = [
"SECRET_KEY", "SECRET_KEY",
"ALGORITHM", "ALGORITHM",
"ACCESS_TOKEN_EXPIRE_MINUTES", "ACCESS_TOKEN_EXPIRE_MINUTES",
"REFRESH_TOKEN_EXPIRE_DAYS",
"STRAVA_CLIENT_ID", "STRAVA_CLIENT_ID",
"STRAVA_CLIENT_SECRET", "STRAVA_CLIENT_SECRET",
"STRAVA_AUTH_CODE", "STRAVA_AUTH_CODE",

View File

@@ -90,9 +90,6 @@ class User(Base):
Integer, nullable=False, comment="User type (one digit)(1 - user, 2 - admin)" 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 = 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( is_active = Column(
Integer, nullable=False, comment="Is user active (1 - active, 2 - not active)" Integer, nullable=False, comment="Is user active (1 - active, 2 - not active)"
) )

View File

@@ -18,6 +18,7 @@ from dependencies import (
dependencies_activities, dependencies_activities,
dependencies_gear, dependencies_gear,
dependencies_global, dependencies_global,
dependencies_security,
) )
from processors import gpx_processor, fit_processor from processors import gpx_processor, fit_processor
@@ -45,7 +46,9 @@ async def read_activities_useractivities_week(
], ],
token_user_id: Annotated[ token_user_id: Annotated[
Callable, 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), 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)], validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
token_user_id: Annotated[ token_user_id: Annotated[
Callable, 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), 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)], validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
token_user_id: Annotated[ token_user_id: Annotated[
Callable, 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), 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)], validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
token_user_id: Annotated[ token_user_id: Annotated[
Callable, 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -214,7 +219,9 @@ async def read_activities_gearactivities(
db: Session = Depends(dependencies_database.get_db), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the activities for the gear # 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( @router.get(
@@ -225,7 +232,7 @@ async def read_activities_gearactivities(
async def read_activities_useractivities_number( async def read_activities_useractivities_number(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the number of activities for the user # Get the number of activities for the user
@@ -252,7 +259,7 @@ async def read_activities_useractivities_pagination(
validate_pagination_values: Annotated[ validate_pagination_values: Annotated[
Callable, Depends(dependencies_global.validate_pagination_values) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the activities for the user with pagination # Get the activities for the user with pagination
@@ -281,7 +288,7 @@ async def read_activities_followed_user_activities_pagination(
validate_pagination_values: Annotated[ validate_pagination_values: Annotated[
Callable, Depends(dependencies_global.validate_pagination_values) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the activities for the following users with pagination # 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( async def read_activities_followed_useractivities_number(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the number of activities for the following users # Get the number of activities for the following users
@@ -324,12 +331,14 @@ async def read_activities_activity_from_id(
], ],
token_user_id: Annotated[ token_user_id: Annotated[
Callable, 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the activity from the database and return it # 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( @router.get(
@@ -341,7 +350,7 @@ async def read_activities_contain_name(
name: str, name: str,
token_user_id: Annotated[ token_user_id: Annotated[
Callable, 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -358,7 +367,7 @@ async def read_activities_contain_name(
async def create_activity_with_uploaded_file( async def create_activity_with_uploaded_file(
token_user_id: Annotated[ token_user_id: Annotated[
Callable, Callable,
Depends(dependencies_session.validate_token_and_get_authenticated_user_id), Depends(dependencies_session.validate_access_token_and_get_authenticated_user_id),
], ],
file: UploadFile, file: UploadFile,
db: Session = Depends(dependencies_database.get_db), db: Session = Depends(dependencies_database.get_db),
@@ -425,7 +434,8 @@ async def create_activity_with_uploaded_file(
) from err ) from err
@router.put("/activities/{activity_id}/addgear/{gear_id}", @router.put(
"/activities/{activity_id}/addgear/{gear_id}",
tags=["activities"], tags=["activities"],
) )
async def activity_add_gear( async def activity_add_gear(
@@ -435,7 +445,9 @@ async def activity_add_gear(
], ],
gear_id: int, gear_id: int,
validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the gear by user id and gear id # 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, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Gear ID {gear_id} for user {token_user_id} not found", detail=f"Gear ID {gear_id} for user {token_user_id} not found",
) )
# Get the activity by id from user id # Get the activity by id from user id
activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db) 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 # Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is
if activity is None: if activity is None:
@@ -460,11 +474,13 @@ async def activity_add_gear(
# Add the gear to the activity # Add the gear to the activity
crud_activities.add_gear_to_activity(activity_id, gear_id, db) crud_activities.add_gear_to_activity(activity_id, gear_id, db)
# Return success message # Return success message
return {"detail": f"Gear ID {gear_id} added to activity successfully"} 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"], tags=["activities"],
) )
async def delete_activity_gear( async def delete_activity_gear(
@@ -472,11 +488,15 @@ async def delete_activity_gear(
validate_activity_id: Annotated[ validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the activity by id from user id # Get the activity by id from user id
activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db) 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 # Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is
if activity is None: if activity is None:
@@ -487,11 +507,13 @@ async def delete_activity_gear(
# Delete gear from the activity # Delete gear from the activity
crud_activities.add_gear_to_activity(activity_id, None, db) crud_activities.add_gear_to_activity(activity_id, None, db)
# Return success message # Return success message
return {"detail": f"Gear ID {activity.gear_id} deleted from activity successfully"} 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"], tags=["activities"],
) )
async def delete_activity( async def delete_activity(
@@ -499,11 +521,15 @@ async def delete_activity(
validate_activity_id: Annotated[ validate_activity_id: Annotated[
Callable, Depends(dependencies_activities.validate_activity_id) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Get the activity by id from user id # Get the activity by id from user id
activity = crud_activities.get_activity_by_id_from_user_id(activity_id, token_user_id, db) 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 # Check if activity is None and raise an HTTPException with a 404 Not Found status code if it is
if activity is None: if activity is None:
@@ -511,10 +537,9 @@ async def delete_activity(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Activity ID {activity_id} for user {token_user_id} not found", detail=f"Activity ID {activity_id} for user {token_user_id} not found",
) )
# Delete the activity # Delete the activity
crud_activities.delete_activity(activity_id, db) crud_activities.delete_activity(activity_id, db)
# Return success message # Return success message
return {"detail": f"Activity {activity_id} deleted successfully"} return {"detail": f"Activity {activity_id} deleted successfully"}

View File

@@ -10,7 +10,7 @@ from schemas import schema_activity_streams
from crud import crud_activity_streams from crud import crud_activity_streams
from dependencies import ( from dependencies import (
dependencies_database, dependencies_database,
dependencies_session, dependencies_security,
dependencies_activities, dependencies_activities,
dependencies_activity_streams, dependencies_activity_streams,
) )
@@ -36,7 +36,7 @@ async def read_activities_streams_for_activity_all(
Callable, Depends(dependencies_activities.validate_activity_id) Callable, Depends(dependencies_activities.validate_activity_id)
], ],
validate_token_validate_admin_access: Annotated[ 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), 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) Callable, Depends(dependencies_activity_streams.validate_activity_stream_type)
], ],
validate_token: Annotated[ validate_token: Annotated[
Callable, Depends(dependencies_session.validate_token) Callable, Depends(dependencies_security.validate_token_expiration)
], ],
db: Session = Depends(dependencies_database.get_db), db: Session = Depends(dependencies_database.get_db),
): ):

View File

@@ -10,7 +10,7 @@ from schemas import schema_followers
from crud import crud_followers from crud import crud_followers
from dependencies import ( from dependencies import (
dependencies_database, dependencies_database,
dependencies_session, dependencies_security,
dependencies_users, dependencies_users,
) )
@@ -32,7 +32,7 @@ logger = logging.getLogger("myLogger")
async def get_user_follower_all( async def get_user_follower_all(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return followers # Return followers
@@ -47,7 +47,7 @@ async def get_user_follower_all(
async def get_user_follower_count_all( async def get_user_follower_count_all(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return followers # Return followers
@@ -69,7 +69,7 @@ async def get_user_follower_count_all(
async def get_user_follower_count( async def get_user_follower_count(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return followers # Return followers
@@ -91,7 +91,7 @@ async def get_user_follower_count(
async def get_user_following_all( async def get_user_following_all(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return followings # Return followings
@@ -106,7 +106,7 @@ async def get_user_following_all(
async def get_user_following_count_all( async def get_user_following_count_all(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return followings # Return followings
@@ -128,7 +128,7 @@ async def get_user_following_count_all(
async def get_user_following_count( async def get_user_following_count(
user_id: int, user_id: int,
validate_user_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return followings # Return followings
@@ -154,7 +154,7 @@ async def read_followers_user_specific_user(
validate_target_user_id: Annotated[ validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Return the follower # Return the follower
@@ -175,7 +175,7 @@ async def create_follow(
validate_target_user_id: Annotated[ validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Create the follower # Create the follower
@@ -195,7 +195,7 @@ async def accept_follow(
validate_target_user_id: Annotated[ validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Accept the follower # Accept the follower
@@ -216,7 +216,7 @@ async def delete_follow(
validate_target_user_id: Annotated[ validate_target_user_id: Annotated[
Callable, Depends(dependencies_users.validate_target_user_id) 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), db: Session = Depends(dependencies_database.get_db),
): ):
# Delete the follower # Delete the follower

View File

@@ -34,7 +34,7 @@ async def read_gear_id(
gear_id: int, gear_id: int,
validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], validate_gear_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
user_id: Annotated[ 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)], 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) Callable, Depends(dependencies_global.validate_pagination_values)
], ],
user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -71,7 +71,7 @@ async def read_gear_user_pagination(
) )
async def read_gear_user_number( async def read_gear_user_number(
user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -94,7 +94,7 @@ async def read_gear_user_number(
async def read_gear_user_by_nickname( async def read_gear_user_by_nickname(
nickname: str, nickname: str,
user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -111,7 +111,7 @@ async def read_gear_user_by_type(
gear_type: int, gear_type: int,
validate_type: Annotated[Callable, Depends(dependencies_gear.validate_gear_type)], validate_type: Annotated[Callable, Depends(dependencies_gear.validate_gear_type)],
user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -127,7 +127,7 @@ async def read_gear_user_by_type(
async def create_gear( async def create_gear(
gear: schema_gear.Gear, gear: schema_gear.Gear,
user_id: Annotated[ 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), 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)], validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
gear: schema_gear.Gear, gear: schema_gear.Gear,
token_user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -170,7 +170,7 @@ async def delete_user(
gear_id: int, gear_id: int,
validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)], validate_id: Annotated[Callable, Depends(dependencies_gear.validate_gear_id)],
token_user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):

View File

@@ -1,26 +1,36 @@
import logging import logging
import bcrypt
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Annotated, Callable from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status, Security from fastapi import (
from fastapi.security import ( APIRouter,
OAuth2PasswordBearer, Depends,
OAuth2PasswordRequestForm, HTTPException,
SecurityScopes, status,
Response,
Request,
) )
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from crud import crud_user_integrations, crud_users from crud import crud_users
from schemas import schema_access_tokens, schema_users from schemas import schema_users
from constants import ( from constants import (
USER_NOT_ACTIVE, USER_NOT_ACTIVE,
REGULAR_ACCESS, REGULAR_ACCESS,
REGULAR_ACCESS_SCOPES, REGULAR_ACCESS_SCOPES,
ADMIN_ACCESS_SCOPES, ADMIN_ACCESS_SCOPES,
SCOPES_DICT, 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 # Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer( oauth2_scheme = OAuth2PasswordBearer(
@@ -35,60 +45,108 @@ router = APIRouter()
logger = logging.getLogger("myLogger") 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): def authenticate_user(username: str, password: str, db: Session):
# Get the user from the database # 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: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username",
headers={"WWW-Authenticate": "Bearer"}, 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 return user
def get_current_user(db: Session, user_id: int): def create_response_with_tokens(response: Response, user: schema_users.User):
# Get the user from the database # Check user access level and set scopes accordingly
user = crud_users.get_user_by_id(user_id, db) if user.access_type == REGULAR_ACCESS:
scopes = REGULAR_ACCESS_SCOPES
else:
scopes = ADMIN_ACCESS_SCOPES
# If the user does not exist raise the exception # Create the access and refresh tokens
if user is None: access_token = dependencies_security.create_token(
raise HTTPException( data={
status_code=status.HTTP_401_UNAUTHORIZED, "sub": user.username,
detail="Could not validate credentials (user not found)", "scopes": scopes,
headers={"WWW-Authenticate": "Bearer"}, "id": user.id,
) "access_type": user.access_type,
"exp": datetime.now(timezone.utc)
user_integrations = crud_user_integrations.get_user_integrations_by_user_id( + timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES),
user.id, db },
) )
if user_integrations is None: refresh_token = dependencies_security.create_token(
raise HTTPException( data={
status_code=status.HTTP_401_UNAUTHORIZED, "sub": user.username,
detail="Could not validate credentials (user integrations not found)", "scopes": "scopes",
headers={"WWW-Authenticate": "Bearer"}, "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: # Set the cookies with the tokens
user.is_strava_linked = 0 response.set_cookie(
else: key="endurain_access_token",
user.is_strava_linked = 1 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 # Set the user id in a cookie
return user response.set_cookie(
key="endurain_logged_user_id",
value=user.id,
httponly=False,
)
# Return the response
return response
@router.post( @router.post("/token", tags=["session"])
"/token", response_model=schema_access_tokens.AccessToken, tags=["session"]
)
async def login_for_access_token( async def login_for_access_token(
response: Response,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()], form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
do_not_expire: bool = False,
db: Session = Depends(dependencies_database.get_db), db: Session = Depends(dependencies_database.get_db),
): ):
user = authenticate_user(form_data.username, form_data.password, 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"}, headers={"WWW-Authenticate": "Bearer"},
) )
expire = None response = create_response_with_tokens(response, user)
if do_not_expire:
expire = datetime.utcnow() + timedelta(days=90)
if user.access_type == REGULAR_ACCESS: return {"message": "Login successful"}
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"
)
@router.get("/users/me", response_model=schema_users.UserMe, tags=["session"]) @router.post("/refresh", tags=["session"])
async def read_users_me( async def refresh_token(
security_scopes: SecurityScopes, response: Response,
request: Request,
user_id: Annotated[ user_id: Annotated[
int, Depends(dependencies_session.validate_token_and_get_authenticated_user_id) int,
], Depends(
check_scopes: Annotated[ dependencies_session.validate_refresh_token_and_get_authenticated_user_id
Callable, Security(schema_access_tokens.check_scopes, scopes=["users:read"]) ),
], ],
db: Session = Depends(dependencies_database.get_db), 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"]) response = create_response_with_tokens(response, user)
async def validate_token(
validate_token: Callable = Depends(dependencies_session.validate_token), return {"message": "Token refreshed successfully"}
db: Session = Depends(dependencies_database.get_db),
):
# Return None if the token is valid
return None

View File

@@ -78,9 +78,7 @@ async def strava_link(
# Redirect to the main page or any other desired page after processing # Redirect to the main page or any other desired page after processing
redirect_url = ( redirect_url = (
"https://" "https://" + os.environ.get("FRONTEND_HOST") + "/settings?stravaLinked=1"
+ os.environ.get("FRONTEND_HOST")
+ "/settings?stravaLinked=1"
) )
# Return a RedirectResponse to the redirect URL # Return a RedirectResponse to the redirect URL
@@ -104,7 +102,10 @@ async def strava_link(
async def strava_retrieve_activities_days( async def strava_retrieve_activities_days(
days: int, days: int,
token_user_id: Annotated[ 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)], # db: Annotated[Session, Depends(dependencies_database.get_db)],
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
@@ -132,7 +133,10 @@ async def strava_retrieve_activities_days(
async def strava_set_user_unique_state( async def strava_set_user_unique_state(
state: str, state: str,
user_id: Annotated[ 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)], 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( async def strava_unset_user_unique_state(
user_id: Annotated[ 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)], 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"]) @router.delete("/strava/unlink", tags=["strava"])
async def strava_unlink( async def strava_unlink(
token_user_id: Annotated[ 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), db: Session = Depends(dependencies_database.get_db),
): ):
@@ -183,7 +187,7 @@ async def strava_unlink(
@router.get("/strava/gear", status_code=202, tags=["strava"]) @router.get("/strava/gear", status_code=202, tags=["strava"])
async def strava_retrieve_gear( async def strava_retrieve_gear(
token_user_id: Annotated[ 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, background_tasks: BackgroundTasks,
): ):

View File

@@ -3,7 +3,7 @@ import logging
from typing import Annotated, Callable 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 fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -16,6 +16,7 @@ from dependencies import (
dependencies_session, dependencies_session,
dependencies_global, dependencies_global,
dependencies_users, dependencies_users,
dependencies_security,
) )
# Define the OAuth2 scheme for handling bearer tokens # Define the OAuth2 scheme for handling bearer tokens
@@ -28,12 +29,65 @@ router = APIRouter()
logger = logging.getLogger("myLogger") 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"]) @router.get("/users/number", response_model=int, tags=["users"])
async def read_users_number( async def read_users_number(
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
return crud_users.get_users_number(db) return crud_users.get_users_number(db)
@@ -49,10 +103,16 @@ async def read_users_all_pagination(
validate_pagination_values: Annotated[ validate_pagination_values: Annotated[
Callable, Depends(dependencies_global.validate_pagination_values) Callable, Depends(dependencies_global.validate_pagination_values)
], ],
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
# Get the users from the database with pagination # Get the users from the database with pagination
return crud_users.get_users_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( async def read_users_contain_username(
username: str, username: str,
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
# Get the users from the database by username # Get the users from the database by username
return crud_users.get_user_if_contains_username(username=username, db=db) 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( async def read_users_username(
username: str, username: str,
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
# Get the user from the database by username # Get the user from the database by username
return crud_users.get_user_by_username(username=username, db=db) 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( async def read_users_id(
user_id: int, user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token: Callable = Depends(dependencies_session.validate_token), validate_access_token: Annotated[
db: Session = Depends(dependencies_database.get_db), 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 # Get the users from the database by id
return crud_users.get_user_by_id(user_id=user_id, db=db) 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"]) @router.get("/users/{username}/id", response_model=int, tags=["users"])
async def read_users_username_id( async def read_users_username_id(
username: str, username: str,
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
# Get the users from the database by username # Get the users from the database by username
return crud_users.get_user_id_by_username(username, db) 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( async def read_users_id_photo_path(
user_id: int, user_id: int,
validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)], validate_id: Annotated[Callable, Depends(dependencies_users.validate_user_id)],
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
# Get the photo_path from the database by id # Get the photo_path from the database by id
return crud_users.get_user_photo_path_by_id(user_id, db) 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"]) @router.post("/users/create", response_model=int, status_code=201, tags=["users"])
async def create_user( async def create_user(
user: schema_users.UserCreate, user: schema_users.UserCreate,
validate_token_validate_admin_access: Annotated[ validate_access_token_and_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_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"])
],
db: Annotated[
Session,
Depends(dependencies_database.get_db),
], ],
db: Session = Depends(dependencies_database.get_db),
): ):
# Create the user in the database # Create the user in the database
created_user = crud_users.create_user(user, db) created_user = crud_users.create_user(user, db)
@@ -171,15 +254,23 @@ async def upload_user_image(
user_id: int, user_id: int,
token_user_id: Annotated[ token_user_id: Annotated[
Callable, Callable,
Depends(dependencies_session.validate_token_and_get_authenticated_user_id), Depends(
dependencies_session.validate_access_token_and_get_authenticated_user_id
),
], ],
file: UploadFile, 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: try:
upload_dir = "user_images" upload_dir = "user_images"
os.makedirs(upload_dir, exist_ok=True) os.makedirs(upload_dir, exist_ok=True)
# Get file extension # Get file extension
_, file_extension = os.path.splitext(file.filename) _, file_extension = os.path.splitext(file.filename)
filename = f"{user_id}{file_extension}" filename = f"{user_id}{file_extension}"
@@ -188,13 +279,11 @@ async def upload_user_image(
with open(file_path_to_save, "wb") as buffer: with open(file_path_to_save, "wb") as buffer:
shutil.copyfileobj(file.file, buffer) shutil.copyfileobj(file.file, buffer)
return crud_users.edit_user_photo_path(user_id, file_path_to_save, db) return crud_users.edit_user_photo_path(user_id, file_path_to_save, db)
except Exception as err: except Exception as err:
# Log the exception # Log the exception
logger.error( logger.error(f"Error in upload_user_image: {err}", exc_info=True)
f"Error in upload_user_image: {err}", exc_info=True
)
# Remove the file after processing # Remove the file after processing
if os.path.exists(file_path_to_save): 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 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 # Update the user in the database
crud_users.edit_user(user_attributtes, db) 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 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 # Update the user password in the database
crud_users.edit_user_password(user_attributtes.id, user_attributtes.password, db) 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 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 # Update the user photo_path in the database
crud_users.delete_user_photo(user_id, db) crud_users.delete_user_photo(user_id, db)
@@ -268,7 +375,13 @@ async def delete_user(
validate_token_validate_admin_access: Annotated[ validate_token_validate_admin_access: Annotated[
Callable, Depends(dependencies_session.validate_token_and_validate_admin_access) 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 # Delete the user in the database
crud_users.delete_user(user_id, db) crud_users.delete_user(user_id, db)

View File

@@ -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

View File

@@ -12,7 +12,6 @@ class User(BaseModel):
gender: int gender: int
access_type: int access_type: int
photo_path: str | None = None photo_path: str | None = None
photo_path_aux: str | None = None
is_active: int is_active: int
class Config: class Config:

View File

@@ -9,7 +9,7 @@ import UserView from '../views/UserView.vue'
import SettingsView from '../views/SettingsView.vue'; import SettingsView from '../views/SettingsView.vue';
import NotFoundView from '../views/NotFoundView.vue'; import NotFoundView from '../views/NotFoundView.vue';
import { auth } from '@/services/auth'; //import { auth } from '@/services/auth';
//import { useAuthStore } from '@/stores/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 accessToken = localStorage.getItem('accessToken');
const tokenType = localStorage.getItem('tokenType'); const tokenType = localStorage.getItem('tokenType');
@@ -79,6 +79,6 @@ router.beforeEach((to, from, next) => {
} else { } else {
next(); next();
} }
}); }); */
export default router; export default router;

View File

@@ -1,4 +1,4 @@
import { fetchGetRequestTokenAsParameter, fetchPostFormUrlEncoded } from '@/utils/serviceUtils'; import { fetchGetRequest, fetchPostFormUrlEncoded } from '@/utils/serviceUtils';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
export const auth = { export const auth = {
@@ -13,9 +13,7 @@ export const auth = {
return exp > currentTime; return exp > currentTime;
}, },
storeLoggedUser(token, userMe) { storeLoggedUser(userMe) {
localStorage.setItem('accessToken', token.access_token);
localStorage.setItem('tokenType', token.token_type);
localStorage.setItem('userMe', JSON.stringify(userMe)); localStorage.setItem('userMe', JSON.stringify(userMe));
}, },
removeLoggedUser() { removeLoggedUser() {
@@ -28,6 +26,6 @@ export const auth = {
return fetchPostFormUrlEncoded('token', formData); return fetchPostFormUrlEncoded('token', formData);
}, },
getUserMe(token) { getUserMe(token) {
return fetchGetRequestTokenAsParameter('users/me', token); return fetchGetRequest('users/me', token);
}, },
}; };

View File

@@ -0,0 +1,7 @@
import { fetchGetRequest, fetchPostFormUrlEncoded } from '@/utils/serviceUtils';
export const session = {
getToken(formData) {
return fetchPostFormUrlEncoded('token', formData);
},
};

View File

@@ -1,6 +1,9 @@
import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest, fetchPostFileRequest } from '@/utils/serviceUtils'; import { fetchGetRequest, fetchPostRequest, fetchPutRequest, fetchDeleteRequest, fetchPostFileRequest } from '@/utils/serviceUtils';
export const users = { export const users = {
getUserMe() {
return fetchGetRequest('users/me');
},
getUsersWithPagination(pageNumber, numRecords) { getUsersWithPagination(pageNumber, numRecords) {
return fetchGetRequest(`users/all/page_number/${pageNumber}/num_records/${numRecords}`); return fetchGetRequest(`users/all/page_number/${pageNumber}/num_records/${numRecords}`);
}, },

View File

@@ -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();
}
}
});

View File

@@ -13,9 +13,9 @@ export async function fetchGetRequest(url) {
// Send the GET request // Send the GET request
const response = await fetch(fullUrl, { const response = await fetch(fullUrl, {
method: 'GET', method: 'GET',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
}, },
}); });
// If the response status is not ok, throw an error // 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) { export async function fetchPostFormUrlEncoded(url, formData) {
// Create the full URL by combining the API URL with the provided URL // Create the full URL by combining the API URL with the provided URL
const fullUrl = `${API_URL}${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 // Send the POST request
const response = await fetch(fullUrl, { const response = await fetch(fullUrl, {
method: 'POST', method: 'POST',
body: formData, body: urlEncodedData,
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },

View File

@@ -21,11 +21,11 @@
<label for="loginPassword">{{ $t("login.password") }}</label> <label for="loginPassword">{{ $t("login.password") }}</label>
</div> </div>
<br> <br>
<div class="form-check"> <!--<div class="form-check">
<input type="checkbox" class="form-check-input" name="loginNeverExpires" v-model="neverExpires"> <input type="checkbox" class="form-check-input" name="loginNeverExpires" v-model="neverExpires">
<label class="form-check-label" for="loginNeverExpires">{{ $t("login.neverExpires") }}</label> <label class="form-check-label" for="loginNeverExpires">{{ $t("login.neverExpires") }}</label>
</div> </div>
<br> <br>-->
<button class="w-100 btn btn-lg btn-primary" type="submit">{{ $t("login.signInButton") }}</button> <button class="w-100 btn btn-lg btn-primary" type="submit">{{ $t("login.signInButton") }}</button>
<!--<div> <!--<div>
<br> <br>
@@ -46,13 +46,16 @@ import { useI18n } from 'vue-i18n';
// Importing the stores // Importing the stores
import { useErrorAlertStore } from '@/stores/Alerts/errorAlert'; import { useErrorAlertStore } from '@/stores/Alerts/errorAlert';
import { useInfoAlertStore } from '@/stores/Alerts/infoAlert'; import { useInfoAlertStore } from '@/stores/Alerts/infoAlert';
import { useSessionStore } from '@/stores/session';
// Importing the services for the login // Importing the services for the login
import { auth } from '@/services/auth'; //import { auth } from '@/services/auth';
import { session } from '@/services/session';
import { users } from '@/services/user';
// Importing the components // Importing the components
import ErrorAlertComponent from '@/components/Alerts/ErrorAlertComponent.vue'; import ErrorAlertComponent from '@/components/Alerts/ErrorAlertComponent.vue';
import InfoAlertComponent from '@/components/Alerts/InfoAlertComponent.vue'; import InfoAlertComponent from '@/components/Alerts/InfoAlertComponent.vue';
// Importing the crypto-js // Importing the crypto-js
import CryptoJS from 'crypto-js'; //import CryptoJS from 'crypto-js';
// Exporting the default object // Exporting the default object
@@ -70,30 +73,33 @@ export default {
const { t } = useI18n(); const { t } = useI18n();
const username = ref(''); const username = ref('');
const password = ref(''); const password = ref('');
const neverExpires = ref(false); //const neverExpires = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const showSessionExpiredMessage = ref(false); const showSessionExpiredMessage = ref(false);
const errorAlertStore = useErrorAlertStore(); const errorAlertStore = useErrorAlertStore();
const infoAlertStore = useInfoAlertStore(); const infoAlertStore = useInfoAlertStore();
const sessionStore = useSessionStore();
// Handle the form submission // Handle the form submission
const submitForm = async () => { const submitForm = async () => {
// Hash the password // Hash the password
const hashedPassword = CryptoJS.SHA256(password.value).toString(CryptoJS.enc.Hex); //const hashedPassword = CryptoJS.SHA256(password.value).toString(CryptoJS.enc.Hex);
// Create the form data // Create the form data
const formData = new URLSearchParams(); const formData = new URLSearchParams();
formData.append('username', username.value); formData.append('username', username.value);
formData.append('password', hashedPassword); formData.append('password', password.value);
formData.append('neverExpires', neverExpires.value); //formData.append('neverExpires', neverExpires.value);
try { try {
// Get the token // Get the token
const token = await auth.getToken(formData); await session.getToken(formData);
// Get the userMe
const userMe = await auth.getUserMe(token.access_token);
// Store the logged user // Get logged user information
auth.storeLoggedUser(token, userMe); const userMe = await users.getUserMe();
// Store the user in the session store
sessionStore.setUser(userMe);
// Redirect to the home page // Redirect to the home page
router.push('/'); router.push('/');
} catch (error) { } catch (error) {
@@ -127,7 +133,7 @@ export default {
return { return {
username, username,
password, password,
neverExpires, //neverExpires,
showSessionExpiredMessage, showSessionExpiredMessage,
errorMessage, errorMessage,
submitForm, submitForm,

View File

@@ -13,4 +13,5 @@ opentelemetry.exporter.otlp==1.22.0
python-multipart==0.0.9 python-multipart==0.0.9
gpxpy==1.6.2 gpxpy==1.6.2
alembic==1.13.1 alembic==1.13.1
joserfc==0.11.1 joserfc==0.11.1
bcrypt==4.1.3