API - init resource protector (that also handles current authentication)
This commit is contained in:
parent
eeae632b01
commit
44c16f6805
@ -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
|
||||||
|
@ -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())
|
||||||
|
80
fittrackee/oauth2/resource_protector.py
Normal file
80
fittrackee/oauth2/resource_protector.py
Normal 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
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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]:
|
||||||
|
@ -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
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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]:
|
||||||
|
Loading…
Reference in New Issue
Block a user