Exemple #1
0
def run_suite (suite):
    # We use a random username so that if a test fails, we don't have to do a cleaning of the DB so that the test suite can run again
    # This also allows us to run concurrent tests without having username conflicts.
    username = '******' + str (random.randint (10000, 100000))
    tests = suite (username)
    state = {'headers': {}}
    t0 = timems ()

    if not type_check (tests, 'list'):
        return print ('Invalid test suite, must be a list.')
    counter = 1

    def run_test (test, counter):
        result = request (state, test, counter, username)

    for test in tests:
        # If test is nested, run a nested loop
        if not (type_check (test[0], 'str')):
            for subtest in test:
                run_test (subtest, counter)
                counter += 1
        else:
           run_test (test, counter)
           counter += 1

    if isinstance (threading.current_thread (), threading._MainThread):
        print ('Test suite successful! (' + str (timems () - t0) + 'ms)')
    else:
        return timems () - t0
Exemple #2
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 '@' 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
Exemple #3
0
def request(state, test, counter, username):

    start = timems()

    if isinstance(threading.current_thread(), threading._MainThread):
        print('Start #' + str(counter) + ': ' + test[0])

    # If no explicit cookie passed, use the one from the state
    if not 'cookie' in test[3] and 'cookie' in state['headers']:
        test[3]['cookie'] = state['headers']['cookie']

    # If path, headers or body are functions, invoke them passing them the current state
    if type_check(test[2], 'fun'):
        test[2] = test[2](state)

    if type_check(test[3], 'fun'):
        test[3] = test[3](state)

    if type_check(test[4], 'fun'):
        test[4] = test[4](state)

    if type_check(test[4], 'dict'):
        test[3]['content-type'] = 'application/json'
        test[4] = json.dumps(test[4])

    # We pass the X-Testing header to let the server know that this is a request coming from an E2E test, thus no transactional emails should be sent.
    test[3]['X-Testing'] = '1'

    r = getattr(requests, test[1])(host + test[2],
                                   headers=test[3],
                                   data=test[4])

    if 'Content-Type' in r.headers and r.headers[
            'Content-Type'] == 'application/json':
        body = r.json()
    else:
        body = r.text

    if r.history and r.history[0]:
        # This will be the case if there's a redirect
        code = r.history[0].status_code
    else:
        code = r.status_code

    output = {'code': code, 'headers': r.headers, 'body': body}

    if (code != test[5]):
        print(output)
        raise Exception('A test failed!')

    if len(test) == 7:
        test[6](state, output, username)

    if isinstance(threading.current_thread(), threading._MainThread):
        print('Done  #' + str(counter) + ': ' + test[0] + ' - ' +
              str(r.status_code) + ' (' + str(timems() - start) + 'ms)')

    return output
Exemple #4
0
 def record_login(self, username, new_password_hash=None):
     """Record the fact that the user logged in, potentially updating their password hash."""
     if new_password_hash:
         self.update_user(username, {
             'password': new_password_hash,
             'last_login': timems()
         })
     else:
         self.update_user(username, {'last_login': timems()})
Exemple #5
0
def request(test, counter):

    start = timems()

    print('Start #' + str(counter) + ': ' + test[0])

    # If no explicit cookie passed, use the one from the state
    if not 'cookie' in test[3] and 'cookie' in state['headers']:
        test[3]['cookie'] = state['headers']['cookie']

    # If path, headers or body are functions, invoke them passing them the current state
    if type_check(test[2], 'fun'):
        test[2] = test[2](state)

    if type_check(test[3], 'fun'):
        test[3] = test[3](state)

    if type_check(test[4], 'fun'):
        test[4] = test[4](state)

    if type_check(test[4], 'dict'):
        test[3]['content-type'] = 'application/json'
        test[4] = json.dumps(test[4])
    r = getattr(requests, test[1])(host + test[2],
                                   headers=test[3],
                                   data=test[4])

    if 'Content-Type' in r.headers and r.headers[
            'Content-Type'] == 'application/json':
        body = r.json()
    else:
        body = r.text

    if r.history and r.history[0]:
        # This will be the case if there's a redirect
        code = r.history[0].status_code
    else:
        code = r.status_code

    output = {'code': code, 'headers': r.headers, 'body': body}

    if (code != test[5]):
        print(output)
        raise Exception('A test failed!')

    if len(test) == 7:
        test[6](state, output)

    print('Done  #' + str(counter) + ': ' + test[0] + ' - ' +
          str(r.status_code) + ' (' + str(timems() - start) + 'ms)')

    return output
