API - add password reset request route - #50

This commit is contained in:
Sam 2020-05-10 17:08:18 +02:00
parent 1a689955d3
commit d6ce02d385
5 changed files with 153 additions and 20 deletions

View File

@ -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'}

View File

@ -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'

View File

@ -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

View File

@ -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:

View 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']