API - init user account activation
This commit is contained in:
		@@ -6,12 +6,13 @@ from typing import Dict, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import jwt
 | 
			
		||||
from flask import Blueprint, current_app, request
 | 
			
		||||
from sqlalchemy import exc, func, or_
 | 
			
		||||
from sqlalchemy import exc, func
 | 
			
		||||
from werkzeug.exceptions import RequestEntityTooLarge
 | 
			
		||||
from werkzeug.utils import secure_filename
 | 
			
		||||
 | 
			
		||||
from fittrackee import appLog, bcrypt, db
 | 
			
		||||
from fittrackee.emails.tasks import (
 | 
			
		||||
    account_confirmation_email,
 | 
			
		||||
    email_updated_to_current_address,
 | 
			
		||||
    email_updated_to_new_address,
 | 
			
		||||
    password_change_email,
 | 
			
		||||
@@ -46,6 +47,9 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    register a user
 | 
			
		||||
 | 
			
		||||
    The newly created account is inactive. The user must confirm his email
 | 
			
		||||
    to activate it.
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
@@ -63,8 +67,6 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "auth_token": "JSON Web Token",
 | 
			
		||||
        "message": "successfully registered",
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -76,18 +78,18 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "message": "Errors: email: valid email must be provided\\n",
 | 
			
		||||
        "message": "Errors: email: valid email must be provided\n",
 | 
			
		||||
        "status": "error"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :<json string username: user name (3 to 30 characters required)
 | 
			
		||||
    :<json string username: username (3 to 30 characters required)
 | 
			
		||||
    :<json string email: user email
 | 
			
		||||
    :<json string password: password (8 characters required)
 | 
			
		||||
 | 
			
		||||
    :statuscode 201: successfully registered
 | 
			
		||||
    :statuscode 400:
 | 
			
		||||
        - invalid payload
 | 
			
		||||
        - sorry, that user already exists
 | 
			
		||||
        - sorry, that username is already taken
 | 
			
		||||
        - Errors:
 | 
			
		||||
            - username: 3 to 30 characters required
 | 
			
		||||
            - username: only alphanumeric characters and the underscore
 | 
			
		||||
@@ -125,30 +127,44 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
        return InvalidPayloadErrorResponse(ret)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        # check for existing user
 | 
			
		||||
        user = User.query.filter(
 | 
			
		||||
            or_(
 | 
			
		||||
                func.lower(User.username) == func.lower(username),
 | 
			
		||||
                func.lower(User.email) == func.lower(email),
 | 
			
		||||
            )
 | 
			
		||||
            func.lower(User.username) == func.lower(username)
 | 
			
		||||
        ).first()
 | 
			
		||||
        if user:
 | 
			
		||||
            return InvalidPayloadErrorResponse(
 | 
			
		||||
                'sorry, that user already exists'
 | 
			
		||||
                'sorry, that username is already taken'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # add new user to db
 | 
			
		||||
        new_user = User(username=username, email=email, password=password)
 | 
			
		||||
        new_user.timezone = 'Europe/Paris'
 | 
			
		||||
        db.session.add(new_user)
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
        # generate auth token
 | 
			
		||||
        auth_token = new_user.encode_auth_token(new_user.id)
 | 
			
		||||
        return {
 | 
			
		||||
            'status': 'success',
 | 
			
		||||
            'message': 'successfully registered',
 | 
			
		||||
            'auth_token': auth_token,
 | 
			
		||||
        }, 201
 | 
			
		||||
        # if a user exists with same email address, no error is returned
 | 
			
		||||
        # since a user has to confirm his email to activate his account
 | 
			
		||||
        user = User.query.filter(
 | 
			
		||||
            func.lower(User.email) == func.lower(email)
 | 
			
		||||
        ).first()
 | 
			
		||||
        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)
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
        return {'status': 'success'}, 200
 | 
			
		||||
    # handler errors
 | 
			
		||||
    except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
 | 
			
		||||
        return handle_error_and_return_response(e, db=db)
 | 
			
		||||
@@ -158,6 +174,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
def login_user() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    user login
 | 
			
		||||
    Only user with active account can log in
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -209,9 +226,9 @@ def login_user() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    email = post_data.get('email', '')
 | 
			
		||||
    password = post_data.get('password')
 | 
			
		||||
    try:
 | 
			
		||||
        # check for existing user
 | 
			
		||||
        user = User.query.filter(
 | 
			
		||||
            func.lower(User.email) == func.lower(email)
 | 
			
		||||
            func.lower(User.email) == func.lower(email),
 | 
			
		||||
            User.is_active == True,  # noqa
 | 
			
		||||
        ).first()
 | 
			
		||||
        if user and bcrypt.check_password_hash(user.password, password):
 | 
			
		||||
            # generate auth token
 | 
			
		||||
@@ -258,6 +275,7 @@ def get_authenticated_user_profile(
 | 
			
		||||
          "email": "sam@example.com",
 | 
			
		||||
          "first_name": null,
 | 
			
		||||
          "imperial_units": false,
 | 
			
		||||
          "is_active": true,
 | 
			
		||||
          "language": "en",
 | 
			
		||||
          "last_name": null,
 | 
			
		||||
          "location": null,
 | 
			
		||||
@@ -357,6 +375,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
          "email": "sam@example.com",
 | 
			
		||||
          "first_name": null,
 | 
			
		||||
          "imperial_units": false,
 | 
			
		||||
          "is_active": true,
 | 
			
		||||
          "language": "en",
 | 
			
		||||
          "last_name": null,
 | 
			
		||||
          "location": null,
 | 
			
		||||
@@ -516,6 +535,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
          "email": "sam@example.com",
 | 
			
		||||
          "first_name": null,
 | 
			
		||||
          "imperial_units": false,
 | 
			
		||||
          "is_active": true,
 | 
			
		||||
          "language": "en",
 | 
			
		||||
          "last_name": null,
 | 
			
		||||
          "location": null,
 | 
			
		||||
@@ -709,6 +729,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
          "email": "sam@example.com",
 | 
			
		||||
          "first_name": null,
 | 
			
		||||
          "imperial_units": false,
 | 
			
		||||
          "is_active": true,
 | 
			
		||||
          "language": "en",
 | 
			
		||||
          "last_name": null,
 | 
			
		||||
          "location": null,
 | 
			
		||||
@@ -1312,3 +1333,63 @@ def update_email() -> Union[Dict, HttpResponse]:
 | 
			
		||||
 | 
			
		||||
    except (exc.OperationalError, ValueError) as e:
 | 
			
		||||
        return handle_error_and_return_response(e, db=db)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/account/confirm', methods=['POST'])
 | 
			
		||||
def confirm_account() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    activate user account after registration
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
    .. sourcecode:: http
 | 
			
		||||
 | 
			
		||||
      POST /api/auth/account/confirm HTTP/1.1
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
    **Example response**:
 | 
			
		||||
 | 
			
		||||
      HTTP/1.1 200 OK
 | 
			
		||||
      Content-Type: application/json
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        "auth_token": "JSON Web Token",
 | 
			
		||||
        "message": "account confirmation successful",
 | 
			
		||||
        "status": "success"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    :<json string token: confirmation token
 | 
			
		||||
 | 
			
		||||
    :statuscode 200: account confirmation successful
 | 
			
		||||
    :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('token') is None:
 | 
			
		||||
        return InvalidPayloadErrorResponse()
 | 
			
		||||
    token = post_data.get('token')
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        user = User.query.filter_by(confirmation_token=token).first()
 | 
			
		||||
 | 
			
		||||
        if not user:
 | 
			
		||||
            return InvalidPayloadErrorResponse()
 | 
			
		||||
 | 
			
		||||
        user.is_active = True
 | 
			
		||||
        user.confirmation_token = None
 | 
			
		||||
 | 
			
		||||
        db.session.commit()
 | 
			
		||||
 | 
			
		||||
        # generate auth token
 | 
			
		||||
        auth_token = user.encode_auth_token(user.id)
 | 
			
		||||
 | 
			
		||||
        response = {
 | 
			
		||||
            'status': 'success',
 | 
			
		||||
            'message': 'account confirmation successful',
 | 
			
		||||
            'auth_token': auth_token,
 | 
			
		||||
        }
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    except (exc.OperationalError, ValueError) as e:
 | 
			
		||||
        return handle_error_and_return_response(e, db=db)
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,7 @@ class User(BaseModel):
 | 
			
		||||
    )
 | 
			
		||||
    language = db.Column(db.String(50), nullable=True)
 | 
			
		||||
    imperial_units = db.Column(db.Boolean, default=False, nullable=False)
 | 
			
		||||
    is_active = 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)
 | 
			
		||||
 | 
			
		||||
