Skip to content

Commit

Permalink
User accounts/management ready except for admin.
Browse files Browse the repository at this point in the history
  • Loading branch information
fpereiro committed Dec 18, 2020
1 parent 19deba9 commit 0acdb04
Show file tree
Hide file tree
Showing 20 changed files with 414 additions and 117 deletions.
2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def main_page(page):
lang = requested_lang()
effective_lang = lang

if page in ['signup', 'login', 'my-profile']:
if page in ['signup', 'login', 'my-profile', 'recover', 'reset']:
return auth_templates(page, lang, render_main_menu(page), request)

# Default to English if requested language is not available
Expand Down
134 changes: 85 additions & 49 deletions auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import bcrypt
import redis
import re
import urllib
from flask import request, make_response, jsonify, redirect, render_template
from utils import type_check, object_check, timems
import datetime
Expand All @@ -18,8 +20,7 @@
cookie_name = config ['session'] ['cookie_name']
session_length = config ['session'] ['session_length'] * 60

# TODO: determine environment properly
env = 'local'
env = os.getenv ('HEROKU_APP_NAME')

def check_password (password, hash):
return bcrypt.checkpw (bytes (password, 'utf-8'), bytes (hash, 'utf-8'))
Expand Down Expand Up @@ -77,11 +78,11 @@ def login ():

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

user = r.hgetall ('user:' + username)
if not user:
Expand Down Expand Up @@ -143,52 +144,54 @@ def signup ():
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
'username': username,
'password': hashed,
'email': email,
'created': timems (),
'verification_pending': hashed_token
}

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

r.hmset ('user:' + username, user);
r.hset ('email', email, username)

if env == 'local':
if not env:
# If on local environment, we return email verification token directly instead of emailing it, for test purposes.
return jsonify ({'username': username, 'token': hashed_token}), 200
else:
# TODO: Replace with template
send_email (email, 'Welcome to Hedy', 'Welcome to Hedy, please verify your email', '<h1>Welcome to Hedy, please verify your email</h1>')
send_email_template ('welcome_verify', email, requested_lang (), os.getenv ('BASE_URL') + '/auth/verify?username=' + urllib.parse.quote_plus (username) + '&token=' + urllib.parse.quote_plus (hashed_token))
return '', 200

@app.route ('/auth/verify', methods=['GET'])
def verify_email ():
username = request.args.get ('username', None)
token = request.args.get ('token', None)
if not token:
return 'No token sent', 403
return 'no token', 400
if not username:
return 'No username sent', 403
return 'no username', 400

user = r.hgetall ('user:' + username)

if not user:
return 'Invalid username', 403
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 token', 403
return 'invalid username/token', 403

r.hdel ('user:' + username, 'verification_pending')
return redirect ('/')
Expand Down Expand Up @@ -230,6 +233,9 @@ def change_password (user):
hashed = hash (body ['new_password'], make_salt ())

r.hset ('user:' + user ['username'], 'password', hashed)
if env:
send_email_template ('change_password', user ['email'], requested_lang (), None)

return '', 200

@app.route ('/profile', methods=['POST'])
Expand Down Expand Up @@ -257,12 +263,12 @@ def update_profile (user):
if 'email' in body:
email = body ['email'].strip ().lower ()
if email != user ['email']:
exists = r.hget ('email', email)
if exists:
return 'email exists', 403
r.hdel ('email', user ['email'])
r.hset ('email', email, user ['username'])
r.hset ('user:' + user ['username'], 'email', email)
exists = r.hget ('email', email)
if exists:
return 'email exists', 403
r.hdel ('email', user ['email'])
r.hset ('email', email, user ['username'])
r.hset ('user:' + user ['username'], 'email', email)

if 'country' in body:
r.hset ('user:' + user ['username'], 'country', body ['country'])
Expand Down Expand Up @@ -297,11 +303,11 @@ def recover ():

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

user = r.hgetall ('user:' + username)

Expand All @@ -313,12 +319,11 @@ def recover ():

r.setex ('token:' + username, session_length, hashed)

if env == 'local':
if not env:
# If on local environment, we return email verification token directly instead of emailing it, for test purposes.
return jsonify ({'token': token}), 200
return jsonify ({'username': username, 'token': token}), 200
else:
# TODO: Replace with template
send_email (user ['email'], 'Reset password', 'Reset password', '<h1>Reset password</h1>')
send_email_template ('recover_password', user ['email'], requested_lang (), os.getenv ('BASE_URL') + '/reset?username=' + urllib.parse.quote_plus (username) + '&token=' + urllib.parse.quote_plus (token))
return '', 200

@app.route ('/auth/reset', methods=['POST'])
Expand All @@ -339,31 +344,47 @@ def reset ():

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

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

