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
|
||||
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']
|
Loading…
Reference in New Issue
Block a user