Exemple #6
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 '@' 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
Exemple #7
0
    def create_class(user):
        if not is_teacher(user):
            return 'Only teachers can create classes', 403

        body = request.json
        # Validations
        if not isinstance(body, dict):
            return 'body must be an object', 400
        if not isinstance(body.get('name'), str):
            return 'name must be a string', 400

        # We use this extra call to verify if the class name doesn't already exist, if so it's a duplicate
        Classes = DATABASE.get_teacher_classes(user['username'], True)
        for Class in Classes:
            if Class['name'] == body['name']:
                return "duplicate", 200

        Class = {
            'id': uuid.uuid4().hex,
            'date': utils.timems(),
            'teacher': user['username'],
            'link': utils.random_id_generator(7),
            'name': body['name']
        }

        DATABASE.store_class(Class)

        return {'id': Class['id']}, 200
Exemple #8
0
def programs_page (request):
    username = current_user(request) ['username']
    if not username:
        return "unauthorized", 403

    from_user = request.args.get('user') or None
    if from_user and not is_admin (request):
        return "unauthorized", 403

    texts=TRANSLATIONS.data [requested_lang ()] ['Programs']
    ui=TRANSLATIONS.data [requested_lang ()] ['ui']
    adventures = load_adventure_for_language(requested_lang ())['adventures']

    result = db_get_many ('programs', {'username': from_user or username}, True)
    programs = []
    now = timems ()
    for item in result:
        measure = texts ['minutes']
        date = round ((now - item ['date']) / 60000)
        if date > 90:
            measure = texts ['hours']
            date = round (date / 60)
        if date > 36:
            measure = texts ['days']

            date = round (date / 24)

        programs.append ({'id': item ['id'], 'code': item ['code'], 'date': texts ['ago-1'] + ' ' + str (date) + ' ' + measure + ' ' + texts ['ago-2'], 'level': item ['level'], 'name': item ['name'], 'adventure_name': item.get ('adventure_name'), 'public': item.get ('public')})

    return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, ui=ui, auth=TRANSLATIONS.data [requested_lang ()] ['Auth'], programs=programs, username=username, current_page='programs', from_user=from_user, adventures=adventures)
Exemple #9
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
Exemple #10
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({})
Exemple #11
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
Exemple #12
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
    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})
Exemple #13
0
def request(method, path, headers={}, body='', cookies=None):

    if method not in ['get', 'post', 'put', 'delete']:
        raise Exception('request - Invalid method: ' + str(method))

    # We pass the X-Testing header to let the server know that this is a request coming from an E2E test, thus no transactional emails should be sent.
    headers['X-Testing'] = '1'

    # If sending an object as body, stringify it and set the proper content-type header
    if isinstance(body, dict):
        headers['content-type'] = 'application/json'
        body = json.dumps(body)

    start = utils.timems()

    response = getattr(requests, method)(HOST + path,
                                         headers=headers,
                                         data=body,
                                         cookies=cookies)

    # Remember all cookies in the cookie jar
    if cookies is not None:
        cookies.update(response.cookies)

    ret = {'time': utils.timems() - start}

    if response.history and response.history[0]:
        # This code branch will be executed if there is a redirect
        ret['code'] = response.history[0].status_code
        ret['headers'] = response.history[0].headers
        if getattr(response.history[0], '_content'):
            # We can assume that bodies returned from redirected responses are always plain text, since no JSON endpoint in the server is reachable through a redirect.
            ret['body'] = getattr(response.history[0],
                                  '_content').decode('utf-8')
    else:
        ret['code'] = response.status_code
        ret['headers'] = response.headers
        if 'Content-Type' in response.headers and response.headers[
                'Content-Type'] == 'application/json':
            ret['body'] = response.json()
        else:
            ret['body'] = response.text

    return ret
Exemple #14
0
    def get_profile(user):
        output = {'username': user['username'], 'email': user['email']}
        if 'birth_year' in user:
            output['birth_year'] = user['birth_year']
        if 'country' in user:
            output['country'] = user['country']
        if 'gender' in user:
            output['gender'] = user['gender']
        if 'verification_pending' in user:
            output['verification_pending'] = True
        output['session_expires_at'] = timems() + session_length * 1000

        return jsonify(output), 200
Exemple #15
0
    def get_profile(user):
        output = {'username': user['username'], 'email': user['email']}
        for field in [
                'birth_year', 'country', 'gender', 'prog_experience',
                'experience_languages'
        ]:
            if field in user:
                output[field] = user[field]
        if 'verification_pending' in user:
            output['verification_pending'] = True
        output['session_expires_at'] = timems() + session_length * 1000

        return jsonify(output), 200
