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 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
def current_user(request): if request.cookies.get(cookie_name): token = db_get('tokens', {'id': request.cookies.get(cookie_name)}) if token: user = db_get('users', {'username': token['username']}) if user: return user return {'username': '', 'email': ''}
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 inner(*args, **kws): User = None if request.cookies.get(cookie_name): token = db_get('tokens', {'id': request.cookies.get(cookie_name)}) if not token: return 'unauthorized', 403 user = db_get('users', {'username': token['username']}) if not user: return 'unauthorized', 403 else: return 'unauthorized', 403 return f(user, *args, **kws)
def verify_email(): username = request.args.get('username', None) token = request.args.get('token', None) if not token: return 'no token', 400 if not username: return 'no username', 400 user = db_get('users', {'username': username}) if not user: return 'invalid username/token', 403 # If user is verified, succeed anyway if not 'verification_pending' in user: return redirect('/') if token != user['verification_pending']: return 'invalid username/token', 403 db_update('users', { 'username': username, 'verification_pending': None }) return redirect('/')
def index(level, step): session_id() # Run this for the side effect of generating a session ID g.level = level = int(level) g.lang = requested_lang() g.prefix = '/hedy' # If step is a string that has more than two characters, it must be an id of a program if step and type_check(step, 'str') and len(step) > 2: result = db_get('programs', {'id': step}) if not result: return 'No such program', 404 # Allow only the owner of the program, the admin user and the teacher users to access the program user = current_user(request) if user['username'] != result['username'] and not is_admin( request) and not is_teacher(request): return 'No such program!', 404 loaded_program = result['code'] # We default to step 1 to provide a meaningful default assignment step = 1 else: loaded_program = None return hedyweb.render_assignment_editor(request=request, course=HEDY_COURSE[g.lang], level_number=level, assignment_number=step, menu=render_main_menu('hedy'), translations=TRANSLATIONS, version=version(), loaded_program=loaded_program)
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
def delete_program (user, program_id): result = db_get ('programs', {'id': program_id}) if not result or result ['username'] != user ['username']: return "", 404 db_del ('programs', {'id': program_id}) 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 redirect ('/programs')
def index(level, step): # Sublevel requested if re.match ('\d+-\d+', level): pass # If level has a dash, we keep it as a string # Normal level requested elif re.match ('\d', level): try: g.level = level = int(level) except: return 'No such Hedy level!', 404 else: return 'No such Hedy level!', 404 g.lang = requested_lang() g.prefix = '/hedy' initialize_gfi_session(g.lang) loaded_program = '' loaded_program_name = '' adventure_name = '' # If step is a string that has more than two characters, it must be an id of a program if step and type_check (step, 'str') and len (step) > 2: result = db_get ('programs', {'id': step}) if not result: return 'No such program', 404 # If the program is not public, allow only the owner of the program, the admin user and the teacher users to access the program user = current_user (request) public_program = 'public' in result and result ['public'] if not public_program and user ['username'] != result ['username'] and not is_admin (request) and not is_teacher (request): return 'No such program!', 404 loaded_program = result ['code'] loaded_program_name = result ['name'] if 'adventure_name' in result: adventure_name = result ['adventure_name'] # We default to step 1 to provide a meaningful default assignment step = 1 adventure_assignments = load_adventure_assignments_per_level(g.lang, level) return hedyweb.render_assignment_editor( request=request, course=HEDY_COURSE[g.lang], level_number=level, assignment_number=step, menu=render_main_menu('hedy'), translations=TRANSLATIONS, version=version(), adventure_assignments=adventure_assignments, loaded_program=loaded_program, loaded_program_name=loaded_program_name, adventure_name=adventure_name)
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
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({})
def get_queryset(self): qset = super(AbstractView, self).get_queryset() for name, deep in [ ('incats', True), ('cats', False) ]: cats = self.request.QUERY_PARAMS.get(name) if cats: model = qset.model # self.get_serializer().opts.model field = model._meta.get_field('cats') if field: cats = utils.str_ints(cats) if cats: if deep: target = field.related.parent_model cats = utils.list_compact([ utils.db_get(target, cat) for cat in cats ]) cats = utils.tree_all_downs(cats) qset = qset.filter(cats__in=cats) # print 'get_queryset', self, type(qset), model, cats return qset
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)
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
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 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
def delete_program (user, program_id): result = db_get ('programs', {'id': program_id}) if not result or result ['username'] != user ['username']: return "", 404 db_del ('programs', {'id': program_id}) return redirect ('/programs')