@@ -149,6 +150,7 @@ class User(BaseModel):
 | 
			
		||||
            'email': self.email,
 | 
			
		||||
            'email_to_confirm': self.email_to_confirm,
 | 
			
		||||
            'first_name': self.first_name,
 | 
			
		||||
            'is_active': self.is_active,
 | 
			
		||||
            'last_name': self.last_name,
 | 
			
		||||
            'location': self.location,
 | 
			
		||||
            'nb_sports': len(sports),
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ def set_admin(username: str) -> None:
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def get_users(auth_user: User) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all users
 | 
			
		||||
    Get all users (regardless their account status)
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 | 
			
		||||
@@ -87,6 +87,7 @@ def get_users(auth_user: User) -> Dict:
 | 
			
		||||
              "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
 | 
			
		||||
              "email": "admin@example.com",
 | 
			
		||||
              "first_name": null,
 | 
			
		||||
              "is_admin": true,
 | 
			
		||||
              "imperial_units": false,
 | 
			
		||||
              "language": "en",
 | 
			
		||||
              "last_name": null,
 | 
			
		||||
@@ -149,6 +150,7 @@ def get_users(auth_user: User) -> Dict:
 | 
			
		||||
              "created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
 | 
			
		||||
              "email": "sam@example.com",
 | 
			
		||||
              "first_name": null,
 | 
			
		||||
              "is_admin": false,
 | 
			
		||||
              "language": "fr",
 | 
			
		||||
              "last_name": null,
 | 
			
		||||
              "location": null,
 | 
			
		||||
