예제 #1
0
    def login():
        body = request.json
        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'username must be a string', 400
        if not object_check(body, 'password', 'str'):
            return 'password must be a string', 400

        # If username has an @-sign, then it's an email
        if re.match('@', body['username']):
            username = r.hget('emails', body['username'])
            if not username:
                return 'invalid username/password', 403
        else:
            username = body['username']

        username = username.strip().lower()

        user = r.hgetall('user:'******'invalid username/password', 403
        if not check_password(body['password'], user['password']):
            return 'invalid username/password', 403

        cookie = make_salt()
        r.setex('sess:' + cookie, session_length, body['username'])
        r.hset('user:'******'username'], 'lastAccess', timems())
        resp = make_response({})
        resp.set_cookie(cookie_name, value=cookie, httponly=True, path='/')
        return resp
예제 #2
0
파일: auth.py 프로젝트: tech189/hedy
    def login ():
        body = request.json
        # Validations
        if not type_check (body, 'dict'):
            return 'body must be an object', 400
        if not object_check (body, 'username', 'str'):
            return 'username must be a string', 400
        if not object_check (body, 'password', 'str'):
            return 'password must be a string', 400

        # If username has an @-sign, then it's an email
        if '@' in body ['username']:
            user = db_get ('users', {'email': body ['username'].strip ().lower ()}, True)
        else:
            user = db_get ('users', {'username': body ['username'].strip ().lower ()})

        if not user:
            return 'invalid username/password', 403
        if not check_password (body ['password'], user ['password']):
            return 'invalid username/password', 403

        cookie = make_salt ()
        db_set ('tokens', {'id': cookie, 'username': user ['username'], 'ttl': times () + session_length})
        db_set ('users', {'username': user ['username'], 'last_login': timems ()})
        resp = make_response ({})
        resp.set_cookie (cookie_name, value=cookie, httponly=True, path='/')
        return resp
예제 #3
0
파일: auth.py 프로젝트: balath/hedy
    def mark_as_teacher():
        if not is_admin(request):
            return 'unauthorized', 403

        body = request.json

        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'body.username must be a string', 400
        if not object_check(body, 'is_teacher', 'bool'):
            return 'body.is_teacher must be boolean', 400

        user = db_get('users', {'username': body['username'].strip().lower()})

        if not user:
            return 'invalid username', 400

        db_set(
            'users', {
                'username': user['username'],
                'is_teacher': 1 if body['is_teacher'] else 0
            })

        return '', 200
예제 #4
0
파일: auth.py 프로젝트: balath/hedy
    def reset():
        body = request.json
        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'body.username must be a string', 400
        if not object_check(body, 'token', 'str'):
            return 'body.token must be a string', 400
        if not object_check(body, 'password', 'str'):
            return 'body.password be a string', 400

        if len(body['password']) < 6:
            return 'password must be at least six characters long', 400

        # There's no need to trim or lowercase username, because it should come within a link prepared by the app itself and not inputted manually by the user.
        token = db_get('tokens', {'id': body['username']})
        if not token:
            return 'invalid username/token', 403
        if not check_password(body['token'], token['token']):
            return 'invalid username/token', 403

        hashed = hash(body['password'], make_salt())
        token = db_del('tokens', {'id': body['username']})
        db_set('users', {'username': body['username'], 'password': hashed})
        user = db_get('users', {'username': body['username']})

        if env:
            send_email_template('reset_password', user['email'],
                                requested_lang(), None)

        return '', 200