Exemple #16
0
def programs_page(request):
    username = current_user(request)['username']
    if not username:
        return "unauthorized", 403

    lang = requested_lang()
    query_lang = request.args.get('lang') or ''
    if query_lang:
        query_lang = '?lang=' + query_lang

    texts = TRANSLATIONS.data[lang]['Programs']

    result = db_get_many('programs', {'username': username}, True)
    programs = []
    now = timems()
    for item in result:
        measure = texts['minutes']
        date = round((now - item['date']) / 60000)
        if date > 90:
            measure = texts['hours']
            date = round(date / 60)
        if date > 36:
            measure = texts['days']

            date = round(date / 24)

        programs.append({
            'id':
            item['id'],
            'code':
            item['code'],
            'date':
            texts['ago-1'] + ' ' + str(date) + ' ' + measure + ' ' +
            texts['ago-2'],
            'level':
            item['level'],
            'name':
            item['name']
        })

    return render_template('programs.html',
                           lang=requested_lang(),
                           menu=render_main_menu('programs'),
                           texts=texts,
                           auth=TRANSLATIONS.data[lang]['Auth'],
                           programs=programs,
                           username=username,
                           current_page='programs',
                           query_lang=query_lang)
Exemple #17
0
def getProfile1(state, response):
    profile = response['body']
    if profile['username'] != username:
        raise Exception('Invalid username (getProfile1)')
    if profile['email'] != username + '@domain.com':
        raise Exception('Invalid username (getProfile1)')
    if not profile['session_expires_at']:
        raise Exception('No session_expires_at (getProfile1)')
    expire = profile['session_expires_at'] - config['session'][
        'session_length'] * 60 * 1000 - timems()
    if expire > 0:
        raise Exception('Invalid session_expires_at (getProfile1), too large')
    # We give the server up to 10ms to respond to the query
    if expire < -10:
        raise Exception('Invalid session_expires_at (getProfile1), too small')
Exemple #18
0
def save_program(user):

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

    # 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 = DATABASE.programs_for_user(user['username'])
    program_id = uuid.uuid4().hex
    overwrite = False
    for program in programs:
        if program['name'] == body['name']:
            overwrite = True
            program_id = program['id']
            break

    stored_program = {
        'id': program_id,
        'session': session_id(),
        'date': timems(),
        'lang': requested_lang(),
        'version': version(),
        'level': body['level'],
        'code': body['code'],
        'name': body['name'],
        'username': user['username']
    }

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

    DATABASE.store_program(stored_program)
    if not overwrite:
        DATABASE.increase_user_program_count(user['username'])

    return jsonify({'name': body['name'], 'id': program_id})
Exemple #19
0
def run_suite(tests):
    if not type_check(tests, 'list'):
        return print('Invalid test suite, must be a list.')
    counter = 1

    def run_test(test, counter):
        result = request(test, counter)

    for test in tests:
        # If test is nested, run a nested loop
        if not (type_check(test[0], 'str')):
            for subtest in test:
                run_test(subtest, counter)
                counter += 1
        else:
            run_test(test, counter)
            counter += 1

    print('Test suite successful! (' + str(timems() - t0) + 'ms)')
Exemple #20
0
    def get_profile(user):
        # The user object we got from 'requires_login' is not fully hydrated yet. Look up the database user.
        user = DATABASE.user_by_username(user['username'])

        output = {'username': user['username'], 'email': user['email']}
        for field in [
                'birth_year', 'country', 'gender', 'prog_experience',
                'experience_languages'
        ]:
            if field in user:
                output[field] = user[field]
        if 'verification_pending' in user:
            output['verification_pending'] = True

        output['student_classes'] = DATABASE.get_student_classes(
            user['username'])

        output['session_expires_at'] = timems() + session_length * 1000

        return jsonify(output), 200
Exemple #21
0
    def create_class(user):
        if not is_teacher(request):
            return 'Only teachers can create classes', 403

        body = request.json
        # Validations
        if not isinstance(body, dict):
            return 'body must be an object', 400
        if not isinstance(body.get('name'), str):
            return 'name must be a string', 400

        Class = {
            'id': uuid.uuid4().hex,
            'date': utils.timems(),
            'teacher': user['username'],
            'link': utils.random_id_generator(7),
            'name': body['name']
        }

        DATABASE.store_class(Class)

        return {}, 200