@@ -269,6 +271,7 @@ def get_single_user(
 | 
			
		||||
            "email": "admin@example.com",
 | 
			
		||||
            "first_name": null,
 | 
			
		||||
            "imperial_units": false,
 | 
			
		||||
            "is_admin": true,
 | 
			
		||||
            "language": "en",
 | 
			
		||||
            "last_name": null,
 | 
			
		||||
            "location": null,
 | 
			
		||||
@@ -421,6 +424,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
 | 
			
		||||
            "email": "admin@example.com",
 | 
			
		||||
            "first_name": null,
 | 
			
		||||
            "imperial_units": false,
 | 
			
		||||
            "is_active": true,
 | 
			
		||||
            "language": "en",
 | 
			
		||||
            "last_name": null,
 | 
			
		||||
            "location": null,
 | 
			
		||||
 
 | 
			
		||||
@@ -62,8 +62,12 @@ def verify_user(
 | 
			
		||||
    current_request: Request, verify_admin: bool
 | 
			
		||||
) -> Tuple[Optional[HttpResponse], Optional[User]]:
 | 
			
		||||
    """
 | 
			
		||||
    Return authenticated user, if the provided token is valid and user has
 | 
			
		||||
    admin rights if 'verify_admin' is True
 | 
			
		||||
    Return authenticated user if
 | 
			
		||||
    - the provided token is valid
 | 
			
		||||
    - the user account is active
 | 
			
		||||
    - the user has admin rights if 'verify_admin' is True
 | 
			
		||||
 | 
			
		||||
    If not, it returns Error Response
 | 
			
		||||
    """
 | 
			
		||||
    default_message = 'provide a valid auth token'
 | 
			
		||||
    auth_header = current_request.headers.get('Authorization')
 | 
			
		||||
@@ -74,7 +78,7 @@ def verify_user(
 | 
			
		||||
    if isinstance(resp, str):
 | 
			
		||||
        return UnauthorizedErrorResponse(resp), None
 | 
			
		||||
    user = User.query.filter_by(id=resp).first()
 | 
			
		||||
    if not user:
 | 
			
		||||
    if not user or not user.is_active:
 | 
			
		||||
        return UnauthorizedErrorResponse(default_message), None
 | 
			
		||||
    if verify_admin and not user.admin:
 | 
			
		||||
        return ForbiddenErrorResponse(), None
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user