From fc43fcd6bf2ad147d2448783cd2d7b64294b5e5a Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 16 Feb 2022 18:07:05 +0100 Subject: [PATCH] API - users refactoring --- fittrackee/tests/users/test_auth_api.py | 2 +- fittrackee/tests/users/test_users_utils.py | 6 +- fittrackee/users/auth.py | 4 +- fittrackee/users/decorators.py | 2 +- fittrackee/users/models.py | 2 +- fittrackee/users/users.py | 2 +- fittrackee/users/utils/__init__.py | 0 fittrackee/users/utils/admin.py | 12 +++ fittrackee/users/utils/controls.py | 88 +++++++++++++++++++ .../users/{utils_token.py => utils/token.py} | 0 fittrackee/workouts/utils/visibility.py | 14 +++ fittrackee/workouts/workouts.py | 2 +- 12 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 fittrackee/users/utils/__init__.py create mode 100644 fittrackee/users/utils/admin.py create mode 100644 fittrackee/users/utils/controls.py rename fittrackee/users/{utils_token.py => utils/token.py} (100%) create mode 100644 fittrackee/workouts/utils/visibility.py diff --git a/fittrackee/tests/users/test_auth_api.py b/fittrackee/tests/users/test_auth_api.py index bdf1f16f..75241be2 100644 --- a/fittrackee/tests/users/test_auth_api.py +++ b/fittrackee/tests/users/test_auth_api.py @@ -8,7 +8,7 @@ from flask import Flask from freezegun import freeze_time from fittrackee.users.models import User, UserSportPreference -from fittrackee.users.utils_token import get_user_token +from fittrackee.users.utils.token import get_user_token from fittrackee.workouts.models import Sport, Workout from ..api_test_case import ApiTestCaseMixin diff --git a/fittrackee/tests/users/test_users_utils.py b/fittrackee/tests/users/test_users_utils.py index 1c83788d..d5c9dd24 100644 --- a/fittrackee/tests/users/test_users_utils.py +++ b/fittrackee/tests/users/test_users_utils.py @@ -5,12 +5,12 @@ from flask import Flask from fittrackee.users.exceptions import UserNotFoundException from fittrackee.users.models import User -from fittrackee.users.utils import ( +from fittrackee.users.utils.admin import set_admin_rights +from fittrackee.users.utils.controls import ( check_passwords, check_username, is_valid_email, register_controls, - set_admin_rights, ) from ..utils import random_string @@ -163,7 +163,7 @@ class TestIsUsernameValid: class TestRegisterControls: - module_path = 'fittrackee.users.utils.' + module_path = 'fittrackee.users.utils.controls.' valid_username = random_string() valid_email = f'{random_string()}@example.com' valid_password = random_string() diff --git a/fittrackee/users/auth.py b/fittrackee/users/auth.py index 98564c3d..5855cf12 100644 --- a/fittrackee/users/auth.py +++ b/fittrackee/users/auth.py @@ -27,8 +27,8 @@ from fittrackee.workouts.models import Sport from .decorators import authenticate from .models import User, UserSportPreference -from .utils import check_passwords, register_controls -from .utils_token import decode_user_token +from .utils.controls import check_passwords, register_controls +from .utils.token import decode_user_token auth_blueprint = Blueprint('auth', __name__) diff --git a/fittrackee/users/decorators.py b/fittrackee/users/decorators.py index 6430071f..e3215b6c 100644 --- a/fittrackee/users/decorators.py +++ b/fittrackee/users/decorators.py @@ -5,7 +5,7 @@ from flask import request from fittrackee.responses import HttpResponse -from .utils import verify_user +from .utils.controls import verify_user def verify_auth_user( diff --git a/fittrackee/users/models.py b/fittrackee/users/models.py index 5e4176e3..e1599dab 100644 --- a/fittrackee/users/models.py +++ b/fittrackee/users/models.py @@ -11,7 +11,7 @@ from sqlalchemy.sql.expression import select from fittrackee import bcrypt, db from fittrackee.workouts.models import Workout -from .utils_token import decode_user_token, get_user_token +from .utils.token import decode_user_token, get_user_token BaseModel: DeclarativeMeta = db.Model diff --git a/fittrackee/users/users.py b/fittrackee/users/users.py index 4fc4ee93..b321c866 100644 --- a/fittrackee/users/users.py +++ b/fittrackee/users/users.py @@ -21,7 +21,7 @@ from fittrackee.workouts.models import Record, Workout, WorkoutSegment from .decorators import authenticate, authenticate_as_admin from .exceptions import UserNotFoundException from .models import User, UserSportPreference -from .utils import set_admin_rights +from .utils.admin import set_admin_rights users_blueprint = Blueprint('users', __name__) diff --git a/fittrackee/users/utils/__init__.py b/fittrackee/users/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fittrackee/users/utils/admin.py b/fittrackee/users/utils/admin.py new file mode 100644 index 00000000..0038ef5a --- /dev/null +++ b/fittrackee/users/utils/admin.py @@ -0,0 +1,12 @@ +from fittrackee import db + +from ..exceptions import UserNotFoundException +from ..models import User + + +def set_admin_rights(username: str) -> None: + user = User.query.filter_by(username=username).first() + if not user: + raise UserNotFoundException() + user.admin = True + db.session.commit() diff --git a/fittrackee/users/utils/controls.py b/fittrackee/users/utils/controls.py new file mode 100644 index 00000000..cd3a9c90 --- /dev/null +++ b/fittrackee/users/utils/controls.py @@ -0,0 +1,88 @@ +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: + """ + Return if email format is valid + """ + mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" + return re.match(mail_pattern, email) is not None + + +def check_passwords(password: str, password_conf: str) -> str: + """ + Verify if password and password confirmation are the same and have + more than 8 characters + + If not, it returns not empty string + """ + ret = '' + if password_conf != password: + ret = 'password: password and password confirmation do not match\n' + if len(password) < 8: + ret += 'password: 8 characters required\n' + return ret + + +def check_username(username: str) -> str: + """ + Return if username is valid + """ + ret = '' + if not 2 < len(username) < 13: + ret += 'username: 3 to 12 characters required\n' + if not re.match(r'^[a-zA-Z0-9_]+$', username): + ret += ( + 'username: only alphanumeric characters and the ' + 'underscore character "_" allowed\n' + ) + return ret + + +def register_controls( + username: str, email: str, password: str, password_conf: str +) -> str: + """ + Verify if username, email and passwords are valid + + If not, it returns not empty string + """ + ret = check_username(username) + if not is_valid_email(email): + ret += 'email: valid email must be provided\n' + ret += check_passwords(password, password_conf) + 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 and user has + admin rights if 'verify_admin' is True + """ + 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: + return UnauthorizedErrorResponse(default_message), None + if verify_admin and not user.admin: + return ForbiddenErrorResponse(), None + return None, user diff --git a/fittrackee/users/utils_token.py b/fittrackee/users/utils/token.py similarity index 100% rename from fittrackee/users/utils_token.py rename to fittrackee/users/utils/token.py diff --git a/fittrackee/workouts/utils/visibility.py b/fittrackee/workouts/utils/visibility.py new file mode 100644 index 00000000..f4a2ff46 --- /dev/null +++ b/fittrackee/workouts/utils/visibility.py @@ -0,0 +1,14 @@ +from typing import Optional + +from fittrackee.responses import ForbiddenErrorResponse, HttpResponse + + +def can_view_workout( + auth_user_id: int, workout_user_id: int +) -> Optional[HttpResponse]: + """ + Return error response if user has no right to view workout + """ + if auth_user_id != workout_user_id: + return ForbiddenErrorResponse() + return None diff --git a/fittrackee/workouts/workouts.py b/fittrackee/workouts/workouts.py index 41f8d8b1..d747c128 100644 --- a/fittrackee/workouts/workouts.py +++ b/fittrackee/workouts/workouts.py @@ -30,7 +30,6 @@ from fittrackee.responses import ( ) from fittrackee.users.decorators import authenticate from fittrackee.users.models import User -from fittrackee.users.utils import can_view_workout from .models import Workout from .utils.convert import convert_in_duration @@ -40,6 +39,7 @@ from .utils.gpx import ( get_chart_data, ) from .utils.short_id import decode_short_id +from .utils.visibility import can_view_workout from .utils.workouts import ( WorkoutException, create_workout,