예제 #5
0
    def update_profile(user):

        body = request.json
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if 'email' in body:
            if not object_check(body, 'email', 'str'):
                return 'body.email must be a string', 400
            if not re.match(
                    '^(([a-zA-Z0-9_\.\-]+)@([\da-zA-Z\.\-]+)\.([a-zA-Z\.]{2,6})\s*)$',
                    body['email']):
                return 'body.email must be a valid email', 400
        if 'country' in body:
            if not body['country'] in countries:
                return 'body.country must be a valid country', 400
        if 'age' in body:
            if not object_check(body, 'age', 'int') or body['age'] <= 0:
                return 'body.age must be an integer larger than 0', 400
        if 'gender' in body:
            if body['gender'] != 'm' and body['gender'] != 'f':
                return 'body.gender must be m/f', 400

        if 'email' in body:
            r.hdel('emails', user['email'])
            r.hset('emails', body['email'], user['username'])
            r.hset('user:'******'username'], 'email', body['email'])

        if 'country' in body:
            r.hset('user:'******'username'], 'country', body['country'])
        if 'age' in body:
            r.hset('user:'******'username'], 'age', body['age'])
        if 'gender' in body:
            r.hset('user:'******'username'], 'gender', body['gender'])
        return '', 200
예제 #6
0
def save_program(user):

    body = request.json
    if not type_check(body, 'dict'):
        return 'body must be an object', 400
    if not object_check(body, 'code', 'str'):
        return 'code must be a string', 400
    if not object_check(body, 'name', 'str'):
        return 'name must be a string', 400
    if not object_check(body, 'level', 'int'):
        return 'level must be an integer', 400

    # We execute the saved program to see if it would generate an error or not
    error = None
    try:
        hedy_errors = TRANSLATIONS.get_translations(requested_lang(),
                                                    'HedyErrorMessages')
        result = hedy.transpile(body['code'], body['level'])
    except hedy.HedyException as E:
        error_template = hedy_errors[E.error_code]
        error = error_template.format(**E.arguments)
    except Exception as E:
        error = str(E)

    name = body['name']

    # We check if a program with a name `xyz` exists in the database for the username. If it does, we exist whether `xyz (1)` exists, until we find a program `xyz (NN)` that doesn't exist yet.
    # It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
    # For now, we bring all existing programs for the user and then search within them for repeated names.
    existing = db_get_many('programs', {'username': user['username']}, True)
    name_counter = 0
    for program in existing:
        if re.match('^' + re.escape(name) + '( \(\d+\))*', program['name']):
            name_counter = name_counter + 1
    if name_counter:
        name = name + ' (' + str(name_counter) + ')'

    db_set(
        'programs', {
            'id': uuid.uuid4().hex,
            'session': session_id(),
            'date': timems(),
            'lang': requested_lang(),
            'version': version(),
            'level': body['level'],
            'code': body['code'],
            'name': name,
            'server_error': error,
            'username': user['username']
        })
    program_count = 0
    if 'program_count' in user:
        program_count = user['program_count']
    db_set('users', {
        'username': user['username'],
        'program_count': program_count + 1
    })

    return jsonify({})
예제 #7
0
    def signup():
        body = request.json
        # Validations, mandatory fields
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'username must be a string', 400
        if re.match('@', body['username']):
            return 'username cannot contain an @-sign', 400
        if re.match(':', body['username']):
            return 'username cannot contain a colon', 400
        if not object_check(body, 'password', 'str'):
            return 'password must be a string', 400
        if len(body['password']) < 6:
            return 'password must be at least six characters long', 400
        if not object_check(body, 'email', 'str'):
            return 'email must be a string', 400
        if not re.match(
                '^(([a-zA-Z0-9_\.\-]+)@([\da-zA-Z\.\-]+)\.([a-zA-Z\.]{2,6})\s*)$',
                body['email']):
            return 'email must be a valid email', 400
        # Validations, optional fields
        if 'country' in body:
            if not body['country'] in countries:
                return 'country must be a valid country', 400
        if 'age' in body:
            if not object_check(body, 'age', 'int') or body['age'] <= 0:
                return 'age must be an integer larger than 0', 400
        if 'gender' in body:
            if body['gender'] != 'm' and body['gender'] != 'f':
                return 'gender must be m/f', 400

        user = r.hgetall('user:'******'username'])
        if user:
            return 'username exists', 403
        email = r.hget('emails', body['email'])
        if email:
            return 'email exists', 403

        hashed = hash(body['password'], make_salt())

        user = {
            'username': body['username'].strip().lower(),
            'password': hashed,
            'email': body['email'].strip().lower(),
            'created': timems()
        }

        if 'country' in body:
            user['country'] = body['country']
        if 'age' in body:
            user['age'] = body['age']
        if 'gender' in body:
            user['gender'] = body['gender']

        r.hmset('user:'******'username'], user)
        r.hset('emails', body['email'], body['username'])

        return '', 200
