浏览代码

Implement password reset

Fixes #2
pull/40/head
父节点
当前提交
dd27cb68a4
共有 10 个文件被更改,包括 230 次插入16 次删除
  1. +5
    -4
      mysql/sql/init.sql
  2. +2
    -2
      src/app/models/register.py
  3. +39
    -1
      src/app/models/user.py
  4. +1
    -0
      src/app/templates/login.html
  5. +25
    -0
      src/app/templates/reset.html
  6. +25
    -0
      src/app/templates/reset_request.html
  7. +3
    -0
      src/app/views/app.py
  8. +22
    -8
      src/app/views/forms.py
  9. +8
    -1
      src/app/views/login.py
  10. +100
    -0
      src/app/views/reset.py

+ 5
- 4
mysql/sql/init.sql 查看文件

@@ -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)
); );




+ 2
- 2
src/app/models/register.py 查看文件

@@ -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,


+ 39
- 1
src/app/models/user.py 查看文件

@@ -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.


+ 1
- 0
src/app/templates/login.html 查看文件

@@ -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:


+ 25
- 0
src/app/templates/reset.html 查看文件

@@ -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>

+ 25
- 0
src/app/templates/reset_request.html 查看文件

@@ -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
- 0
src/app/views/app.py 查看文件

@@ -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',


+ 22
- 8
src/app/views/forms.py 查看文件

@@ -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

+ 8
- 1
src/app/views/login.py 查看文件

@@ -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())


+ 100
- 0
src/app/views/reset.py 查看文件

@@ -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.")

正在加载...
取消
保存