| @@ -3,6 +3,7 @@ CREATE TABLE users ( | |||||
| userid INT UNSIGNED AUTO_INCREMENT, | userid INT UNSIGNED AUTO_INCREMENT, | ||||
| username VARCHAR(45) UNIQUE NOT NULL, | username VARCHAR(45) UNIQUE NOT NULL, | ||||
| password VARCHAR(60) NOT NULL, | password VARCHAR(60) NOT NULL, | ||||
| temporary_password VARCHAR(60) NOT NULL DEFAULT "", | |||||
| full_name VARCHAR(200) NOT NULL, | full_name VARCHAR(200) NOT NULL, | ||||
| company VARCHAR(50), | company VARCHAR(50), | ||||
| email VARCHAR(50) NOT NULL, | email VARCHAR(50) NOT NULL, | ||||
| @@ -11,10 +12,10 @@ CREATE TABLE users ( | |||||
| state VARCHAR(50), | state VARCHAR(50), | ||||
| postal_code VARCHAR(50), | postal_code VARCHAR(50), | ||||
| country VARCHAR(50), | country VARCHAR(50), | ||||
| login_attempts INT UNSIGNED, | |||||
| last_login_attempt INT UNSIGNED, | |||||
| verified BOOLEAN, | |||||
| token VARCHAR(50), | |||||
| login_attempts INT UNSIGNED DEFAULT 0, | |||||
| last_login_attempt INT UNSIGNED DEFAULT 0, | |||||
| verified BOOLEAN DEFAULT 0, | |||||
| token VARCHAR(50) NOT NULL DEFAULT "", | |||||
| PRIMARY KEY (userid) | PRIMARY KEY (userid) | ||||
| ); | ); | ||||
| @@ -37,8 +37,8 @@ def set_user(username, password, full_name, company, email, | |||||
| query = (""" | query = (""" | ||||
| INSERT INTO users (userid, username, password, full_name, company, | INSERT INTO users (userid, username, password, full_name, company, | ||||
| email, street_address, city, state, postal_code, | email, street_address, city, state, postal_code, | ||||
| country, login_attempts, last_login_attempt, verified, token) | |||||
| VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0, 0, 0, %s) | |||||
| country, token) | |||||
| VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) | |||||
| """) | """) | ||||
| try: | try: | ||||
| cursor.execute(query, (username, password, full_name, company, email, | cursor.execute(query, (username, password, full_name, company, email, | ||||
| @@ -15,7 +15,7 @@ def get_user(username): | |||||
| """ | """ | ||||
| db.connect() | db.connect() | ||||
| cursor = db.cursor() | cursor = db.cursor() | ||||
| query = ("SELECT userid, username, password, login_attempts, last_login_attempt from users where username = %s") | |||||
| query = ("SELECT userid, username, email, password, temporary_password, login_attempts, last_login_attempt from users where username = %s") | |||||
| user = None | user = None | ||||
| try: | try: | ||||
| cursor.execute(query, (username,)) | cursor.execute(query, (username,)) | ||||
| @@ -54,6 +54,44 @@ def get_users(): | |||||
| return users | return users | ||||
| def set_temporary_password(userid, password): | |||||
| """ | |||||
| Set a temporary password for a user | |||||
| """ | |||||
| db.connect() | |||||
| cursor = db.cursor() | |||||
| query = ("UPDATE users SET temporary_password = %s WHERE userid = %s") | |||||
| try: | |||||
| cursor.execute(query, (password, userid)) | |||||
| db.commit() | |||||
| except mysql.connector.Error as err: | |||||
| logger.error("Failed executing query: %s", err) | |||||
| cursor.fetchall() | |||||
| exit(1) | |||||
| finally: | |||||
| cursor.close() | |||||
| db.close() | |||||
| def set_password(userid, password): | |||||
| """ | |||||
| Set the password for a user | |||||
| """ | |||||
| db.connect() | |||||
| cursor = db.cursor() | |||||
| query = ("UPDATE users SET password = %s WHERE userid = %s") | |||||
| try: | |||||
| cursor.execute(query, (password, userid)) | |||||
| db.commit() | |||||
| except mysql.connector.Error as err: | |||||
| logger.error("Failed executing query: %s", err) | |||||
| cursor.fetchall() | |||||
| exit(1) | |||||
| finally: | |||||
| cursor.close() | |||||
| db.close() | |||||
| def set_login_attempts(userid, num, timestamp): | def set_login_attempts(userid, num, timestamp): | ||||
| """ | """ | ||||
| Set the number and timestamp of the failed login attempts for the given user. | Set the number and timestamp of the failed login attempts for the given user. | ||||
| @@ -17,6 +17,7 @@ $def with (nav, login_form, message) | |||||
| <form method="POST"> | <form method="POST"> | ||||
| $:csrf_field() | $:csrf_field() | ||||
| $:login_form.render() | $:login_form.render() | ||||
| <a href="/request_reset">Forgot your password?</a> | |||||
| </form> | </form> | ||||
| $else: | $else: | ||||
| @@ -0,0 +1,25 @@ | |||||
| $def with (nav, reset_form, message) | |||||
| <head> | |||||
| <title>Beelance2</title> | |||||
| <meta charset="utf-8"> | |||||
| <link rel="stylesheet" type="text/css" href="static/stylesheet.css"> | |||||
| <link rel="shortcut icon" type="image/png" href="static/honeybee.png"/> | |||||
| </head> | |||||
| <body> | |||||
| $:nav | |||||
| <h2>Reset password</h2> | |||||
| <form method="POST"> | |||||
| $:csrf_field() | |||||
| $:reset_form.render() | |||||
| </form> | |||||
| <p>$message</p> | |||||
| </body> | |||||
| <footer></footer> | |||||
| @@ -0,0 +1,25 @@ | |||||
| $def with (nav, reset_request_form, message) | |||||
| <head> | |||||
| <title>Beelance2</title> | |||||
| <meta charset="utf-8"> | |||||
| <link rel="stylesheet" type="text/css" href="static/stylesheet.css"> | |||||
| <link rel="shortcut icon" type="image/png" href="static/honeybee.png"/> | |||||
| </head> | |||||
| <body> | |||||
| $:nav | |||||
| <h2>Reset password</h2> | |||||
| <form method="POST"> | |||||
| $:csrf_field() | |||||
| $:reset_request_form.render() | |||||
| </form> | |||||
| <p>$message</p> | |||||
| </body> | |||||
| <footer></footer> | |||||
| @@ -3,6 +3,7 @@ import web | |||||
| from views.login import Login | from views.login import Login | ||||
| from views.logout import Logout | from views.logout import Logout | ||||
| from views.register import Register, Verify | from views.register import Register, Verify | ||||
| from views.reset import RequestReset, Reset | |||||
| from views.new_project import New_project | from views.new_project import New_project | ||||
| from views.open_projects import Open_projects | from views.open_projects import Open_projects | ||||
| from views.project import Project | from views.project import Project | ||||
| @@ -18,6 +19,8 @@ urls = ( | |||||
| '/login', 'Login', | '/login', 'Login', | ||||
| '/logout', 'Logout', | '/logout', 'Logout', | ||||
| '/verify', 'Verify', | '/verify', 'Verify', | ||||
| '/reset', 'Reset', | |||||
| '/request_reset', 'RequestReset', | |||||
| '/register', 'Register', | '/register', 'Register', | ||||
| '/new_project', 'New_project', | '/new_project', 'New_project', | ||||
| '/open_projects', 'Open_projects', | '/open_projects', 'Open_projects', | ||||
| @@ -1,5 +1,5 @@ | |||||
| from web import form | from web import form | ||||
| from models.project import get_categories | |||||
| from models.project import get_categories | |||||
| from models.user import get_users, get_user_id_by_name | from models.user import get_users, get_user_id_by_name | ||||
| @@ -9,7 +9,7 @@ vpass = form.regexp(r".{6,100}$", '- Must be atleast 6 characters long') | |||||
| number = form.regexp(r"^[0-9]+$", "- Must be a number") | number = form.regexp(r"^[0-9]+$", "- Must be a number") | ||||
| not_empty = form.regexp(r".+", "- This field is required") | not_empty = form.regexp(r".+", "- This field is required") | ||||
| # Define the login form | |||||
| # Define the login form | |||||
| login_form = form.Form( | login_form = form.Form( | ||||
| form.Textbox("username", description="Username"), | form.Textbox("username", description="Username"), | ||||
| form.Password("password", description="Password"), | form.Password("password", description="Password"), | ||||
| @@ -17,7 +17,7 @@ login_form = form.Form( | |||||
| form.Button("Log In", type="submit", description="Login"), | form.Button("Log In", type="submit", description="Login"), | ||||
| ) | ) | ||||
| # Define the register form | |||||
| # Define the register form | |||||
| register_form = form.Form( | register_form = form.Form( | ||||
| form.Textbox("username", not_empty, description="Username"), | form.Textbox("username", not_empty, description="Username"), | ||||
| form.Textbox("full_name", not_empty, description="Full name"), | form.Textbox("full_name", not_empty, description="Full name"), | ||||
| @@ -32,6 +32,20 @@ register_form = form.Form( | |||||
| form.Button("Register", type="submit", description="Register") | form.Button("Register", type="submit", description="Register") | ||||
| ) | ) | ||||
| # Define the reset forms | |||||
| request_reset_form = form.Form( | |||||
| form.Textbox("username", description="Username"), | |||||
| form.Textbox("email", description="Email address"), | |||||
| form.Button("Submit", type="submit", description="Submit"), | |||||
| ) | |||||
| reset_form = form.Form( | |||||
| form.Password("temporary", description="Temporary password"), | |||||
| form.Password("password", description="New password"), | |||||
| form.Password("repeat", description="Repeat new password"), | |||||
| form.Button("Change password", type="submit", description="Change password"), | |||||
| ) | |||||
| # Define the project view form | # Define the project view form | ||||
| project_form = form.Form( | project_form = form.Form( | ||||
| form.Input("myfile", type="file"), | form.Input("myfile", type="file"), | ||||
| @@ -47,7 +61,7 @@ def get_task_form_elements(identifier=0, task_title="", task_description="", bud | |||||
| Generate a set of task form elements | Generate a set of task form elements | ||||
| :param identifier: The id of the task | :param identifier: The id of the task | ||||
| :param task_title: Task title | :param task_title: Task title | ||||
| :param task_description: Task description | |||||
| :param task_description: Task description | |||||
| :param budget: Task budget | :param budget: Task budget | ||||
| :type identifier: int, str | :type identifier: int, str | ||||
| :type task_title: str | :type task_title: str | ||||
| @@ -66,12 +80,12 @@ def get_project_form_elements(project_title="", project_description="", category | |||||
| """ | """ | ||||
| Generate a set of project form elements | Generate a set of project form elements | ||||
| :param project_title: Project title | :param project_title: Project title | ||||
| :param project_description: Project description | |||||
| :param project_description: Project description | |||||
| :param category_name: Name of the belonging category | :param category_name: Name of the belonging category | ||||
| :type project_title: str | :type project_title: str | ||||
| :type project_description: str | :type project_description: str | ||||
| :type category_name: str | :type category_name: str | ||||
| :return: A set of project form elements | |||||
| :return: A set of project form elements | |||||
| """ | """ | ||||
| categories = get_categories() | categories = get_categories() | ||||
| project_form_elements = ( | project_form_elements = ( | ||||
| @@ -97,7 +111,7 @@ def get_user_form_elements(identifier=0, user_name="", read_permission=True, wri | |||||
| :return: The form elements to add users to a project | :return: The form elements to add users to a project | ||||
| """ | """ | ||||
| user_form_elements = ( | user_form_elements = ( | ||||
| form.Textbox("user_name_" + str(identifier), description="User", value=user_name, placeholder="Leave blank for open project"), | |||||
| form.Textbox("user_name_" + str(identifier), description="User", value=user_name, placeholder="Leave blank for open project"), | |||||
| form.Checkbox("read_permission_" + str(identifier), description="Read Permission", checked=read_permission, value=True), | form.Checkbox("read_permission_" + str(identifier), description="Read Permission", checked=read_permission, value=True), | ||||
| form.Checkbox("write_permission_" + str(identifier), description="Write Permission", checked=write_permission, value=True), | form.Checkbox("write_permission_" + str(identifier), description="Write Permission", checked=write_permission, value=True), | ||||
| form.Checkbox("modify_permission_" + str(identifier), description="Modify Permission", checked=modify_permission, value=True) | form.Checkbox("modify_permission_" + str(identifier), description="Modify Permission", checked=modify_permission, value=True) | ||||
| @@ -147,5 +161,5 @@ def get_apply_permissions_form(identifier=0, read_permission="TRUE", write_permi | |||||
| form.Checkbox("read_permission_" + str(identifier), description="Read Permission", checked=(read_permission=="TRUE"), value=True), | form.Checkbox("read_permission_" + str(identifier), description="Read Permission", checked=(read_permission=="TRUE"), value=True), | ||||
| form.Checkbox("write_permission_" + str(identifier), description="Write Permission", checked=(write_permission=="TRUE"), value=True), | form.Checkbox("write_permission_" + str(identifier), description="Write Permission", checked=(write_permission=="TRUE"), value=True), | ||||
| form.Checkbox("modify_permission_" + str(identifier), description="Modify Permission", checked=(modify_permission=="TRUE"), value=True) | form.Checkbox("modify_permission_" + str(identifier), description="Modify Permission", checked=(modify_permission=="TRUE"), value=True) | ||||
| ) | |||||
| ) | |||||
| return user_permissions | return user_permissions | ||||
| @@ -55,7 +55,7 @@ class Login(): | |||||
| if user is None: | if user is None: | ||||
| return render.login(nav, login_form, "- User authentication failed") | return render.login(nav, login_form, "- User authentication failed") | ||||
| userid, username, password_hash, login_attempts, last_login_attempt = user | |||||
| userid, username, _, password_hash, temporary_password, login_attempts, last_login_attempt = user | |||||
| if login_attempts > login_attempts_threshold and last_login_attempt + login_timeout > time.time(): | if login_attempts > login_attempts_threshold and last_login_attempt + login_timeout > time.time(): | ||||
| return render.login(nav, login_form, "- There have been too many incorrect login attempts for your account. You have to wait a minute before you can log in.") | return render.login(nav, login_form, "- There have been too many incorrect login attempts for your account. You have to wait a minute before you can log in.") | ||||
| @@ -67,9 +67,16 @@ class Login(): | |||||
| if not models.user.is_verified(userid): | if not models.user.is_verified(userid): | ||||
| return render.login(nav, login_form, "- User not authenticated yet. Please check you email.") | return render.login(nav, login_form, "- User not authenticated yet. Please check you email.") | ||||
| if temporary_password: | |||||
| logger.info("A password reset was requested for user %s, but they logged in with the old password before resetting") | |||||
| models.user.set_temporary_password(userid, "") | |||||
| models.user.set_login_attempts(userid, 0, time.time()) | models.user.set_login_attempts(userid, 0, time.time()) | ||||
| self.login(username, userid, data.remember) | self.login(username, userid, data.remember) | ||||
| raise web.seeother("/") | raise web.seeother("/") | ||||
| elif temporary_password and bcrypt.checkpw(data.password.encode('UTF-8'), temporary_password.encode('UTF-8')): | |||||
| session.temporary_userid = userid | |||||
| raise web.seeother("/reset") | |||||
| else: | else: | ||||
| logger.warning("Incorrect login attempt on user %s by IP %s", username, web.ctx.ip) | logger.warning("Incorrect login attempt on user %s by IP %s", username, web.ctx.ip) | ||||
| models.user.set_login_attempts(userid, login_attempts+1, time.time()) | models.user.set_login_attempts(userid, login_attempts+1, time.time()) | ||||
| @@ -0,0 +1,100 @@ | |||||
| import web | |||||
| from uuid import uuid4 | |||||
| from views.forms import reset_form, request_reset_form | |||||
| from views.utils import get_nav_bar, csrf_protected, get_render, password_weakness, sendmail | |||||
| import models.user | |||||
| import logging | |||||
| import bcrypt | |||||
| logger = logging.getLogger(__name__) | |||||
| class RequestReset: | |||||
| def GET(self): | |||||
| session = web.ctx.session | |||||
| nav = get_nav_bar(session) | |||||
| return get_render().reset_request(nav, request_reset_form, "") | |||||
| @csrf_protected | |||||
| def POST(self): | |||||
| session = web.ctx.session | |||||
| nav = get_nav_bar(session) | |||||
| data = web.input(username="", email="") | |||||
| render = get_render() | |||||
| user = models.user.get_user(data.username) | |||||
| if user and user[2] == data.email: | |||||
| password = uuid4().hex | |||||
| password_hash = bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt()) | |||||
| models.user.set_temporary_password(user[0], password_hash) | |||||
| sendmail( | |||||
| 'Reset your Beelance password', | |||||
| """ | |||||
| Hi! | |||||
| Someone requested a password reset for your account. If that wasn't you you can ignore this email. | |||||
| If you want to reset your password, log in with this password: {password} | |||||
| Then you will be able to set a new password. | |||||
| """.format(password=password), | |||||
| "", | |||||
| data.email, | |||||
| ) | |||||
| logger.info("User %s requested a password reset", data.username) | |||||
| else: | |||||
| logger.info("Incorrect reset request with username %s and email %s", data.username, data.email) | |||||
| return render.reset_request(nav, request_reset_form, "An email has been sent, if the username and email is correct") | |||||
| class Reset: | |||||
| def GET(self): | |||||
| session = web.ctx.session | |||||
| nav = get_nav_bar(session) | |||||
| render = get_render() | |||||
| if 'temporary_userid' not in session or not session.temporary_userid: | |||||
| return render.reset(nav, reset_form, "Something went wrong. Try logging in with the temporary password again.") | |||||
| return get_render().reset(nav, reset_form, "") | |||||
| @csrf_protected | |||||
| def POST(self): | |||||
| session = web.ctx.session | |||||
| nav = get_nav_bar(session) | |||||
| data = web.input(temporary="", password="", repeat="") | |||||
| render = get_render() | |||||
| if 'temporary_userid' not in session or not session.temporary_userid: | |||||
| return render.reset(nav, reset_form, "Something went wrong. Try logging in with the temporary password again.") | |||||
| userid = session.temporary_userid | |||||
| username = models.user.get_user_name_by_id(userid) | |||||
| user = models.user.get_user(username) | |||||
| temporary_password = user[4] | |||||
| # Check that the temporary password is correct | |||||
| if not bcrypt.checkpw(data.temporary.encode('UTF-8'), temporary_password.encode('UTF-8')): | |||||
| return render.reset(nav, reset_form, "Incorrect temporary password") | |||||
| # Check that the passwords match | |||||
| if data.password != data.repeat: | |||||
| return render.reset(nav, reset_form, "The repeated password doesn't match the first") | |||||
| # Check password security | |||||
| weakness = password_weakness(data.password, username) | |||||
| if weakness is not None: | |||||
| return render.reset(nav, reset_form, weakness) | |||||
| # Set the new password and log the user in | |||||
| password_hash = bcrypt.hashpw(data.password.encode('UTF-8'), bcrypt.gensalt()) | |||||
| models.user.set_password(userid, password_hash) | |||||
| models.user.set_temporary_password(userid, "") | |||||
| session.temporary_userid = None | |||||
| logger.info("User %s has changed their password", username) | |||||
| return get_render().reset(nav, reset_form, "Your password has been reset. You can log in again now.") | |||||