| @@ -3,6 +3,7 @@ CREATE TABLE users ( | |||
| userid INT UNSIGNED AUTO_INCREMENT, | |||
| username VARCHAR(45) UNIQUE NOT NULL, | |||
| password VARCHAR(60) NOT NULL, | |||
| temporary_password VARCHAR(60) NOT NULL DEFAULT "", | |||
| full_name VARCHAR(200) NOT NULL, | |||
| company VARCHAR(50), | |||
| email VARCHAR(50) NOT NULL, | |||
| @@ -11,10 +12,10 @@ CREATE TABLE users ( | |||
| state VARCHAR(50), | |||
| postal_code 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) | |||
| ); | |||
| @@ -37,8 +37,8 @@ def set_user(username, password, full_name, company, email, | |||
| query = (""" | |||
| INSERT INTO users (userid, username, password, full_name, company, | |||
| 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: | |||
| cursor.execute(query, (username, password, full_name, company, email, | |||
| @@ -15,7 +15,7 @@ def get_user(username): | |||
| """ | |||
| db.connect() | |||
| 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 | |||
| try: | |||
| cursor.execute(query, (username,)) | |||
| @@ -54,6 +54,44 @@ def get_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): | |||
| """ | |||
| 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"> | |||
| $:csrf_field() | |||
| $:login_form.render() | |||
| <a href="/request_reset">Forgot your password?</a> | |||
| </form> | |||
| $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.logout import Logout | |||
| from views.register import Register, Verify | |||
| from views.reset import RequestReset, Reset | |||
| from views.new_project import New_project | |||
| from views.open_projects import Open_projects | |||
| from views.project import Project | |||
| @@ -18,6 +19,8 @@ urls = ( | |||
| '/login', 'Login', | |||
| '/logout', 'Logout', | |||
| '/verify', 'Verify', | |||
| '/reset', 'Reset', | |||
| '/request_reset', 'RequestReset', | |||
| '/register', 'Register', | |||
| '/new_project', 'New_project', | |||
| '/open_projects', 'Open_projects', | |||
| @@ -1,5 +1,5 @@ | |||
| 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 | |||
| @@ -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") | |||
| not_empty = form.regexp(r".+", "- This field is required") | |||
| # Define the login form | |||
| # Define the login form | |||
| login_form = form.Form( | |||
| form.Textbox("username", description="Username"), | |||
| form.Password("password", description="Password"), | |||
| @@ -17,7 +17,7 @@ login_form = form.Form( | |||
| form.Button("Log In", type="submit", description="Login"), | |||
| ) | |||
| # Define the register form | |||
| # Define the register form | |||
| register_form = form.Form( | |||
| form.Textbox("username", not_empty, description="Username"), | |||
| form.Textbox("full_name", not_empty, description="Full name"), | |||
| @@ -32,6 +32,20 @@ register_form = form.Form( | |||
| 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 | |||
| project_form = form.Form( | |||
| 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 | |||
| :param identifier: The id of the task | |||
| :param task_title: Task title | |||
| :param task_description: Task description | |||
| :param task_description: Task description | |||
| :param budget: Task budget | |||
| :type identifier: int, 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 | |||
| :param project_title: Project title | |||
| :param project_description: Project description | |||
| :param project_description: Project description | |||
| :param category_name: Name of the belonging category | |||
| :type project_title: str | |||
| :type project_description: str | |||
| :type category_name: str | |||
| :return: A set of project form elements | |||
| :return: A set of project form elements | |||
| """ | |||
| categories = get_categories() | |||
| 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 | |||
| """ | |||
| 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("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) | |||
| @@ -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("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) | |||
| ) | |||
| ) | |||
| return user_permissions | |||
| @@ -55,7 +55,7 @@ class Login(): | |||
| if user is None: | |||
| 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(): | |||
| 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): | |||
| 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()) | |||
| self.login(username, userid, data.remember) | |||
| 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: | |||
| 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()) | |||
| @@ -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.") | |||