예제 #8
0
파일: auth.py 프로젝트: balath/hedy
    def login():
        body = request.json
        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'username must be a string', 400
        if not object_check(body, 'password', 'str'):
            return 'password must be a string', 400

        # If username has an @-sign, then it's an email
        if '@' in body['username']:
            user = db_get('users', {'email': body['username'].strip().lower()},
                          True)
        else:
            user = db_get('users',
                          {'username': body['username'].strip().lower()})

        if not user:
            return 'invalid username/password', 403
        if not check_password(body['password'], user['password']):
            return 'invalid username/password', 403

        # If the number of bcrypt rounds has changed, create a new hash.
        new_hash = None
        if config['bcrypt_rounds'] != extract_bcrypt_rounds(user['password']):
            new_hash = hash(body['password'], make_salt())

        cookie = make_salt()
        db_set(
            'tokens', {
                'id': cookie,
                'username': user['username'],
                'ttl': times() + session_length
            })
        if new_hash:
            db_set(
                'users', {
                    'username': user['username'],
                    'password': new_hash,
                    'last_login': timems()
                })
        else:
            db_set('users', {
                'username': user['username'],
                'last_login': timems()
            })
        resp = make_response({})
        # We set the cookie to expire in a year, just so that the browser won't invalidate it if the same cookie gets renewed by constant use.
        # The server will decide whether the cookie expires.
        resp.set_cookie(cookie_name,
                        value=cookie,
                        httponly=True,
                        secure=True,
                        samesite='Lax',
                        path='/',
                        max_age=365 * 24 * 60 * 60)
        return resp
예제 #9
0
파일: app.py 프로젝트: Tazaria/hedy
def save_program(user):

    body = request.json
    if not type_check(body, 'dict'):
        return 'body must be an object', 400
    if not object_check(body, 'code', 'str'):
        return 'code must be a string', 400
    if not object_check(body, 'name', 'str'):
        return 'name must be a string', 400
    if not object_check(body, 'level', 'int'):
        return 'level must be an integer', 400
    if 'adventure_name' in body:
        if not object_check(body, 'adventure_name', 'str'):
            return 'if present, adventure_name must be a string', 400

    name = body['name']

    # We check if a program with a name `xyz` exists in the database for the username.
    # It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
    # For now, we bring all existing programs for the user and then search within them for repeated names.
    programs = db_get_many('programs', {'username': user['username']}, True)
    program = {}
    overwrite = False
    for program in programs:
        if program['name'] == name:
            overwrite = True
            break

    stored_program = {
        'id': program.get('id') if overwrite else uuid.uuid4().hex,
        'session': session_id(),
        'date': timems(),
        'lang': requested_lang(),
        'version': version(),
        'level': body['level'],
        'code': body['code'],
        'name': name,
        'username': user['username']
    }

    if 'adventure_name' in body:
        stored_program['adventure_name'] = body['adventure_name']

    if overwrite:
        db_update('programs', stored_program)
    else:
        db_create('programs', stored_program)

    program_count = 0
    if 'program_count' in user:
        program_count = user['program_count']
    db_update('users', {
        'username': user['username'],
        'program_count': program_count + 1
    })

    return jsonify({'name': name})
