API - init user email update (WIP)
This commit is contained in:
parent
6c6b4b0ea4
commit
d64579bfa3
@ -0,0 +1,46 @@
|
|||||||
|
"""update User table
|
||||||
|
|
||||||
|
Revision ID: 5e3a3a31c432
|
||||||
|
Revises: e30007d681cb
|
||||||
|
Create Date: 2022-02-23 11:05:24.223304
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '5e3a3a31c432'
|
||||||
|
down_revision = 'e30007d681cb'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.alter_column(
|
||||||
|
'users', 'username', existing_type=sa.String(length=20),
|
||||||
|
type_=sa.String(length=255), existing_nullable=False
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
'users', 'email', existing_type=sa.String(length=120),
|
||||||
|
type_=sa.String(length=255), existing_nullable=False
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
'users',
|
||||||
|
sa.Column('email_to_confirm', sa.String(length=255), nullable=True))
|
||||||
|
op.add_column(
|
||||||
|
'users',
|
||||||
|
sa.Column('confirmation_token', sa.String(length=255), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('users', 'confirmation_token')
|
||||||
|
op.drop_column('users', 'email_to_confirm')
|
||||||
|
op.alter_column(
|
||||||
|
'users', 'email', existing_type=sa.String(length=255),
|
||||||
|
type_=sa.String(length=120), existing_nullable=False
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
'users', 'username', existing_type=sa.String(length=255),
|
||||||
|
type_=sa.String(length=20), existing_nullable=False
|
||||||
|
)
|
@ -1,30 +0,0 @@
|
|||||||
"""update username length
|
|
||||||
|
|
||||||
Revision ID: 5e3a3a31c432
|
|
||||||
Revises: e30007d681cb
|
|
||||||
Create Date: 2022-02-23 11:05:24.223304
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '5e3a3a31c432'
|
|
||||||
down_revision = 'e30007d681cb'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.alter_column(
|
|
||||||
'users', 'username', existing_type=sa.String(length=20),
|
|
||||||
type_=sa.String(length=255), existing_nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.alter_column(
|
|
||||||
'users', 'username', existing_type=sa.String(length=255),
|
|
||||||
type_=sa.String(length=20), existing_nullable=False
|
|
||||||
)
|
|
@ -593,7 +593,7 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_400(response, error_message='current password is missing')
|
self.assert_400(response)
|
||||||
|
|
||||||
def test_it_returns_error_if_current_password_is_missing(
|
def test_it_returns_error_if_current_password_is_missing(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
@ -605,12 +605,38 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
response = client.patch(
|
response = client.patch(
|
||||||
'/api/auth/profile/edit/account',
|
'/api/auth/profile/edit/account',
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
data=json.dumps(dict(new_password=random_string())),
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
|
new_password=random_string(),
|
||||||
|
)
|
||||||
|
),
|
||||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_400(response, error_message='current password is missing')
|
self.assert_400(response, error_message='current password is missing')
|
||||||
|
|
||||||
|
def test_it_returns_error_if_email_is_missing(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
'/api/auth/profile/edit/account',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
password='12345678',
|
||||||
|
new_password=random_string(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response, 'email is missing')
|
||||||
|
|
||||||
def test_it_returns_error_if_current_password_is_invalid(
|
def test_it_returns_error_if_current_password_is_invalid(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -623,6 +649,7 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
dict(
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
password=random_string(),
|
password=random_string(),
|
||||||
new_password=random_string(),
|
new_password=random_string(),
|
||||||
)
|
)
|
||||||
@ -632,6 +659,129 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_401(response, error_message='invalid credentials')
|
self.assert_401(response, error_message='invalid credentials')
|
||||||
|
|
||||||
|
def test_it_does_not_returns_error_if_no_new_password_provided(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
'/api/auth/profile/edit/account',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
|
password='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert data['message'] == 'user account updated'
|
||||||
|
|
||||||
|
def test_it_does_not_update_user_account_if_no_new_password_provided(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
current_hashed_password = user_1.password
|
||||||
|
current_email = user_1.email
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
'/api/auth/profile/edit/account',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
|
password='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert current_email == user_1.email
|
||||||
|
assert current_hashed_password == user_1.password
|
||||||
|
|
||||||
|
def test_it_returns_error_if_new_email_is_invalid(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
'/api/auth/profile/edit/account',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=random_string(),
|
||||||
|
password='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response, 'email: valid email must be provided\n')
|
||||||
|
|
||||||
|
def test_it_does_not_update_email_if_new_email_provided(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
current_email = user_1.email
|
||||||
|
new_email = 'new.email@example.com'
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
'/api/auth/profile/edit/account',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=new_email,
|
||||||
|
password='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert current_email == user_1.email
|
||||||
|
assert new_email == user_1.email_to_confirm
|
||||||
|
assert user_1.confirmation_token is not None
|
||||||
|
|
||||||
|
def test_it_updates_email_to_confirm_when_new_email_provided(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
previous_confirmation_token = random_string()
|
||||||
|
user_1.email_to_confirm = 'new.email@example.com'
|
||||||
|
user_1.confirmation_token = previous_confirmation_token
|
||||||
|
new_email = 'another.email@example.com'
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
'/api/auth/profile/edit/account',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
email=new_email,
|
||||||
|
password='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert new_email == user_1.email_to_confirm
|
||||||
|
assert user_1.confirmation_token != previous_confirmation_token
|
||||||
|
|
||||||
def test_it_returns_error_if_controls_fail_on_new_password(
|
def test_it_returns_error_if_controls_fail_on_new_password(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -644,6 +794,7 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
dict(
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
password='12345678',
|
password='12345678',
|
||||||
new_password=random_string(length=3),
|
new_password=random_string(length=3),
|
||||||
)
|
)
|
||||||
@ -666,6 +817,7 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
dict(
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
password='12345678',
|
password='12345678',
|
||||||
new_password=random_string(),
|
new_password=random_string(),
|
||||||
)
|
)
|
||||||
@ -689,6 +841,7 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
dict(
|
dict(
|
||||||
|
email=user_1.email,
|
||||||
password='12345678',
|
password='12345678',
|
||||||
new_password=new_password,
|
new_password=new_password,
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import secrets
|
||||||
from typing import Dict, Tuple, Union
|
from typing import Dict, Tuple, Union
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
@ -27,7 +28,7 @@ from fittrackee.workouts.models import Sport
|
|||||||
|
|
||||||
from .decorators import authenticate
|
from .decorators import authenticate
|
||||||
from .models import User, UserSportPreference
|
from .models import User, UserSportPreference
|
||||||
from .utils.controls import check_password, register_controls
|
from .utils.controls import check_password, is_valid_email, register_controls
|
||||||
from .utils.token import decode_user_token
|
from .utils.token import decode_user_token
|
||||||
|
|
||||||
auth_blueprint = Blueprint('auth', __name__)
|
auth_blueprint = Blueprint('auth', __name__)
|
||||||
@ -546,7 +547,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
@authenticate
|
@authenticate
|
||||||
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
update authenticated user password
|
update authenticated user email and password
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -630,48 +631,67 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
"status": "success"
|
"status": "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
:<json string password: user password
|
:<json string email: user email
|
||||||
|
:<json string password: user current password
|
||||||
|
:<json string new_password: user new password
|
||||||
|
|
||||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||||
|
|
||||||
:statuscode 200: user account updated
|
:statuscode 200: user account updated
|
||||||
:statuscode 400:
|
:statuscode 400:
|
||||||
- invalid payload
|
- invalid payload
|
||||||
|
- email is missing
|
||||||
|
- current password is missing
|
||||||
|
- email: valid email must be provided
|
||||||
- password: 8 characters required
|
- password: 8 characters required
|
||||||
:statuscode 401:
|
:statuscode 401:
|
||||||
- provide a valid auth token
|
- provide a valid auth token
|
||||||
- signature expired, please log in again
|
- signature expired, please log in again
|
||||||
- invalid token, please log in again
|
- invalid token, please log in again
|
||||||
|
- invalid credentials
|
||||||
:statuscode 500: error, please try again or contact the administrator
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or not data.get('password'):
|
if not data:
|
||||||
return InvalidPayloadErrorResponse('current password is missing')
|
return InvalidPayloadErrorResponse()
|
||||||
|
email_to_confirm = data.get('email')
|
||||||
|
if not email_to_confirm:
|
||||||
|
return InvalidPayloadErrorResponse('email is missing')
|
||||||
current_password = data.get('password')
|
current_password = data.get('password')
|
||||||
|
if not current_password:
|
||||||
|
return InvalidPayloadErrorResponse('current password is missing')
|
||||||
if not bcrypt.check_password_hash(auth_user.password, current_password):
|
if not bcrypt.check_password_hash(auth_user.password, current_password):
|
||||||
return UnauthorizedErrorResponse('invalid credentials')
|
return UnauthorizedErrorResponse('invalid credentials')
|
||||||
|
|
||||||
new_password = data.get('new_password')
|
new_password = data.get('new_password')
|
||||||
message = check_password(new_password)
|
error_messages = ''
|
||||||
if message != '':
|
try:
|
||||||
return InvalidPayloadErrorResponse(message)
|
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)
|
||||||
|
else:
|
||||||
|
error_messages = 'email: valid email must be provided\n'
|
||||||
|
|
||||||
|
if new_password is not None:
|
||||||
|
error_messages += check_password(new_password)
|
||||||
|
if error_messages == '':
|
||||||
hashed_password = bcrypt.generate_password_hash(
|
hashed_password = bcrypt.generate_password_hash(
|
||||||
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||||
).decode()
|
).decode()
|
||||||
|
|
||||||
try:
|
|
||||||
auth_user.password = hashed_password
|
auth_user.password = hashed_password
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
|
if error_messages != '':
|
||||||
|
return InvalidPayloadErrorResponse(error_messages)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
return {
|
return {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'user account updated',
|
'message': 'user account updated',
|
||||||
'data': auth_user.serialize(),
|
'data': auth_user.serialize(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# handler errors
|
|
||||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||||
return handle_error_and_return_response(e, db=db)
|
return handle_error_and_return_response(e, db=db)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class User(BaseModel):
|
|||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
username = db.Column(db.String(255), unique=True, nullable=False)
|
username = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
password = db.Column(db.String(255), nullable=False)
|
password = db.Column(db.String(255), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, nullable=False)
|
created_at = db.Column(db.DateTime, nullable=False)
|
||||||
admin = db.Column(db.Boolean, default=False, nullable=False)
|
admin = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
@ -45,6 +45,8 @@ class User(BaseModel):
|
|||||||
)
|
)
|
||||||
language = db.Column(db.String(50), nullable=True)
|
language = db.Column(db.String(50), nullable=True)
|
||||||
imperial_units = db.Column(db.Boolean, default=False, nullable=False)
|
imperial_units = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
|
email_to_confirm = db.Column(db.String(255), nullable=True)
|
||||||
|
confirmation_token = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<User {self.username!r}>'
|
return f'<User {self.username!r}>'
|
||||||
|
Loading…
Reference in New Issue
Block a user