Exemple #22
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
Exemple #23
0
import requests
import json
import random
from utils import type_check, timems
import urllib.parse
from config import config

host = 'http://localhost:' + str(config['port']) + '/'

t0 = timems()

state = {'headers': {}}


# test structure: tag method path headers body code
def request(test, counter):

    start = timems()

    print('Start #' + str(counter) + ': ' + test[0])

    # If no explicit cookie passed, use the one from the state
    if not 'cookie' in test[3] and 'cookie' in state['headers']:
        test[3]['cookie'] = state['headers']['cookie']

    # If path, headers or body are functions, invoke them passing them the current state
    if type_check(test[2], 'fun'):
        test[2] = test[2](state)

    if type_check(test[3], 'fun'):
        test[3] = test[3](state)
Exemple #24
0
def programs_page(request):
    username = current_user(request)['username']
    if not username:
        # redirect users to /login if they are not logged in
        url = request.url.replace('/programs', '/login')
        return redirect(url, code=302)

    from_user = request.args.get('user') or None
    if from_user and not is_admin(request):
        if not is_teacher(request):
            return "unauthorized", 403
        students = DATABASE.get_teacher_students(username)
        if from_user not in students:
            return "unauthorized", 403

    texts = TRANSLATIONS.get_translations(requested_lang(), 'Programs')
    ui = TRANSLATIONS.get_translations(requested_lang(), 'ui')
    adventures = load_adventure_for_language(requested_lang())['adventures']

    result = DATABASE.programs_for_user(from_user or username)
    programs = []
    now = timems()
    for item in result:
        program_age = now - item['date']
        if program_age < 1000 * 60 * 60:
            measure = texts['minutes']
            date = round(program_age / (1000 * 60))
        elif program_age < 1000 * 60 * 60 * 24:
            measure = texts['hours']
            date = round(program_age / (1000 * 60 * 60))
        else:
            measure = texts['days']
            date = round(program_age / (1000 * 60 * 60 * 24))

        programs.append({
            'id':
            item['id'],
            'code':
            item['code'],
            'date':
            texts['ago-1'] + ' ' + str(date) + ' ' + measure + ' ' +
            texts['ago-2'],
            'level':
            item['level'],
            'name':
            item['name'],
            'adventure_name':
            item.get('adventure_name'),
            'public':
            item.get('public')
        })

    return render_template('programs.html',
                           lang=requested_lang(),
                           menu=render_main_menu('programs'),
                           texts=texts,
                           ui=ui,
                           auth=TRANSLATIONS.get_translations(
                               requested_lang(), 'Auth'),
                           programs=programs,
                           username=username,
                           is_teacher=is_teacher(request),
                           current_page='programs',
                           from_user=from_user,
                           adventures=adventures)
Exemple #25
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:
            # 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
Exemple #26
0
    def signup():
        body = request.json
        # Validations, mandatory fields
        if not isinstance(body, dict):
            return 'body must be an object', 400
        if not isinstance(body.get('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 isinstance(body.get('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 isinstance(body.get('email'), str):
            return 'email must be a string', 400
        if not valid_email(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 isinstance(body.get('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
        if 'prog_experience' in body and body['prog_experience'] not in [
                'yes', 'no'
        ]:
            return 'If present, prog_experience must be "yes" or "no"', 400
        if 'experience_languages' in body:
            if not isinstance(body['experience_languages'], list):
                return 'If present, experience_languages must be an array', 400
            for language in body['experience_languages']:
                if language not in [
                        'scratch', 'other_block', 'python', 'other_text'
                ]:
                    return 'Invalid language: ' + str(language), 400

        user = DATABASE.user_by_username(body['username'].strip().lower())
        if user:
            return 'username exists', 403
        email = DATABASE.user_by_email(body['email'].strip().lower())
        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 not is_testing_request(
                request) 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,
            'last_login': timems()
        }

        for field in [
                'country', 'birth_year', 'gender', 'prog_experience',
                'experience_languages'
        ]:
            if field in body:
                if field == 'experience_languages' and len(body[field]) == 0:
                    continue
                user[field] = body[field]

        DATABASE.store_user(user)

        print(user)

        # We automatically login the user
        cookie = make_salt()
        DATABASE.store_token({
            'id': cookie,
            'username': user['username'],
            'ttl': times() + session_length
        })

        # If this is an e2e test, we return the email verification token directly instead of emailing it.
        if is_testing_request(request):
            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', 'http://localhost') +
                '/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=is_heroku(),
                        samesite='Lax',
                        path='/',
                        max_age=365 * 24 * 60 * 60)
        return resp