예제 #10
0
파일: app.py 프로젝트: TiBiBa/hedy
def share_unshare_program(user):
    body = request.json
    if not type_check (body, 'dict'):
        return 'body must be an object', 400
    if not object_check (body, 'id', 'str'):
        return 'id must be a string', 400
    if not object_check (body, 'public', 'bool'):
        return 'public must be a string', 400

    result = db_get ('programs', {'id': body ['id']})
    if not result or result ['username'] != user ['username']:
        return 'No such program!', 404

    db_update ('programs', {'id': body ['id'], 'public': 1 if body ['public'] else None})
    return jsonify({})
예제 #11
0
    def recover():
        body = request.json
        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'body.username must be a string', 400

        # If username has an @-sign, then it's an email
        if re.match('@', body['username']):
            username = r.hget('emails', body['username'])
            if not username:
                return 'invalid username/password', 403
        else:
            username = body['username']

        username = username.strip().lower()

        user = r.hgetall('user:'******'invalid username', 403

        token = make_salt()
        hashed = hash(token, make_salt())

        r.setex('token:' + hashed, session_length, body['username'])
        # TODO: when in non-local environment, email the token instead of returning it
        return token
예제 #12
0
파일: auth.py 프로젝트: tech189/hedy
    def recover ():
        body = request.json
        # Validations
        if not type_check (body, 'dict'):
            return 'body must be an object', 400
        if not object_check (body, 'username', 'str'):
            return 'body.username must be a string', 400

        # If username has an @-sign, then it's an email
        if '@' in body ['username']:
            user = db_get ('users', {'email': body ['username'].strip ().lower ()}, True)
        else:
            user = db_get ('users', {'username': body ['username'].strip ().lower ()})

        if not user:
            return 'invalid username', 403

        token = make_salt ()
        hashed = hash (token, make_salt ())

        db_set ('tokens', {'id': user ['username'], 'token': hashed, 'ttl': times () + session_length})

        if not env:
            # If on local environment, we return email verification token directly instead of emailing it, for test purposes.
            return jsonify ({'username': user ['username'], 'token': token}), 200
        else:
            send_email_template ('recover_password', user ['email'], requested_lang (), os.getenv ('BASE_URL') + '/reset?username='******'username']) + '&token=' + urllib.parse.quote_plus (token))
            return '', 200
예제 #13
0
    def change_password(user):

        body = request.json
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'oldPassword', 'str'):
            return 'body.oldPassword must be a string', 400
        if not object_check(body, 'newPassword', 'str'):
            return 'body.newPassword must be a string', 400

        if not check_password(body['oldPassword'], user['password']):
            return 'invalid username/password', 403

        hashed = hash(body['newPassword'], make_salt())

        r.hset('user:'******'username'], 'password', hashed)
        return '', 200
예제 #14
0
    def reset():
        body = request.json
        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'token', 'str'):
            return 'body.token must be a string', 400
        if not object_check(body, 'password', 'str'):
            return 'body.password be a string', 400

        username = r.get('token:' + body['token'])
        if not username:
            return 'invalid token', 403

        hashed = hash(body['password'], make_salt())
        r.delete('token:' + body['token'])
        r['hset']('user:'******'password', hashed)
        return {}, 200
