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
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
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
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()})
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
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
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
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)
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
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({})
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
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})
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
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
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
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)
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')
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})
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)')
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
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
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
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)
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)
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
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