API - init resource protector (that also handles current authentication)

This commit is contained in:
Sam 2022-05-27 15:51:40 +02:00
parent eeae632b01
commit 44c16f6805
15 changed files with 145 additions and 121 deletions

View File

@ -4,12 +4,12 @@ from flask import Blueprint, current_app, request
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
HttpResponse, HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
handle_error_and_return_response, handle_error_and_return_response,
) )
from fittrackee.users.decorators import authenticate_as_admin
from fittrackee.users.models import User from fittrackee.users.models import User
from fittrackee.users.utils.controls import is_valid_email from fittrackee.users.utils.controls import is_valid_email
@ -67,7 +67,7 @@ def get_application_config() -> Union[Dict, HttpResponse]:
@config_blueprint.route('/config', methods=['PATCH']) @config_blueprint.route('/config', methods=['PATCH'])
@authenticate_as_admin @require_auth(as_admin=True)
def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]: def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
Update Application config Update Application config

View File

@ -1,11 +1,14 @@
from authlib.integrations.sqla_oauth2 import create_revocation_endpoint from authlib.integrations.sqla_oauth2 import (
create_bearer_token_validator,
create_revocation_endpoint,
)
from authlib.oauth2.rfc7636 import CodeChallenge from authlib.oauth2.rfc7636 import CodeChallenge
from flask import Flask from flask import Flask
from fittrackee import db from fittrackee import db
from .grants import AuthorizationCodeGrant, OAuth2Token, RefreshTokenGrant from .grants import AuthorizationCodeGrant, OAuth2Token, RefreshTokenGrant
from .server import authorization_server from .server import authorization_server, require_auth
def config_oauth(app: Flask) -> None: def config_oauth(app: Flask) -> None:
@ -21,3 +24,7 @@ def config_oauth(app: Flask) -> None:
revocation_cls = create_revocation_endpoint(db.session, OAuth2Token) revocation_cls = create_revocation_endpoint(db.session, OAuth2Token)
revocation_cls.CLIENT_AUTH_METHODS = ['client_secret_post'] revocation_cls.CLIENT_AUTH_METHODS = ['client_secret_post']
authorization_server.register_endpoint(revocation_cls) authorization_server.register_endpoint(revocation_cls)
# protect resource
bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
require_auth.register_token_validator(bearer_cls())

View File

@ -0,0 +1,80 @@
from functools import wraps
from typing import Any, Callable, List, Union
from authlib.integrations.flask_oauth2 import ResourceProtector
from authlib.oauth2 import OAuth2Error
from authlib.oauth2.rfc6749.errors import MissingAuthorizationError
from flask import current_app, request
from werkzeug.exceptions import RequestEntityTooLarge
from fittrackee.responses import (
ForbiddenErrorResponse,
PayloadTooLargeErrorResponse,
UnauthorizedErrorResponse,
)
from fittrackee.users.models import User
class CustomResourceProtector(ResourceProtector):
def __call__(
self,
scopes: Union[str, List] = None,
as_admin: bool = False,
) -> Callable:
def wrapper(f: Callable) -> Callable:
@wraps(f)
def decorated(*args: Any, **kwargs: Any) -> Callable:
auth_user = None
auth_header = request.headers.get('Authorization')
if not auth_header:
return UnauthorizedErrorResponse(
'provide a valid auth token'
)
# First-party application (Fittrackee front-end)
# in this case, scopes will be ignored
auth_token = auth_header.split(' ')[1]
resp = User.decode_auth_token(auth_token)
if isinstance(resp, int):
auth_user = User.query.filter_by(id=resp).first()
# Third-party applications
if not auth_user:
current_token = None
try:
current_token = self.acquire_token(scopes)
except MissingAuthorizationError as error:
self.raise_error_response(error)
except OAuth2Error as error:
self.raise_error_response(error)
except RequestEntityTooLarge:
file_type = ''
if request.endpoint in [
'auth.edit_picture',
'workouts.post_workout',
]:
file_type = (
'picture'
if request.endpoint == 'auth.edit_picture'
else 'workout'
)
return PayloadTooLargeErrorResponse(
file_type=file_type,
file_size=request.content_length,
max_size=current_app.config['MAX_CONTENT_LENGTH'],
)
auth_user = (
None if current_token is None else current_token.user
)
if not auth_user or not auth_user.is_active:
return UnauthorizedErrorResponse(
'provide a valid auth token'
)
if as_admin and not auth_user.admin:
return ForbiddenErrorResponse()
return f(auth_user, *args, **kwargs)
return decorated
return wrapper