예제 #15
0
파일: auth.py 프로젝트: tech189/hedy
    def update_profile (user):

        body = request.json
        if not type_check (body, 'dict'):
            return 'body must be an object', 400
        if 'email' in body:
            if not object_check (body, 'email', 'str'):
                return 'body.email must be a string', 400
            if not re.match ('^(([a-zA-Z0-9_\.\-]+)@([\da-zA-Z\.\-]+)\.([a-zA-Z\.]{2,6})\s*)$', body ['email']):
                return 'body.email must be a valid email', 400
        if 'country' in body:
            if not body ['country'] in countries:
                return 'body.country must be a valid country', 400
        if 'birth_year' in body:
            if not object_check (body, 'birth_year', 'int') or body ['birth_year'] <= 1900 or body ['birth_year'] > datetime.datetime.now ().year:
                return 'birth_year must be a year between 1900 and ' + str (datetime.datetime.now ().year), 400
        if 'gender' in body:
            if body ['gender'] != 'm' and body ['gender'] != 'f' and body ['gender'] != 'o':
                return 'body.gender must be m/f/o', 400

        resp = {}
        if 'email' in body:
            email = body ['email'].strip ().lower ()
            if email != user ['email']:
                exists = db_get ('users', {'email': email}, True)
                if exists:
                    return 'email exists', 403
                token = make_salt ()
                hashed_token = hash (token, make_salt ())
                db_set ('users', {'username': user ['username'], 'email': email, 'verification_pending': hashed_token})
                if not env:
                   # If on local environment, we return email verification token directly instead of emailing it, for test purposes.
                   resp = {'username': user ['username'], 'token': hashed_token}
                else:
                    send_email_template ('welcome_verify', email, requested_lang (), os.getenv ('BASE_URL') + '/auth/verify?username='******'&token=' + urllib.parse.quote_plus (hashed_token))

        if 'country' in body:
            db_set ('users', {'username': user ['username'], 'country': body ['country']})
        if 'birth_year' in body:
            db_set ('users', {'username': user ['username'], 'birth_year': body ['birth_year']})
        if 'gender' in body:
            db_set ('users', {'username': user ['username'], 'gender': body ['gender']})

        return jsonify (resp)
예제 #16
0
    def change_user_email():
        if not is_admin(request):
            return 'unauthorized', 403

        body = request.json

        # Validations
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'body.username must be a string', 400
        if not object_check(body, 'email', 'str'):
            return 'body.email must be a string', 400
        if not valid_email(body['email']):
            return 'email must be a valid email', 400

        user = db_get('users', {'username': body['username'].strip().lower()})

        if not user:
            return 'invalid username', 400

        token = make_salt()
        hashed_token = hash(token, make_salt())

        # We assume that this email is not in use by any other users. In other words, we trust the admin to enter a valid, not yet used email address.
        db_update(
            'users', {
                'username': user['username'],
                'email': body['email'],
                'verification_pending': hashed_token
            })

        # If this is an e2e test, we return the email verification token directly instead of emailing it.
        if is_testing_request(request):
            resp = {'username': user['username'], 'token': hashed_token}
        else:
            send_email_template(
                'welcome_verify', body['email'], requested_lang(),
                os.getenv('BASE_URL') + '/auth/verify?username='******'username']) + '&token=' +
                urllib.parse.quote_plus(hashed_token))

        return '', 200
예제 #17
0
파일: auth.py 프로젝트: tech189/hedy
    def change_password (user):

        body = request.json
        if not type_check (body, 'dict'):
            return 'body must be an object', 400
        if not object_check (body, 'old_password', 'str'):
            return 'body.old_password must be a string', 400
        if not object_check (body, 'new_password', 'str'):
            return 'body.new_password must be a string', 400

        if len (body ['new_password']) < 6:
            return 'password must be at least six characters long', 400

        if not check_password (body ['old_password'], user ['password']):
            return 'invalid username/password', 403

        hashed = hash (body ['new_password'], make_salt ())

        db_set ('users', {'username': user ['username'], 'password': hashed})
        if env:
            send_email_template ('change_password', user ['email'], requested_lang (), None)

        return '', 200
