It is now possible to reset the password, we only need to implement the
mail feature. At the moment we cannot send a mail with the recovery code
to the user.
Fixes: #10095
Signed-off-by: Jonatan Schlag <jonatan.schlag(a)ipfire.org>
---
Makefile.am | 6 ++-
src/buildservice/users.py | 35 +++++++++++++++++
src/templates/user-forgot-password.html | 9 +----
.../user-requested-password-recovery.html | 18 +++++++++
src/templates/user-reset-password-fail.html | 18 +++++++++
src/templates/user-reset-password-success.html | 18 +++++++++
src/templates/user-reset-password.html | 36 ++++++++++++++++++
src/web/__init__.py | 1 +
src/web/auth.py | 44 ++++++++++++++++++++--
9 files changed, 174 insertions(+), 11 deletions(-)
create mode 100644 src/templates/user-requested-password-recovery.html
create mode 100644 src/templates/user-reset-password-fail.html
create mode 100644 src/templates/user-reset-password-success.html
create mode 100644 src/templates/user-reset-password.html
diff --git a/Makefile.am b/Makefile.am
index bc5cd94..ccbf96c 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -214,7 +214,11 @@ dist_templates_DATA = \
src/templates/user-profile.html \
src/templates/user-profile-need-activation.html \
src/templates/user-profile-passwd.html \
- src/templates/user-profile-passwd-ok.html
+ src/templates/user-profile-passwd-ok.html \
+ src/templates/user-requested-password-recovery.html \
+ src/templates/user-reset-password.html \
+ src/templates//user-reset-password-success.html \
+ src/templates//user-reset-password-fail.html
templatesdir = $(datadir)/templates
diff --git a/src/buildservice/users.py b/src/buildservice/users.py
index 7c98d4b..a4ce2b0 100644
--- a/src/buildservice/users.py
+++ b/src/buildservice/users.py
@@ -1,5 +1,6 @@
#!/usr/bin/python
+import datetime
import email.utils
import hashlib
import logging
@@ -185,6 +186,10 @@ class Users(base.Object):
LEFT JOIN users_emails ON users.id = users_emails.user_id \
WHERE users_emails.email = %s", email)
+ def get_by_password_recovery_code(self, code):
+ return self._get_user("SELECT * FROM users \
+ WHERE password_recovery_code = %s AND password_recovery_code_expires_at > NOW()", code)
+
def find_maintainers(self, maintainers):
email_addresses = []
@@ -297,6 +302,10 @@ class User(base.DataObject):
"""
Update the passphrase the users uses to log on.
"""
+ # We cannot set the password for ldap users
+ if self.ldap_dn:
+ raise AttributeError("Cannot set passphrase for LDAP user")
+
self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s",
generate_password_hash(passphrase), self.id)
@@ -437,6 +446,32 @@ class User(base.DataObject):
timezone = property(get_timezone, set_timezone)
+ def get_password_recovery_code(self):
+ return self.data.password_recovery_code
+
+ def set_password_recovery_code(self, code):
+ self._set_attribute("password_recovery_code", code)
+
+ self._set_attribute("password_recovery_code_expires_at",
+ datetime.datetime.utcnow() + datetime.timedelta(days=1))
+
+ password_recovery_code = property(get_password_recovery_code, set_password_recovery_code)
+
+ def forgot_password(self):
+ log.debug("User %s reqested password recovery" % self.name)
+
+ # We cannot reset te password for ldap users
+ if self.ldap_dn:
+ # Maybe we should send an email with an explanation
+ return
+
+ # Add a recovery code to the database and a timestamp when this code expires
+ self.password_recovery_code = generate_random_string(64)
+
+ # XXX
+ # We should send an email with the activation code
+
+
@property
def activated(self):
return self.data.activated
diff --git a/src/templates/user-forgot-password.html b/src/templates/user-forgot-password.html
index 2896ea4..3c21804 100644
--- a/src/templates/user-forgot-password.html
+++ b/src/templates/user-forgot-password.html
@@ -17,11 +17,6 @@
<h1>{{ _("Forgot password") }}</h1>
</div>
- <!-- XXX --->
- <div class="alert alert-warning">
- {{ _("Work in progress!") }}
- </div>
-
<div class="row">
<div class="span6">
<p>
@@ -29,7 +24,7 @@
{{ _("However, we allow to re-activate your account.") }}
</p>
<p>
- {{ _("You need to enter your username below.") }}
+ {{ _("You need to enter your username or your email address below") }}
{{ _("After that, you will receive an email with intructions how to go on.") }}
</p>
<hr>
@@ -39,7 +34,7 @@
<fieldset>
<div class="control-group">
- <label class="control-label" for="name">{{ _("Your username") }}</label>
+ <label class="control-label" for="name">{{ _("Your username or email address") }}</label>
<div class="controls">
<input type="text" class="input-xlarge" id="name" name="name" />
</div>
diff --git a/src/templates/user-requested-password-recovery.html b/src/templates/user-requested-password-recovery.html
new file mode 100644
index 0000000..29eb95a
--- /dev/null
+++ b/src/templates/user-requested-password-recovery.html
@@ -0,0 +1,18 @@
+{% extends "base-form2.html" %}
+
+{% block title %}{{ _("Requested password recovery") }}{% end block %}
+
+{% block body %}
+ <div class="page-header">
+ <h1>{{ _("Password recovery requested") }}</h1>
+ </div>
+
+ <div class="row">
+ <div class="span6">
+ <p>
+ {{ _("An email with instructions how to recover your password was send to your primary email address.") }}
+ </p>
+ <hr>
+ </div>
+ </div>
+{% end %}
diff --git a/src/templates/user-reset-password-fail.html b/src/templates/user-reset-password-fail.html
new file mode 100644
index 0000000..1daaef3
--- /dev/null
+++ b/src/templates/user-reset-password-fail.html
@@ -0,0 +1,18 @@
+{% extends "base-form2.html" %}
+
+{% block title %}{{ _("Password reset failed") }}{% end block %}
+
+{% block body %}
+ <div class="page-header">
+ <h1>{{ _("Password reset failed") }}</h1>
+ </div>
+
+ <div class="row">
+ <div class="span6">
+ <p>
+ {{ message }}
+ </p>
+ <hr>
+ </div>
+ </div>
+{% end %}
diff --git a/src/templates/user-reset-password-success.html b/src/templates/user-reset-password-success.html
new file mode 100644
index 0000000..f75b3a7
--- /dev/null
+++ b/src/templates/user-reset-password-success.html
@@ -0,0 +1,18 @@
+{% extends "base-form2.html" %}
+
+{% block title %}{{ _("Password reset succeeded") }}{% end block %}
+
+{% block body %}
+ <div class="page-header">
+ <h1>{{ _("Password reset succeeded") }}</h1>
+ </div>
+
+ <div class="row">
+ <div class="span6">
+ <p>
+ {{ _("Successfully reset your password") }}
+ </p>
+ <hr>
+ </div>
+ </div>
+{% end %}
diff --git a/src/templates/user-reset-password.html b/src/templates/user-reset-password.html
new file mode 100644
index 0000000..1fa07e4
--- /dev/null
+++ b/src/templates/user-reset-password.html
@@ -0,0 +1,36 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Register a new account") }}{% end block %}
+
+{% block body %}
+ <div class="page-header">
+ <h2>
+ {{ _("Reset password") }}
+ </h2>
+ </div>
+
+ <form class="form-horizontal" method="POST" action="">
+ {% raw xsrf_form_html() %}
+ <input type="hidden" name="code" value="{{ user.password_recovery_code }}">
+
+ <fieldset>
+ <div class="control-group">
+ <label class="control-label" for="password1">{{ _("Password") }}</label>
+ <div class="controls">
+ <input type="password" class="input-xlarge" id="password1" name="password1">
+ </div>
+ </div>
+
+ <div class="control-group">
+ <label class="control-label" for="password2">{{ _("Confirm password") }}</label>
+ <div class="controls">
+ <input type="password" class="input-xlarge" id="password2" name="password2">
+ </div>
+ </div>
+ </fieldset>
+
+ <div class="form-actions">
+ <button type="submit" class="btn btn-primary">{{ _("Reset password") }}</button>
+ </div>
+ </form>
+{% end block %}
diff --git a/src/web/__init__.py b/src/web/__init__.py
index 5be08d8..f44a123 100644
--- a/src/web/__init__.py
+++ b/src/web/__init__.py
@@ -118,6 +118,7 @@ class Application(tornado.web.Application):
(r"/logout", auth.LogoutHandler),
(r"/register", auth.RegisterHandler),
(r"/password-recovery", auth.PasswordRecoveryHandler),
+ (r"/password-reset", auth.PasswordResetHandler),
# User profiles
(r"/users", users.UsersHandler),
diff --git a/src/web/auth.py b/src/web/auth.py
index 4538db5..811b3e9 100644
--- a/src/web/auth.py
+++ b/src/web/auth.py
@@ -143,10 +143,48 @@ class PasswordRecoveryHandler(base.BaseHandler):
def post(self):
username = self.get_argument("name", None)
- if not username:
- return self.get()
+ with self.db.transaction():
+ user = self.backend.users.get_by_email(username) \
+ or self.backend.users.get_by_name(username)
+
+ if user:
+ user.forgot_password()
+
+ self.render("user-requested-password-recovery.html")
+
+
+class PasswordResetHandler(base.BaseHandler):
+ def get(self):
+ code = self.get_argument("code")
+
+ user = self.backend.users.get_by_password_recovery_code(code)
+ if not user:
+ raise tornado.web.HTTPError(400)
+
+ self.render("user-reset-password.html", user=user)
+
+ def post(self):
+ _ = self.locale.translate
+
+ code = self.get_argument("code")
+ pass1 = self.get_argument("password1")
+ pass2 = self.get_argument("password2")
+
+ user = self.backend.users.get_by_password_recovery_code(code)
+ if not user:
+ raise tornado.web.HTTPError(400)
+
+ if not pass1 == pass2:
+ return self.render("user-reset-password-fail.html",
+ message=_("Second password does not match"))
+
+ # XXX Check password strength
+
+ with self.db.transaction():
+ user.passphrase = pass1
+ user.password_recovery_code = None
- # XXX TODO
+ self.render("user-reset-password-success.html")
class LogoutHandler(base.BaseHandler):
--
2.11.0