hashed = hash (body ['password'], make_salt ())
r.delete ('token:' + username);
r.hset ('user:' + username, 'password', hashed)
email = r.hget ('user:' + username, 'email')

if env != 'local':
# TODO: Replace with template
send_email (email, 'Your password has been changed', 'Your password has been changed', '<h1>Your password has been changed</h1>')
if env:
send_email_template ('reset_password', email, requested_lang (), None)

return '', 200

@app.route ('/users', methods=['GET'])
@requires_login
def get_users (user):
if user ['email'] != os.getenv ('ADMIN_EMAIL'):
return '', 403
# TODO: implement
# username, email, birth_year (int), country, gender, created (int), last_access (int|undefined), verification_pending (make boolean)
# sort by last created first
output = []
return jsonify (output), 200

# Turn off verbose logs from boto/SES, thanks to https://github.com/boto/boto3/issues/521
import logging
for name in logging.Logger.manager.loggerDict.keys ():
if ('boto' in name) or ('urllib3' in name) or ('s3transfer' in name) or ('boto3' in name) or ('botocore' in name) or ('nose' in name):
logging.getLogger (name).setLevel (logging.CRITICAL)

# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-using-sdk-python.html
email_client = boto3.client ('ses', region_name = config ['email'] ['region'], aws_access_key_id = '...', aws_secret_access_key = '...')
email_client = boto3.client ('ses', region_name = config ['email'] ['region'], aws_access_key_id = os.getenv ('AWS_SES_ACCESS_KEY'), aws_secret_access_key = os.getenv ('AWS_SES_SECRET_KEY'))

def send_email (recipient, subject, body_plain, body_html):
try:
Expand All @@ -383,10 +404,25 @@ def send_email (recipient, subject, body_plain, body_html):
else:
print ('Email sent to ' + recipient)

def send_email_template (template, email, lang, link):
texts = TRANSLATIONS.data [lang] ['Auth']
subject = texts ['email_' + template + '_subject']
body = texts ['email_' + template + '_body'].split ('\n')
body = [texts ['email_hello']] + body
if link:
body = body + ['@@LINK@@']
body = body + texts ['email_goodbye'].split ('\n')

body_plain = '\n'.join (body)
body_html = '<p>' + '</p><p>'.join (body) + '</p>'
if link:
body_plain = body_plain.replace ('@@LINK@@', 'Please copy and paste this link into a new tab: ' + link)
body_html = body_html.replace ('@@LINK@@', '<a href="' + link + '">Link</a>')

send_email (email, subject, body_plain, body_html)