View File

@ -3,8 +3,8 @@ from typing import Dict, Tuple, Union
from flask import Blueprint, Response, request from flask import Blueprint, Response, request
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import HttpResponse, InvalidPayloadErrorResponse from fittrackee.responses import HttpResponse, InvalidPayloadErrorResponse
from fittrackee.users.decorators import authenticate
from fittrackee.users.models import User from fittrackee.users.models import User
from .client import create_oauth_client from .client import create_oauth_client
@ -21,7 +21,7 @@ EXPECTED_METADATA_KEYS = [
@oauth_blueprint.route('/oauth/apps', methods=['POST']) @oauth_blueprint.route('/oauth/apps', methods=['POST'])
@authenticate @require_auth()
def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]: def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
client_metadata = request.get_json() client_metadata = request.get_json()
if not client_metadata: if not client_metadata:
@ -55,7 +55,7 @@ def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
@oauth_blueprint.route('/oauth/authorize', methods=['POST']) @oauth_blueprint.route('/oauth/authorize', methods=['POST'])
@authenticate @require_auth()
def authorize(auth_user: User) -> Response: def authorize(auth_user: User) -> Response:
data = request.form data = request.form
if not data or 'client_id' not in data or 'response_type' not in data: if not data or 'client_id' not in data or 'response_type' not in data:

View File

@ -7,6 +7,7 @@ from authlib.integrations.sqla_oauth2 import (
from fittrackee import db from fittrackee import db
from .models import OAuth2Client, OAuth2Token from .models import OAuth2Client, OAuth2Token
from .resource_protector import CustomResourceProtector
query_client = create_query_client_func(db.session, OAuth2Client) query_client = create_query_client_func(db.session, OAuth2Client)
save_token = create_save_token_func(db.session, OAuth2Token) save_token = create_save_token_func(db.session, OAuth2Token)
@ -14,3 +15,4 @@ authorization_server = AuthorizationServer(
query_client=query_client, query_client=query_client,
save_token=save_token, save_token=save_token,
) )
require_auth = CustomResourceProtector()

View File

@ -147,6 +147,18 @@ class ApiTestCaseMixin(RandomMixin):
error='invalid_request', error='invalid_request',
) )
@staticmethod
def assert_invalid_token(response: TestResponse) -> Dict:
return assert_oauth_errored_response(
response,
401,
error='invalid_token',
error_description=(
'The access token provided is expired, revoked, malformed, '
'or invalid for other reasons.'
),
)
class CallArgsMixin: class CallArgsMixin:
@staticmethod @staticmethod

View File

