API & Client - resend account confirmation email
This commit is contained in:
3
fittrackee/tests/fixtures/fixtures_users.py
vendored
3
fittrackee/tests/fixtures/fixtures_users.py
vendored
@ -6,6 +6,8 @@ from fittrackee import db
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
from fittrackee.workouts.models import Sport
|
||||
|
||||
from ..utils import random_string
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_1() -> User:
|
||||
@ -97,6 +99,7 @@ def inactive_user() -> User:
|
||||
user = User(
|
||||
username='inactive', email='inactive@example.com', password='12345678'
|
||||
)
|
||||
user.confirmation_token = random_string()
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
@ -2134,3 +2134,129 @@ class TestConfirmationAccount(ApiTestCaseMixin):
|
||||
assert data['message'] == 'account confirmation successful'
|
||||
assert inactive_user.is_active is True
|
||||
assert inactive_user.confirmation_token is None
|
||||
|
||||
|
||||
class TestResendAccountConfirmationEmail(ApiTestCaseMixin):
|
||||
def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None:
|
||||
client = app.test_client()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict()),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
self.assert_400(response)
|
||||
|
||||
def test_it_does_not_return_error_if_account_does_not_exist(
|
||||
self, app: Flask
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=self.random_email())),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'confirmation email resent'
|
||||
|
||||
def test_it_does_not_return_error_if_account_already_active(
|
||||
self, app: Flask, user_1: User
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=user_1.email)),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'confirmation email resent'
|
||||
|
||||
def test_it_does_not_call_account_confirmation_email_if_user_is_active(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
account_confirmation_email_mock: Mock,
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
|
||||
client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=user_1.email)),
|
||||
content_type='application/json',
|
||||
environ_base={'HTTP_USER_AGENT': USER_AGENT},
|
||||
)
|
||||
|
||||
account_confirmation_email_mock.send.assert_not_called()
|
||||
|
||||
def test_it_returns_success_if_user_is_inactive(
|
||||
self, app: Flask, inactive_user: User
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
|
||||
response = client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=inactive_user.email)),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data.decode())
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'confirmation email resent'
|
||||
|
||||
def test_it_updates_token_if_user_is_inactive(
|
||||
self, app: Flask, inactive_user: User
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
previous_token = inactive_user.confirmation_token
|
||||
|
||||
client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=inactive_user.email)),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
assert inactive_user.confirmation_token != previous_token
|
||||
|
||||
def test_it_calls_account_confirmation_email_if_user_is_inactive(
|
||||
self,
|
||||
app: Flask,
|
||||
inactive_user: User,
|
||||
account_confirmation_email_mock: Mock,
|
||||
) -> None:
|
||||
client = app.test_client()
|
||||
expected_token = self.random_string()
|
||||
|
||||
with patch('secrets.token_urlsafe', return_value=expected_token):
|
||||
client.post(
|
||||
'/api/auth/account/resend-confirmation',
|
||||
data=json.dumps(dict(email=inactive_user.email)),
|
||||
content_type='application/json',
|
||||
environ_base={'HTTP_USER_AGENT': USER_AGENT},
|
||||
)
|
||||
|
||||
account_confirmation_email_mock.send.assert_called_once_with(
|
||||
{
|
||||
'language': 'en',
|
||||
'email': inactive_user.email,
|
||||
},
|
||||
{
|
||||
'username': inactive_user.username,
|
||||
'fittrackee_url': 'http://0.0.0.0:5000',
|
||||
'operating_system': 'linux',
|
||||
'browser_name': 'firefox',
|
||||
'account_confirmation_url': (
|
||||
'http://0.0.0.0:5000/account-confirmation'
|
||||
f'?token={expected_token}'
|
||||
),
|
||||
},
|
||||
)
|
||||
|
@ -42,6 +42,25 @@ auth_blueprint = Blueprint('auth', __name__)
|
||||
HEX_COLOR_REGEX = regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
|
||||
|
||||
|
||||
def send_account_confirmation_email(user: User) -> None:
|
||||
ui_url = current_app.config['UI_URL']
|
||||
email_data = {
|
||||
'username': user.username,
|
||||
'fittrackee_url': ui_url,
|
||||
'operating_system': request.user_agent.platform, # type: ignore # noqa
|
||||
'browser_name': request.user_agent.browser, # type: ignore
|
||||
'account_confirmation_url': (
|
||||
f'{ui_url}/account-confirmation'
|
||||
f'?token={user.confirmation_token}'
|
||||
),
|
||||
}
|
||||
user_data = {
|
||||
'language': 'en',
|
||||
'email': user.email,
|
||||
}
|
||||
account_confirmation_email.send(user_data, email_data)
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/register', methods=['POST'])
|
||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
"""
|
||||
@ -59,11 +78,11 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- successful registration
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 CREATED
|
||||
HTTP/1.1 200 SUCCESS
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@ -86,7 +105,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
:<json string email: user email
|
||||
:<json string password: password (8 characters required)
|
||||
|
||||
:statuscode 201: successfully registered
|
||||
:statuscode 200: success
|
||||
:statuscode 400:
|
||||
- invalid payload
|
||||
- sorry, that username is already taken
|
||||
@ -105,7 +124,6 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
if not current_app.config.get('is_registration_enabled'):
|
||||
return ForbiddenErrorResponse('error, registration is disabled')
|
||||
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
if (
|
||||
not post_data
|
||||
@ -143,26 +161,11 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
||||
if not user:
|
||||
new_user = User(username=username, email=email, password=password)
|
||||
new_user.timezone = 'Europe/Paris'
|
||||
new_user.confirmation_token = secrets.token_urlsafe(16)
|
||||
new_user.confirmation_token = secrets.token_urlsafe(30)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
ui_url = current_app.config['UI_URL']
|
||||
email_data = {
|
||||
'username': new_user.username,
|
||||
'fittrackee_url': ui_url,
|
||||
'operating_system': request.user_agent.platform, # type: ignore # noqa
|
||||
'browser_name': request.user_agent.browser, # type: ignore
|
||||
'account_confirmation_url': (
|
||||
f'{ui_url}/account-confirmation'
|
||||
f'?token={new_user.confirmation_token}'
|
||||
),
|
||||
}
|
||||
user_data = {
|
||||
'language': 'en',
|
||||
'email': new_user.email,
|
||||
}
|
||||
account_confirmation_email.send(user_data, email_data)
|
||||
send_account_confirmation_email(new_user)
|
||||
|
||||
return {'status': 'success'}, 200
|
||||
# handler errors
|
||||
@ -634,7 +637,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
if email_to_confirm != auth_user.email:
|
||||
if is_valid_email(email_to_confirm):
|
||||
auth_user.email_to_confirm = email_to_confirm
|
||||
auth_user.confirmation_token = secrets.token_urlsafe(16)
|
||||
auth_user.confirmation_token = secrets.token_urlsafe(30)
|
||||
else:
|
||||
error_messages = 'email: valid email must be provided\n'
|
||||
|
||||
@ -1393,3 +1396,54 @@ def confirm_account() -> Union[Dict, HttpResponse]:
|
||||
|
||||
except (exc.OperationalError, ValueError) as e:
|
||||
return handle_error_and_return_response(e, db=db)
|
||||
|
||||
|
||||
@auth_blueprint.route('/auth/account/resend-confirmation', methods=['POST'])
|
||||
def resend_account_confirmation_email() -> Union[Dict, HttpResponse]:
|
||||
"""
|
||||
resend email with instructions to confirm account
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/auth/account/resend-confirmation HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "confirmation email resent",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json string email: user email
|
||||
|
||||
:statuscode 200: confirmation email resent
|
||||
: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:
|
||||
return InvalidPayloadErrorResponse()
|
||||
email = post_data.get('email')
|
||||
|
||||
try:
|
||||
user = User.query.filter_by(email=email, is_active=False).first()
|
||||
if user:
|
||||
user.confirmation_token = secrets.token_urlsafe(30)
|
||||
db.session.commit()
|
||||
|
||||
send_account_confirmation_email(user)
|
||||
|
||||
response = {
|
||||
'status': 'success',
|
||||
'message': 'confirmation email resent',
|
||||
}
|
||||
return response
|
||||
except (exc.OperationalError, ValueError) as e:
|
||||
return handle_error_and_return_response(e, db=db)
|
||||
|
@ -1,5 +1,4 @@
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
import shutil
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
@ -517,7 +516,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
'reset_password' in user_data
|
||||
and user_data['reset_password'] is True
|
||||
):
|
||||
new_password = secrets.token_urlsafe(random.randint(16, 20))
|
||||
new_password = secrets.token_urlsafe(30)
|
||||
user.password = bcrypt.generate_password_hash(
|
||||
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||
).decode()
|
||||
@ -526,7 +525,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
||||
if 'new_email' in user_data:
|
||||
if is_valid_email(user_data['new_email']):
|
||||
user.email_to_confirm = user_data['new_email']
|
||||
user.confirmation_token = secrets.token_urlsafe(16)
|
||||
user.confirmation_token = secrets.token_urlsafe(30)
|
||||
send_new_address_email = True
|
||||
else:
|
||||
return InvalidPayloadErrorResponse(
|
||||
|
Reference in New Issue
Block a user