diff --git a/fittrackee_api/fittrackee_api/config.py b/fittrackee_api/fittrackee_api/config.py index 8b4c8f25..b482611f 100644 --- a/fittrackee_api/fittrackee_api/config.py +++ b/fittrackee_api/fittrackee_api/config.py @@ -12,6 +12,7 @@ class BaseConfig: BCRYPT_LOG_ROUNDS = 13 TOKEN_EXPIRATION_DAYS = 30 TOKEN_EXPIRATION_SECONDS = 0 + PASSWORD_TOKEN_EXPIRATION_SECONDS = 3600 UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads') PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'} ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'} diff --git a/fittrackee_api/fittrackee_api/tests/test_auth_api.py b/fittrackee_api/fittrackee_api/tests/test_auth_api.py index 4b1909d8..c144832c 100644 --- a/fittrackee_api/fittrackee_api/tests/test_auth_api.py +++ b/fittrackee_api/fittrackee_api/tests/test_auth_api.py @@ -912,3 +912,60 @@ class TestRegistrationConfiguration: content_type='application/json', ) assert response.status_code == 201 + + +class TestPasswordResetRequest: + def test_it_requests_password_reset_when_user_exists(self, app, user_1): + client = app.test_client() + response = client.post( + '/api/auth/password-reset/request', + data=json.dumps(dict(email='test@test.com')), + content_type='application/json', + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + assert data['status'] == 'success' + assert data['message'] == 'Password reset request processed.' + + def test_it_does_not_return_error_when_user_does_not_exist(self, app): + client = app.test_client() + + response = client.post( + '/api/auth/password-reset/request', + data=json.dumps(dict(email='test@test.com')), + content_type='application/json', + ) + + assert response.status_code == 200 + data = json.loads(response.data.decode()) + assert data['status'] == 'success' + assert data['message'] == 'Password reset request processed.' + + def test_it_returns_error_on_invalid_payload(self, app): + client = app.test_client() + + response = client.post( + '/api/auth/password-reset/request', + data=json.dumps(dict(usernmae='test')), + content_type='application/json', + ) + + assert response.status_code == 400 + data = json.loads(response.data.decode()) + assert data['message'] == 'Invalid payload.' + assert data['status'] == 'error' + + def test_it_returns_error_on_empty_payload(self, app): + client = app.test_client() + + response = client.post( + '/api/auth/password-reset/request', + data=json.dumps(dict()), + content_type='application/json', + ) + + assert response.status_code == 400 + data = json.loads(response.data.decode()) + assert data['message'] == 'Invalid payload.' + assert data['status'] == 'error' diff --git a/fittrackee_api/fittrackee_api/users/auth.py b/fittrackee_api/fittrackee_api/users/auth.py index 06c980bf..88488ca0 100644 --- a/fittrackee_api/fittrackee_api/users/auth.py +++ b/fittrackee_api/fittrackee_api/users/auth.py @@ -655,3 +655,50 @@ def del_picture(auth_user_id): 'message': 'Error during picture deletion.', } return jsonify(response_object), 500 + + +@auth_blueprint.route('/auth/password-reset/request', methods=['POST']) +def request_password_reset(): + """ + handle password reset request + + **Example request**: + + .. sourcecode:: http + + POST /api/auth/password-reset/request HTTP/1.1 + Content-Type: application/json + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "message": "Password reset request processed.", + "status": "success" + } + + :' def __init__( - self, username, email, password, created_at=datetime.datetime.utcnow() + self, username, email, password, created_at=datetime.utcnow() ): self.username = username self.email = email @@ -56,20 +57,19 @@ class User(db.Model): :return: JWToken """ try: - payload = { - 'exp': datetime.datetime.utcnow() - + datetime.timedelta( - days=current_app.config.get('TOKEN_EXPIRATION_DAYS'), - seconds=current_app.config.get('TOKEN_EXPIRATION_SECONDS'), - ), - 'iat': datetime.datetime.utcnow(), - 'sub': user_id, - } - return jwt.encode( - payload, - current_app.config.get('SECRET_KEY'), - algorithm='HS256', - ) + return get_user_token(user_id) + except Exception as e: + return e + + @staticmethod + def encode_password_reset_token(user_id): + """ + Generates the auth token + :param user_id: - + :return: JWToken + """ + try: + return get_user_token(user_id, password_reset=True) except Exception as e: return e @@ -81,10 +81,7 @@ class User(db.Model): :return: integer|string """ try: - payload = jwt.decode( - auth_token, current_app.config.get('SECRET_KEY') - ) - return payload['sub'] + return decode_user_token(auth_token) except jwt.ExpiredSignatureError: return 'Signature expired. Please log in again.' except jwt.InvalidTokenError: diff --git a/fittrackee_api/fittrackee_api/users/utils_token.py b/fittrackee_api/fittrackee_api/users/utils_token.py new file mode 100644 index 00000000..12c5f656 --- /dev/null +++ b/fittrackee_api/fittrackee_api/users/utils_token.py @@ -0,0 +1,31 @@ +from datetime import datetime, timedelta + +import jwt +from flask import current_app + + +def get_user_token(user_id, password_reset=False): + expiration_days = ( + 0 + if password_reset + else current_app.config.get('TOKEN_EXPIRATION_DAYS') + ) + expiration_seconds = ( + current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS') + if password_reset + else current_app.config.get('TOKEN_EXPIRATION_SECONDS') + ) + payload = { + 'exp': datetime.utcnow() + + timedelta(days=expiration_days, seconds=expiration_seconds), + 'iat': datetime.utcnow(), + 'sub': user_id, + } + return jwt.encode( + payload, current_app.config.get('SECRET_KEY'), algorithm='HS256', + ) + + +def decode_user_token(auth_token): + payload = jwt.decode(auth_token, current_app.config.get('SECRET_KEY')) + return payload['sub']