예제 #18
0
    def signup():
        body = request.json
        # Validations, mandatory fields
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'username must be a string', 400
        if '@' in body['username']:
            return 'username cannot contain an @-sign', 400
        if ':' in body['username']:
            return 'username cannot contain a colon', 400
        if len(body['username'].strip()) < 3:
            return 'username must be at least three characters long', 400
        if not object_check(body, 'password', 'str'):
            return 'password must be a string', 400
        if len(body['password']) < 6:
            return 'password must be at least six characters long', 400
        if not object_check(body, 'email', 'str'):
            return 'email must be a string', 400
        if not re.match(
                '^(([a-zA-Z0-9_\.\-]+)@([\da-zA-Z\.\-]+)\.([a-zA-Z\.]{2,6})\s*)$',
                body['email']):
            return 'email must be a valid email', 400
        # Validations, optional fields
        if 'country' in body:
            if not body['country'] in countries:
                return 'country must be a valid country', 400
        if 'birth_year' in body:
            if not object_check(
                    body, 'birth_year',
                    'int') or body['birth_year'] <= 1900 or body[
                        'birth_year'] > datetime.datetime.now().year:
                return 'birth_year must be a year between 1900 and ' + datetime.datetime.now(
                ).year, 400
        if 'gender' in body:
            if body['gender'] != 'm' and body['gender'] != 'f' and body[
                    'gender'] != 'o':
                return 'gender must be m/f/o', 400

        user = db_get('users', {'username': body['username'].strip().lower()})
        if user:
            return 'username exists', 403
        email = db_get('users', {'email': body['email'].strip().lower()}, True)
        if email:
            return 'email exists', 403

        hashed = hash(body['password'], make_salt())

        token = make_salt()
        hashed_token = hash(token, make_salt())
        username = body['username'].strip().lower()
        email = body['email'].strip().lower()

        if env and 'subscribe' in body and body['subscribe'] == True:
            send_email(config['email']['sender'],
                       'Subscription to Hedy newsletter on signup', email,
                       '<p>' + email + '</p>')

        user = {
            'username': username,
            'password': hashed,
            'email': email,
            'created': timems(),
            'verification_pending': hashed_token
        }

        if 'country' in body:
            user['country'] = body['country']
        if 'birth_year' in body:
            user['birth_year'] = body['birth_year']
        if 'gender' in body:
            user['gender'] = body['gender']

        db_set('users', user)

        # We automatically login the user
        cookie = make_salt()
        db_set(
            'tokens', {
                'id': cookie,
                'username': user['username'],
                'ttl': times() + session_length
            })
        db_set('users', {'username': user['username'], 'last_login': timems()})

        # If on local environment, we return email verification token directly instead of emailing it, for test purposes.
        if not env:
            resp = make_response({'username': username, 'token': hashed_token})
        # Otherwise, we send an email with a verification link and we return an empty body
        else:
            send_email_template(
                'welcome_verify', email, requested_lang(),
                os.getenv('BASE_URL') + '/auth/verify?username='******'&token=' +
                urllib.parse.quote_plus(hashed_token))
            resp = make_response({})

        resp.set_cookie(cookie_name, value=cookie, httponly=True, path='/')
        return resp