@ -477,7 +477,7 @@ class TestUserProfile(ApiTestCaseMixin):
'/api/auth/profile', headers=dict(Authorization='Bearer invalid') '/api/auth/profile', headers=dict(Authorization='Bearer invalid')
) )
self.assert_401(response, 'invalid token, please log in again') self.assert_invalid_token(response)
def test_it_returns_user(self, app: Flask, user_1: User) -> None: def test_it_returns_user(self, app: Flask, user_1: User) -> None:
client, auth_token = self.get_test_client_and_auth_token( client, auth_token = self.get_test_client_and_auth_token(

View File

@ -19,6 +19,7 @@ from fittrackee.emails.tasks import (
reset_password_email, reset_password_email,
) )
from fittrackee.files import get_absolute_file_path from fittrackee.files import get_absolute_file_path
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse, HttpResponse,
@ -32,7 +33,6 @@ from fittrackee.responses import (
from fittrackee.utils import get_readable_duration from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Sport from fittrackee.workouts.models import Sport
from .decorators import authenticate
from .models import User, UserSportPreference from .models import User, UserSportPreference
from .utils.controls import check_password, is_valid_email, register_controls from .utils.controls import check_password, is_valid_email, register_controls
from .utils.token import decode_user_token from .utils.token import decode_user_token
@ -252,7 +252,7 @@ def login_user() -> Union[Dict, HttpResponse]:
@auth_blueprint.route('/auth/profile', methods=['GET']) @auth_blueprint.route('/auth/profile', methods=['GET'])
@authenticate @require_auth()
def get_authenticated_user_profile( def get_authenticated_user_profile(
auth_user: User, auth_user: User,
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -354,7 +354,7 @@ def get_authenticated_user_profile(
@auth_blueprint.route('/auth/profile/edit', methods=['POST']) @auth_blueprint.route('/auth/profile/edit', methods=['POST'])
@authenticate @require_auth()
def edit_user(auth_user: User) -> Union[Dict, HttpResponse]: def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
edit authenticated user profile edit authenticated user profile
@ -502,7 +502,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
@auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH']) @auth_blueprint.route('/auth/profile/edit/account', methods=['PATCH'])
@authenticate @require_auth()
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]: def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
update authenticated user email and password update authenticated user email and password
@ -712,7 +712,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
@auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST']) @auth_blueprint.route('/auth/profile/edit/preferences', methods=['POST'])
@authenticate @require_auth()
def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]: def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
edit authenticated user preferences edit authenticated user preferences
@ -853,7 +853,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
@auth_blueprint.route('/auth/profile/edit/sports', methods=['POST']) @auth_blueprint.route('/auth/profile/edit/sports', methods=['POST'])
@authenticate @require_auth()
def edit_user_sport_preferences( def edit_user_sport_preferences(
auth_user: User, auth_user: User,
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -959,7 +959,7 @@ def edit_user_sport_preferences(
@auth_blueprint.route( @auth_blueprint.route(
'/auth/profile/reset/sports/<sport_id>', methods=['DELETE'] '/auth/profile/reset/sports/<sport_id>', methods=['DELETE']
) )
@authenticate @require_auth()
def reset_user_sport_preferences( def reset_user_sport_preferences(
auth_user: User, sport_id: int auth_user: User, sport_id: int
) -> Union[Tuple[Dict, int], HttpResponse]: ) -> Union[Tuple[Dict, int], HttpResponse]:
@ -1014,7 +1014,7 @@ def reset_user_sport_preferences(
@auth_blueprint.route('/auth/picture', methods=['POST']) @auth_blueprint.route('/auth/picture', methods=['POST'])
@authenticate @require_auth()
def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]: def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
update authenticated user picture update authenticated user picture
@ -1102,7 +1102,7 @@ def edit_picture(auth_user: User) -> Union[Dict, HttpResponse]:
@auth_blueprint.route('/auth/picture', methods=['DELETE']) @auth_blueprint.route('/auth/picture', methods=['DELETE'])
@authenticate @require_auth()
def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: def del_picture(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
""" """
delete authenticated user picture delete authenticated user picture

View File

@ -1,39 +0,0 @@
from functools import wraps
from typing import Any, Callable, Union
from flask import request
from fittrackee.responses import HttpResponse
from .utils.controls import verify_user
def verify_auth_user(
f: Callable, verify_admin: bool, *args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
response_object, user = verify_user(request, verify_admin=verify_admin)
if response_object:
return response_object
return f(user, *args, **kwargs)
def authenticate(f: Callable) -> Callable:
@wraps(f)
def decorated_function(
*args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
verify_admin = False
return verify_auth_user(f, verify_admin, *args, **kwargs)
return decorated_function
def authenticate_as_admin(f: Callable) -> Callable:
@wraps(f)
def decorated_function(
*args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
verify_admin = True
return verify_auth_user(f, verify_admin, *args, **kwargs)
return decorated_function

View File

@ -12,6 +12,7 @@ from fittrackee.emails.tasks import (
reset_password_email, reset_password_email,
) )
from fittrackee.files import get_absolute_file_path from fittrackee.files import get_absolute_file_path
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse, HttpResponse,
@ -23,7 +24,6 @@ from fittrackee.responses import (
from fittrackee.utils import get_readable_duration from fittrackee.utils import get_readable_duration
from fittrackee.workouts.models import Record, Workout, WorkoutSegment from fittrackee.workouts.models import Record, Workout, WorkoutSegment
from .decorators import authenticate, authenticate_as_admin
from .exceptions import InvalidEmailException, UserNotFoundException from .exceptions import InvalidEmailException, UserNotFoundException
from .models import User, UserSportPreference from .models import User, UserSportPreference
from .utils.admin import UserManagerService from .utils.admin import UserManagerService
@ -34,7 +34,7 @@ USER_PER_PAGE = 10
@users_blueprint.route('/users', methods=['GET']) @users_blueprint.route('/users', methods=['GET'])
@authenticate_as_admin @require_auth(as_admin=True)
def get_users(auth_user: User) -> Dict: def get_users(auth_user: User) -> Dict:
""" """
Get all users (regardless their account status), if authenticated user Get all users (regardless their account status), if authenticated user
@ -235,7 +235,7 @@ def get_users(auth_user: User) -> Dict:
@users_blueprint.route('/users/<user_name>', methods=['GET']) @users_blueprint.route('/users/<user_name>', methods=['GET'])
@authenticate @require_auth()
def get_single_user( def get_single_user(
auth_user: User, user_name: str auth_user: User, user_name: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -394,7 +394,7 @@ def get_picture(user_name: str) -> Any:
@users_blueprint.route('/users/<user_name>', methods=['PATCH']) @users_blueprint.route('/users/<user_name>', methods=['PATCH'])
@authenticate_as_admin @require_auth(as_admin=True)
def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]: def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
""" """
Update user account Update user account
@ -593,7 +593,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
@users_blueprint.route('/users/<user_name>', methods=['DELETE']) @users_blueprint.route('/users/<user_name>', methods=['DELETE'])
@authenticate @require_auth()
def delete_user( def delete_user(
auth_user: User, user_name: str auth_user: User, user_name: str
) -> Union[Tuple[Dict, int], HttpResponse]: ) -> Union[Tuple[Dict, int], HttpResponse]:

View File

@ -1,15 +1,4 @@
import re import re
from typing import Optional, Tuple
from flask import Request
from fittrackee.responses import (
ForbiddenErrorResponse,
HttpResponse,
UnauthorizedErrorResponse,
)
from ..models import User
def is_valid_email(email: str) -> bool: def is_valid_email(email: str) -> bool:
@ -58,30 +47,3 @@ def register_controls(username: str, email: str, password: str) -> str:
ret += 'email: valid email must be provided\n' ret += 'email: valid email must be provided\n'
ret += check_password(password) ret += check_password(password)
return ret return ret
def verify_user(
current_request: Request, verify_admin: bool
) -> Tuple[Optional[HttpResponse], Optional[User]]:
"""
Return authenticated user if
- the provided token is valid
- the user account is active
- the user has admin rights if 'verify_admin' is True
If not, it returns Error Response
"""
default_message = 'provide a valid auth token'
auth_header = current_request.headers.get('Authorization')
if not auth_header:
return UnauthorizedErrorResponse(default_message), None
auth_token = auth_header.split(' ')[1]
resp = User.decode_auth_token(auth_token)
if isinstance(resp, str):
return UnauthorizedErrorResponse(resp), None
user = User.query.filter_by(id=resp).first()
if not user or not user.is_active:
return UnauthorizedErrorResponse(default_message), None
if verify_admin and not user.admin:
return ForbiddenErrorResponse(), None
return None, user

View File

@ -2,7 +2,7 @@ from typing import Dict
from flask import Blueprint from flask import Blueprint
from fittrackee.users.decorators import authenticate from fittrackee.oauth2.server import require_auth
from fittrackee.users.models import User from fittrackee.users.models import User
from .models import Record from .models import Record
@ -11,7 +11,7 @@ records_blueprint = Blueprint('records', __name__)
@records_blueprint.route('/records', methods=['GET']) @records_blueprint.route('/records', methods=['GET'])
@authenticate @require_auth()
def get_records(auth_user: User) -> Dict: def get_records(auth_user: User) -> Dict:
""" """
Get all records for authenticated user. Get all records for authenticated user.

View File

@ -4,13 +4,13 @@ from flask import Blueprint, request
from sqlalchemy import exc from sqlalchemy import exc
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
DataNotFoundErrorResponse, DataNotFoundErrorResponse,
HttpResponse, HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
handle_error_and_return_response, handle_error_and_return_response,
) )
from fittrackee.users.decorators import authenticate, authenticate_as_admin
from fittrackee.users.models import User, UserSportPreference from fittrackee.users.models import User, UserSportPreference
from .models import Sport from .models import Sport
@ -19,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
@sports_blueprint.route('/sports', methods=['GET']) @sports_blueprint.route('/sports', methods=['GET'])
@authenticate @require_auth()
def get_sports(auth_user: User) -> Dict: def get_sports(auth_user: User) -> Dict:
""" """
Get all sports Get all sports
@ -195,7 +195,7 @@ def get_sports(auth_user: User) -> Dict:
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET']) @sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
@authenticate @require_auth()
def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
""" """
Get a sport Get a sport
@ -304,7 +304,7 @@ def get_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH']) @sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
@authenticate_as_admin @require_auth(as_admin=True)
def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]: def update_sport(auth_user: User, sport_id: int) -> Union[Dict, HttpResponse]:
""" """
Update a sport Update a sport

View File

@ -5,6 +5,7 @@ from flask import Blueprint, request
from sqlalchemy import func from sqlalchemy import func
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
HttpResponse, HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
@ -12,7 +13,6 @@ from fittrackee.responses import (
UserNotFoundErrorResponse, UserNotFoundErrorResponse,
handle_error_and_return_response, handle_error_and_return_response,
) )
from fittrackee.users.decorators import authenticate, authenticate_as_admin
from fittrackee.users.models import User from fittrackee.users.models import User
from .models import Sport, Workout from .models import Sport, Workout
@ -174,7 +174,7 @@ def get_workouts(
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET']) @stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
@authenticate @require_auth()
def get_workouts_by_time( def get_workouts_by_time(
auth_user: User, user_name: str auth_user: User, user_name: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -281,7 +281,7 @@ def get_workouts_by_time(
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET']) @stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
@authenticate @require_auth()
def get_workouts_by_sport( def get_workouts_by_sport(
auth_user: User, user_name: str auth_user: User, user_name: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -377,7 +377,7 @@ def get_workouts_by_sport(
@stats_blueprint.route('/stats/all', methods=['GET']) @stats_blueprint.route('/stats/all', methods=['GET'])
@authenticate_as_admin @require_auth(as_admin=True)
def get_application_stats(auth_user: User) -> Dict: def get_application_stats(auth_user: User) -> Dict:
""" """
Get all application statistics Get all application statistics

View File

@ -17,6 +17,7 @@ from werkzeug.exceptions import RequestEntityTooLarge
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from fittrackee import appLog, db from fittrackee import appLog, db
from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
DataInvalidPayloadErrorResponse, DataInvalidPayloadErrorResponse,
DataNotFoundErrorResponse, DataNotFoundErrorResponse,
@ -28,7 +29,6 @@ from fittrackee.responses import (
get_error_response_if_file_is_invalid, get_error_response_if_file_is_invalid,
handle_error_and_return_response, handle_error_and_return_response,
) )
from fittrackee.users.decorators import authenticate
from fittrackee.users.models import User from fittrackee.users.models import User
from .models import Workout from .models import Workout
@ -56,7 +56,7 @@ MAX_WORKOUTS_PER_PAGE = 100
@workouts_blueprint.route('/workouts', methods=['GET']) @workouts_blueprint.route('/workouts', methods=['GET'])
@authenticate @require_auth()
def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]: def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
Get workouts for the authenticated user. Get workouts for the authenticated user.
@ -298,7 +298,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
@workouts_blueprint.route( @workouts_blueprint.route(
'/workouts/<string:workout_short_id>', methods=['GET'] '/workouts/<string:workout_short_id>', methods=['GET']
) )
@authenticate @require_auth()
def get_workout( def get_workout(
auth_user: User, workout_short_id: str auth_user: User, workout_short_id: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -462,7 +462,7 @@ def get_workout_data(
@workouts_blueprint.route( @workouts_blueprint.route(
'/workouts/<string:workout_short_id>/gpx', methods=['GET'] '/workouts/<string:workout_short_id>/gpx', methods=['GET']
) )
@authenticate @require_auth()
def get_workout_gpx( def get_workout_gpx(
auth_user: User, workout_short_id: str auth_user: User, workout_short_id: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -512,7 +512,7 @@ def get_workout_gpx(
@workouts_blueprint.route( @workouts_blueprint.route(
'/workouts/<string:workout_short_id>/chart_data', methods=['GET'] '/workouts/<string:workout_short_id>/chart_data', methods=['GET']
) )
@authenticate @require_auth()
def get_workout_chart_data( def get_workout_chart_data(
auth_user: User, workout_short_id: str auth_user: User, workout_short_id: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -582,7 +582,7 @@ def get_workout_chart_data(
'/workouts/<string:workout_short_id>/gpx/segment/<int:segment_id>', '/workouts/<string:workout_short_id>/gpx/segment/<int:segment_id>',
methods=['GET'], methods=['GET'],
) )
@authenticate @require_auth()
def get_segment_gpx( def get_segment_gpx(
auth_user: User, workout_short_id: str, segment_id: int auth_user: User, workout_short_id: str, segment_id: int
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -634,7 +634,7 @@ def get_segment_gpx(
'<int:segment_id>', '<int:segment_id>',
methods=['GET'], methods=['GET'],
) )
@authenticate @require_auth()
def get_segment_chart_data( def get_segment_chart_data(
auth_user: User, workout_short_id: str, segment_id: int auth_user: User, workout_short_id: str, segment_id: int
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -705,7 +705,7 @@ def get_segment_chart_data(
@workouts_blueprint.route( @workouts_blueprint.route(
'/workouts/<string:workout_short_id>/gpx/download', methods=['GET'] '/workouts/<string:workout_short_id>/gpx/download', methods=['GET']
) )
@authenticate @require_auth()
def download_workout_gpx( def download_workout_gpx(
auth_user: User, workout_short_id: str auth_user: User, workout_short_id: str
) -> Union[HttpResponse, Response]: ) -> Union[HttpResponse, Response]:
@ -848,7 +848,7 @@ def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
@workouts_blueprint.route('/workouts', methods=['POST']) @workouts_blueprint.route('/workouts', methods=['POST'])
@authenticate @require_auth()
def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]: def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
""" """
Post an workout with a gpx file Post an workout with a gpx file
@ -1016,7 +1016,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
@workouts_blueprint.route('/workouts/no_gpx', methods=['POST']) @workouts_blueprint.route('/workouts/no_gpx', methods=['POST'])
@authenticate @require_auth()
def post_workout_no_gpx( def post_workout_no_gpx(
auth_user: User, auth_user: User,
) -> Union[Tuple[Dict, int], HttpResponse]: ) -> Union[Tuple[Dict, int], HttpResponse]:
@ -1164,7 +1164,7 @@ def post_workout_no_gpx(
@workouts_blueprint.route( @workouts_blueprint.route(
'/workouts/<string:workout_short_id>', methods=['PATCH'] '/workouts/<string:workout_short_id>', methods=['PATCH']
) )
@authenticate @require_auth()
def update_workout( def update_workout(
auth_user: User, workout_short_id: str auth_user: User, workout_short_id: str
) -> Union[Dict, HttpResponse]: ) -> Union[Dict, HttpResponse]:
@ -1311,7 +1311,7 @@ def update_workout(
@workouts_blueprint.route( @workouts_blueprint.route(
'/workouts/<string:workout_short_id>', methods=['DELETE'] '/workouts/<string:workout_short_id>', methods=['DELETE']
) )
@authenticate @require_auth()
def delete_workout( def delete_workout(
auth_user: User, workout_short_id: str auth_user: User, workout_short_id: str
) -> Union[Tuple[Dict, int], HttpResponse]: ) -> Union[Tuple[Dict, int], HttpResponse]: