API - add password reset request route - #50
This commit is contained in:
		| @@ -12,6 +12,7 @@ class BaseConfig: | |||||||
|     BCRYPT_LOG_ROUNDS = 13 |     BCRYPT_LOG_ROUNDS = 13 | ||||||
|     TOKEN_EXPIRATION_DAYS = 30 |     TOKEN_EXPIRATION_DAYS = 30 | ||||||
|     TOKEN_EXPIRATION_SECONDS = 0 |     TOKEN_EXPIRATION_SECONDS = 0 | ||||||
|  |     PASSWORD_TOKEN_EXPIRATION_SECONDS = 3600 | ||||||
|     UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads') |     UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads') | ||||||
|     PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'} |     PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'} | ||||||
|     ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'} |     ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'} | ||||||
|   | |||||||
| @@ -912,3 +912,60 @@ class TestRegistrationConfiguration: | |||||||
|             content_type='application/json', |             content_type='application/json', | ||||||
|         ) |         ) | ||||||
|         assert response.status_code == 201 |         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' | ||||||
|   | |||||||
| @@ -655,3 +655,50 @@ def del_picture(auth_user_id): | |||||||
|             'message': 'Error during picture deletion.', |             'message': 'Error during picture deletion.', | ||||||
|         } |         } | ||||||
|         return jsonify(response_object), 500 |         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" | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |     :<json string email: user email | ||||||
|  |  | ||||||
|  |     :statuscode 200: Password reset request processed. | ||||||
|  |     :statuscode 400: Invalid payload. | ||||||
|  |     :statuscode 500: Error. Please try again or contact the administrator. | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     post_data = request.get_json() | ||||||
|  |     if not post_data or post_data.get('email') is None: | ||||||
|  |         response_object = {'status': 'error', 'message': 'Invalid payload.'} | ||||||
|  |         return jsonify(response_object), 400 | ||||||
|  |     email = post_data.get('email') | ||||||
|  |  | ||||||
|  |     user = User.query.filter(User.email == email).first() | ||||||
|  |     if user: | ||||||
|  |         password_reset_token = user.encode_auth_token(user.id) | ||||||
|  |     response_object = { | ||||||
|  |         'status': 'success', | ||||||
|  |         'message': 'Password reset request processed.', | ||||||
|  |     } | ||||||
|  |     return jsonify(response_object), 200 | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| import jwt | import jwt | ||||||
| from fittrackee_api import bcrypt, db | from fittrackee_api import bcrypt, db | ||||||
| @@ -8,6 +8,7 @@ from sqlalchemy.ext.hybrid import hybrid_property | |||||||
| from sqlalchemy.sql.expression import select | from sqlalchemy.sql.expression import select | ||||||
|  |  | ||||||
| from ..activities.models import Activity | from ..activities.models import Activity | ||||||
|  | from .utils_token import decode_user_token, get_user_token | ||||||
|  |  | ||||||
|  |  | ||||||
| class User(db.Model): | class User(db.Model): | ||||||
| @@ -39,7 +40,7 @@ class User(db.Model): | |||||||
|         return f'<User {self.username!r}>' |         return f'<User {self.username!r}>' | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, username, email, password, created_at=datetime.datetime.utcnow() |         self, username, email, password, created_at=datetime.utcnow() | ||||||
|     ): |     ): | ||||||
|         self.username = username |         self.username = username | ||||||
|         self.email = email |         self.email = email | ||||||
| @@ -56,20 +57,19 @@ class User(db.Model): | |||||||
|         :return: JWToken |         :return: JWToken | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             payload = { |             return get_user_token(user_id) | ||||||
|                 'exp': datetime.datetime.utcnow() |         except Exception as e: | ||||||
|                 + datetime.timedelta( |             return e | ||||||
|                     days=current_app.config.get('TOKEN_EXPIRATION_DAYS'), |  | ||||||
|                     seconds=current_app.config.get('TOKEN_EXPIRATION_SECONDS'), |     @staticmethod | ||||||
|                 ), |     def encode_password_reset_token(user_id): | ||||||
|                 'iat': datetime.datetime.utcnow(), |         """ | ||||||
|                 'sub': user_id, |         Generates the auth token | ||||||
|             } |         :param user_id: - | ||||||
|             return jwt.encode( |         :return: JWToken | ||||||
|                 payload, |         """ | ||||||
|                 current_app.config.get('SECRET_KEY'), |         try: | ||||||
|                 algorithm='HS256', |             return get_user_token(user_id, password_reset=True) | ||||||
|             ) |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             return e |             return e | ||||||
|  |  | ||||||
| @@ -81,10 +81,7 @@ class User(db.Model): | |||||||
|         :return: integer|string |         :return: integer|string | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             payload = jwt.decode( |             return decode_user_token(auth_token) | ||||||
|                 auth_token, current_app.config.get('SECRET_KEY') |  | ||||||
|             ) |  | ||||||
|             return payload['sub'] |  | ||||||
|         except jwt.ExpiredSignatureError: |         except jwt.ExpiredSignatureError: | ||||||
|             return 'Signature expired. Please log in again.' |             return 'Signature expired. Please log in again.' | ||||||
|         except jwt.InvalidTokenError: |         except jwt.InvalidTokenError: | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								fittrackee_api/fittrackee_api/users/utils_token.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								fittrackee_api/fittrackee_api/users/utils_token.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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'] | ||||||
		Reference in New Issue
	
	Block a user