API - add password reset request route - #50
This commit is contained in:
parent
1a689955d3
commit
d6ce02d385
@ -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']
|
Loading…
Reference in New Issue
Block a user