API - add password reset request route - #50
This commit is contained in:
		@@ -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'}
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :<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
 | 
			
		||||
from fittrackee_api import bcrypt, db
 | 
			
		||||
@@ -8,6 +8,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
 | 
			
		||||
from sqlalchemy.sql.expression import select
 | 
			
		||||
 | 
			
		||||
from ..activities.models import Activity
 | 
			
		||||
from .utils_token import decode_user_token, get_user_token
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(db.Model):
 | 
			
		||||
@@ -39,7 +40,7 @@ class User(db.Model):
 | 
			
		||||
        return f'<User {self.username!r}>'
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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