def auth_templates (page, lang, menu, request):
if page == 'signup':
return render_template ('signup.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request))
if page == 'login':
return render_template ('login.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request))
if page == 'my-profile':
return render_template ('profile.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request))
if page in ['signup', 'login', 'recover', 'reset']:
return render_template (page + '.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request))
23 changes: 23 additions & 0 deletions coursedata/texts/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Auth:
male: "Male"
other: "Other"
country: "Country"
subscribe_newsletter: "Subscribe to the newsletter"
already_account: "Already have an account?"
login_long: "Log in to your account"
login: "Log in"
Expand All @@ -65,3 +66,25 @@ Auth:
profile_updated: "Profile updated."
password_updated: "Password updated."
signup_success: "Success! Please login :)."
forgot_password: "Forgot your password?"
recover_password: "Request a password reset"
send_password_recovery: "Send me a password recovery link"
sent_password_recovery: "You should soon receive an email with instructions on how to reset your password."
reset_password: "Reset password"
password_resetted: "Your password has been successfully resetted. Please login."
invalid_username_password: "Invalid username/password."
invalid_username: "Invalid username."
invalid_password: "Invalid password."
invalid_reset_link: "Invalid reset password link."
exists_email: "That email is already in use."
exists_username: "That username is already in use."
email_hello: "Hi!"
email_goodbye: "Thank you!\nThe Hedy team"
email_welcome_verify_subject: "Your Hedy account has been created"
email_welcome_verify_body: "Your Hedy account has been created successfully. Welcome!\nPlease click on the link below to verify your email address."
email_change_password_subject: "Your Hedy password has been changed"
email_change_password_body: "Your Hedy password has been changed. If you did this, all is good.\nIf you didn't change your password, please contact us immediately by replying to this email."
email_recover_password_subject: "Reset your Hedy password"
email_recover_password_body: "By clicking the link below, you can set a new Hedy password. If you haven't required a password reset, please ignore this email."
email_reset_password_subject: "Your Hedy password has been resetted"
email_reset_password_body: "Your Hedy password has been resetted to a new one. If you did this, all is good.\nIf you didn't change your password, please contact us immediately by replying to this email."
23 changes: 23 additions & 0 deletions coursedata/texts/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Auth:
male: "Masculino"
other: "Otro"
country: "País"
subscribe_newsletter: "Subscribirse al newsletter"
already_account: "¿Ya tienes una cuenta?"
login_long: "Ingresar a mi cuenta"
login: "Ingresar"
Expand All @@ -65,3 +66,25 @@ Auth:
profile_updated: "Perfil actualizado."
password_updated: "Contraseña actualizada."
signup_success: "¡Tu cuenta se ha creado con éxito! Ya puedes entrar con tu nuevo nombre de usuario :)."
forgot_password: "¿Olvidaste tu contraseña?"
recover_password: "Reestablecer contraseña"
send_password_recovery: "Envíame un link para reestablecer mi contraseña"
sent_password_recovery: "Pronto recibirás un correo electrónico con instrucciones sobre cómo reestablecer tu contraseña."
reset_password: "Establecer nueva contraseña"
password_resetted: "Tu contraseña ha sido cambiada exitosamente. Ya puedes entrar nuevamente a tu cuenta usando la nueva contraseña."
invalid_username_password: "Combinación de usuario/contraseña inválida."
invalid_username: "Nombre de usuario inválido."
invalid_password: "Contraseña inválida."
invalid_reset_link: "Link de reestablecer contraseña inválido."
exists_email: "Esa dirección de correo electrónico ya está en uso."
exists_username: "Ese nombre de usuario ya está en uso."
email_hello: "¡Hola!"
email_goodbye: "¡Gracias!\nEl equipo de Hedy"
email_welcome_verify_subject: "Tu cuenta de Hedy ha sido creada"
email_welcome_verify_body: "Tu cuenta de Hedy ha sido creada exitosamente. ¡Bienvenid@!\nPor favor haz click en el link debajo para verificar tu dirección de correo electrónico."
email_change_password_subject: "Tu contraseña de Hedy ha sido cambiada"
email_change_password_body: "Tu contraseña de Hedy ha sido cambiada. Si tú has hecho esto, todo marcha bien.\nSi tú no has realizado esta acción, por favor contáctanos inmediatamente respondiendo a este correo electrónico."
email_recover_password_subject: "Reestablecer tu contraseña de Hedy"
email_recover_password_body: "Haciendo click en el link debajo, puedes reestablecer tu contraseña de Hedy. Si tú no has requerido reestablecer tu contraseña de Hedy, por favor ignora este correo electrónico."
email_reset_password_subject: "Tu contraseña de Hedy se ha reestablecido"
email_reset_password_body: "Tu contraseña de Hedy se ha reestablecido. Si tú has hecho esto, todo marcha bien.\nSi tú no has reestablecido tu contraseña, por favor contáctanos inmediataamente respondiendo a este correo electrónico."
23 changes: 23 additions & 0 deletions coursedata/texts/fr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Auth:
male: "Male"
other: "Other"
country: "Country"
subscribe_newsletter: "Subscribe to the newsletter"
already_account: "Already have an account?"
login_long: "Log in to your account"
login: "Log in"
Expand All @@ -76,3 +77,25 @@ Auth:
profile_updated: "Profile updated."
password_updated: "Password updated."
signup_success: "Success! Please login :)."
forgot_password: "Forgot your password?"
recover_password: "Request a password reset"
send_password_recovery: "Send me a password recovery link"
sent_password_recovery: "You should soon receive an email with instructions on how to reset your password."
reset_password: "Reset password"
password_resetted: "Your password has been successfully resetted. Please login."
invalid_username_password: "Invalid username/password."
invalid_username: "Invalid username."
invalid_password: "Invalid password."
invalid_reset_link: "Invalid reset password link."
exists_email: "That email is already in use."
exists_username: "That username is already in use."
email_hello: "Hi!"
email_goodbye: "Thank you!\nThe Hedy team"
email_welcome_verify_subject: "Your Hedy account has been created"
email_welcome_verify_body: "Your Hedy account has been created successfully. Welcome!\nPlease click on the link below to verify your email address."
email_change_password_subject: "Your Hedy password has been changed"
email_change_password_body: "Your Hedy password has been changed. If you did this, all is good.\nIf you didn't change your password, please contact us immediately by replying to this email."
email_recover_password_subject: "Reset your Hedy password"
email_recover_password_body: "By clicking the link below, you can set a new Hedy password. If you haven't required a password reset, please ignore this email."
email_reset_password_subject: "Your Hedy password has been resetted"
email_reset_password_body: "Your Hedy password has been resetted to a new one. If you did this, all is good.\nIf you didn't change your password, please contact us immediately by replying to this email."
Loading

0 comments on commit 0acdb04

Please sign in to comment.