예제 #19
0
파일: auth.py 프로젝트: balath/hedy
    def signup():
        body = request.json
        # Validations, mandatory fields
        if not type_check(body, 'dict'):
            return 'body must be an object', 400
        if not object_check(body, 'username', 'str'):
            return 'username must be a string', 400
        if '@' in body['username']:
            return 'username cannot contain an @-sign', 400
        if ':' in body['username']:
            return 'username cannot contain a colon', 400
        if len(body['username'].strip()) < 3:
            return 'username must be at least three characters long', 400
        if not object_check(body, 'password', 'str'):
            return 'password must be a string', 400
        if len(body['password']) < 6:
            return 'password must be at least six characters long', 400
        if not object_check(body, 'email', 'str'):
            return 'email must be a string', 400
        if not re.match(
                '^(([a-zA-Z0-9_\.\-]+)@([\da-zA-Z\.\-]+)\.([a-zA-Z\.]{2,6})\s*)$',
                body['email']):
            return 'email must be a valid email', 400
        # Validations, optional fields
        if 'country' in body:
            if not body['country'] in countries:
                return 'country must be a valid country', 400
        if 'birth_year' in body:
            if not object_check(
                    body, 'birth_year',
                    'int') or body['birth_year'] <= 1900 or body[
                        'birth_year'] > datetime.datetime.now().year:
                return 'birth_year must be a year between 1900 and ' + datetime.datetime.now(
                ).year, 400
        if 'gender' in body:
            if body['gender'] != 'm' and body['gender'] != 'f' and body[
                    'gender'] != 'o':
                return 'gender must be m/f/o', 400

        user = db_get('users', {'username': body['username'].strip().lower()})
        if user:
            return 'username exists', 403
        email = db_get('users', {'email': body['email'].strip().lower()}, True)
        if email:
            return 'email exists', 403

        hashed = hash(body['password'], make_salt())

        token = make_salt()
        hashed_token = hash(token, make_salt())
        username = body['username'].strip().lower()
        email = body['email'].strip().lower()

        if env and 'subscribe' in body and body['subscribe'] == True:
            # If we have a Mailchimp API key, we use it to add the subscriber through the API
            if os.getenv('MAILCHIMP_API_KEY') and os.getenv(
                    'MAILCHIMP_AUDIENCE_ID'):
                # The first domain in the path is the server name, which is contained in the Mailchimp API key
                request_path = 'https://' + os.getenv(
                    'MAILCHIMP_API_KEY').split(
                        '-')[1] + '.api.mailchimp.com/3.0/lists/' + os.getenv(
                            'MAILCHIMP_AUDIENCE_ID') + '/members'
                request_headers = {
                    'Content-Type': 'application/json',
                    'Authorization': 'apikey ' + os.getenv('MAILCHIMP_API_KEY')
                }
                request_body = {'email_address': email, 'status': 'subscribed'}
                r = requests.post(request_path,
                                  headers=request_headers,
                                  data=json.dumps(request_body))

                subscription_error = None
                if r.status_code != 200 and r.status_code != 400:
                    subscription_error = True
                # We can get a 400 if the email is already subscribed to the list. We should ignore this error.
                if r.status_code == 400 and not re.match(
                        '.*already a list member', r.text):
                    subscription_error = True
                # If there's an error in subscription through the API, we report it to the main email address
                if subscription_error:
                    send_email(
                        config['email']['sender'],
                        'ERROR - Subscription to Hedy newsletter on signup',
                        email, '<p>' + email + '</p><pre>Status:' +
                        str(r.status_code) + '    Body:' + r.text + '</pre>')
            # Otherwise, we send an email to notify about this to the main email address
            else:
                send_email(config['email']['sender'],
                           'Subscription to Hedy newsletter on signup', email,
                           '<p>' + email + '</p>')

        user = {
            'username': username,
            'password': hashed,
            'email': email,
            'created': timems(),
            'verification_pending': hashed_token
        }

        if 'country' in body:
            user['country'] = body['country']
        if 'birth_year' in body:
            user['birth_year'] = body['birth_year']
        if 'gender' in body:
            user['gender'] = body['gender']

        db_set('users', user)

        # We automatically login the user
        cookie = make_salt()
        db_set(
            'tokens', {
                'id': cookie,
                'username': user['username'],
                'ttl': times() + session_length
            })
        db_set('users', {'username': user['username'], 'last_login': timems()})

        # If on local environment, we return email verification token directly instead of emailing it, for test purposes.
        if not env:
            resp = make_response({'username': username, 'token': hashed_token})
        # Otherwise, we send an email with a verification link and we return an empty body
        else:
            send_email_template(
                'welcome_verify', email, requested_lang(),
                os.getenv('BASE_URL') + '/auth/verify?username='******'&token=' +
                urllib.parse.quote_plus(hashed_token))
            resp = make_response({})

        # We set the cookie to expire in a year, just so that the browser won't invalidate it if the same cookie gets renewed by constant use.
        # The server will decide whether the cookie expires.
        resp.set_cookie(cookie_name,
                        value=cookie,
                        httponly=True,
                        secure=True,
                        samesite='Lax',
                        path='/',
                        max_age=365 * 24 * 60 * 60)
        return resp