[ADD] reset_password module

bzr revid: chs@openerp.com-20120615161108-3nvxx4o8b3ozjxvw
This commit is contained in:
Christophe Simonis 2012-06-15 18:11:08 +02:00
parent 77d954c4d9
commit 0fb2419d25
9 changed files with 373 additions and 0 deletions

View File

@ -0,0 +1,2 @@
import res_users
import controllers

View File

@ -0,0 +1,23 @@
{
'name': 'Reset Password',
'description': 'Allow users to reset their password from the login page',
'author': 'OpenERP SA',
'version': '1.0',
'category': 'Tools',
'website': 'http://www.openerp.com',
'installable': True,
'depends': ['anonymous', 'email_template'],
'data': [
'email_templates.xml',
'res_users.xml',
],
'js': [
'static/src/js/reset_password.js',
],
'css': [
'static/src/css/reset_password.css',
],
'qweb': [
'static/src/xml/reset_password.xml',
],
}

View File

@ -0,0 +1,17 @@
import simplejson
import urllib2
import werkzeug
from openerp.addons.web.common import http as oeweb
class ResetPassword(oeweb.Controller):
_cp_path = '/reset_password'
@oeweb.httprequest
def index(self, req, db, token):
req.session.authenticate(db, 'anonymous', 'anonymous', {})
url = '/web/webclient/home#client_action=reset_password&token=%s' % (token,)
redirect = werkzeug.utils.redirect(url)
cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
redirect.set_cookie('instance0|session_id', cookie_val)
return redirect

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record model="email.template" id="email_no_user">
<field name="name">Reset Password No User</field>
<field name="model_id" ref="base.model_res_company"/>
<field name="email_from"><![CDATA[${object.name} <${object.email}>]]></field>
<field name="email_to">(set by reset_password module)</field>
<field name="subject">Password reset attempt</field>
<field name="body_text"><![CDATA[
You (or someone else) enter this email address when asking for password reset for an OpenERP account on ${ctx['url']}.
However this email is not associated to any account.
If you have an OpenERP account at this url, please verify your email on your preferences.
If you don't have an OpenERP account, you can ignore this email.
For more information about OpenERP, visit http://www.openerp.com
Kind Regards.
]]></field>
</record>
<record model="email.template" id="email_reset_link">
<field name="name">Reset Password</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
<field name="email_to">(set by reset_password module)</field>
<field name="subject">Password reset</field>
<field name="body_text"><![CDATA[
You (or someone else) enter this email address when asking for password reset for an OpenERP account on ${ctx['url']}.
If you don't have asked for password reset, you can ignore this email.
To continue the password reset process, use the following link: ${object._rp_get_link()}
Kind Regards.
]]></field>
</record>
<record model="email.template" id="email_password_changed">
<field name="name">Password Changed</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
<field name="email_to">(set by reset_password module)</field>
<field name="subject">Password chaned</field>
<field name="body_text"><![CDATA[
Your password for ${ctx['url']} has been changed.
Kind Regards.
]]></field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,118 @@
import urlparse
import itsdangerous
from openerp.tools import config
from openerp.osv import osv, fields
TWENTY_FOUR_HOURS = 24 * 60 * 60
def serializer(dbname):
key = '%s.%s' % (dbname, config['admin_passwd'])
return itsdangerous.URLSafeTimedSerializer(key)
def generate_token(dbname, user):
s = serializer(dbname)
return s.dumps((user.id, user.user_email))
def valid_token(dbname, token, max_age=TWENTY_FOUR_HOURS):
try:
unsign_token(dbname, token, max_age)
return True
except itsdangerous.BadSignature:
return False
def unsign_token(dbname, token, max_age=TWENTY_FOUR_HOURS):
# TODO avoid replay by comparing timestamp with last connection date of user ? (need a query)
s = serializer(dbname)
return s.loads(token, max_age)
class res_users(osv.osv):
_inherit = 'res.users'
_sql_constraints = [
('email_uniq', 'UNIQUE (user_email)', 'You can not have two users with the same email!')
]
def _rp_send_email(self, cr, uid, email, tpl_name, res_id, context=None):
model, tpl_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reset_password', tpl_name)
assert model == 'email.template'
host = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '')
ctx = dict(context or {}, url=host)
msg_id = self.pool.get(model).send_mail(cr, uid, tpl_id, res_id, force_send=False, context=ctx)
MailMessage = self.pool.get('mail.message')
MailMessage.write(cr, uid, [msg_id], {'email_to': email}, context=context)
MailMessage.send(cr, uid, [msg_id], context=context)
def _rp_get_link(self, cr, uid, ids, context=None):
assert len(ids) == 1
user = self.browse(cr, uid, ids[0], context=context)
host = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', '')
token = generate_token(cr.dbname, user)
link = urlparse.urljoin(host, '/reset_password?db=%s&token=%s' % (cr.dbname, token))
return link
def send_reset_password_request(self, cr, uid, email, context=None):
uid = 1
ids = self.search(cr, uid, [('user_email', '=', email)], context=context)
assert len(ids) <= 1
if not ids:
_m, company_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company')
self._rp_send_email(cr, uid, email, 'email_no_user', company_id, context=context)
else:
self._rp_send_email(cr, uid, email, 'email_reset_link', ids[0], context=context)
return True
res_users()
class reset_pw_wizard(osv.TransientModel):
_name = 'reset_password.wizard'
_rec_name = 'pw'
_columns = {
'pw': fields.char('Password', size=64),
'cpw': fields.char('Confirm Password', size=64),
'token': fields.char('Token', size=128),
'state': fields.selection([(x, x) for x in 'draft done missmatch error'.split()], required=True),
}
_defaults = {
'state': 'draft',
}
def create(self, cr, uid, values, context=None):
# NOTE here, invalid values raises exceptions to avoid storing
# sensitive data into the database (which then are available to anyone)
token = values.get('token')
pw = values.get('pw')
cpw = values.get('cpw')
if pw != cpw:
raise osv.except_osv('Error', 'Passwords missmatch')
Users = self.pool.get('res.users')
try:
user_id, user_email = unsign_token(cr.dbname, token)
except Exception:
raise osv.except_osv('Error', 'Invalid token')
Users.write(cr, 1, user_id, {'password': pw}, context=context)
Users._rp_send_email(cr, 1, user_email, 'email_password_changed', user_id, context=context)
values = {'state': 'done'}
return super(reset_pw_wizard, self).create(cr, uid, values, context)
def change(self, cr, uid, ids, context=None):
return True
def onchange_token(self, cr, uid, ids, token, context=None):
if not valid_token(cr.dbname, token):
return {'value': {'state': 'error'}}
return {}
def onchange_pw(self, cr, uid, ids, pw, cpw, context=None):
if pw != cpw:
return {'value': {'state': 'missmatch'}}
return {'value': {'state': 'draft'}}

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- TODO get own css -->
<record id="reset_password_wizard_form_view" model="ir.ui.view">
<field name="name">reset_password.wizard.form</field>
<field name="model">reset_password.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Reset Password" version="7.0">
<field name="state" invisible="1"/>
<field name="token" on_change="onchange_token(token)" invisible="1"/>
<group colspan="4" states="draft,missmatch">
<field name="pw" required='1' on_change="onchange_pw(pw,cpw)"/>
<field name="cpw" required='1' on_change="onchange_pw(pw,cpw)"/>
<group colspan="4" states="missmatch">
<div>Passwords missmatch</div>
</group>
<group colspan="2" col="1">
<button string="Change Password" name="change" icon="gtk-dialog-authentication" attrs="{'readonly': [('state', '=', 'missmatch')]}"/>
</group>
</group>
<group colspan="4" states="error" col="1">
<div>Invalid or expired token</div>
<button special="cancel" string="Close"/>
</group>
<group colspan="4" states="done" col="1">
<div>Password changed. We sent you an email confirming the password change.</div>
<button special="cancel" string="Close"/>
</group>
</form>
</field>
</record>
<record id="action_reset" model="ir.actions.act_window">
<field name="name">Reset Password</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">reset_password.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,12 @@
.openerp .oe_login .oe_login_pane {
height: 152px;
}
.openerp .oe_login .oe_login_pane ul.oe_login_switch a {
color: #eeeeee;
margin: 0 8px;
}
.openerp .oe_login .oe_login_pane ul.oe_login_switch a:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,75 @@
openerp.reset_password = function(instance) {
var _t = instance.web._t;
instance.web.Login.include({
start: function() {
var $e = this.$element;
$e.find('.oe_login_switch a').click(function() {
$e.find('.oe_login_switch').toggle();
var $m = $e.find('form input[name=is_reset_pw]');
$m.attr('checked', !$m.is(':checked'));
});
return this._super();
},
on_submit: function(ev) {
if(ev) {
ev.preventDefault();
}
var $e = this.$element;
var db = $e.find("form [name=db]").val();
if (!db) {
this.do_warn(_t("Login"), _t("No database selected !"));
return false;
}
var $m = $e.find('form input[name=is_reset_pw]');
if ($m.is(':checked')) {
var email = $e.find('form input[name=email]').val()
return this.do_reset_password(db, email);
} else {
return this._super(ev);
}
},
do_reset_password: function(db, email) {
var self = this;
instance.connection.session_authenticate(db, 'anonymous', 'anonymous', true).pipe(function () {
var func = new instance.web.Model("res.users").get_func("send_reset_password_request");
return func(email).then(function(res) {
// show message
self.do_notify(_t('Reset Password'), _.str.sprintf(_t('We have sent an email to %s with further instructions'), email), true);
}, function(error, event) {
// no traceback please
event.preventDefault();
});
}).fail(function(error, event) {
// cannot log as anonymous or reset_password not installed
self.do_warn(_t('Reset Password'), _.str.sprintf(_t('Reset Password functionnality is not available for database %s'), db), true);
});
},
});
instance.reset_password = {};
instance.reset_password.ResetPassword = instance.web.Widget.extend({
init: function(parent, params) {
this._super(parent);
this.token = (params && params.token) || false;
},
start: function() {
this.do_action({
name: 'Reset Password',
type: 'ir.actions.act_window',
context: {default_token: this.token},
res_model: 'reset_password.wizard',
target: 'new',
views: [[false, 'form']],
});
}
});
instance.web.client_actions.add("reset_password", "instance.reset_password.ResetPassword");
};

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates id="template" xml:space="preserve">
<t t-extend="Login">
<t t-jquery="form ul:first">
// addClass does not work :(
this.attr('class', 'oe_login_switch');
</t>
<t t-jquery="form ul:first li:last" t-operation="after">
<li>
<a href="#">Forgot your password?</a>
</li>
</t>
<t t-jquery="form ul:first" t-operation="after">
<ul class="oe_login_switch" style="display:none;">
<li style="display:none;"><input type="checkbox" name="is_reset_pw"/></li>
<li>Email</li>
<li><input type="email" name="email"/></li>
<li><button name="submit">Reset Password</button></li>
<li><a href="#">&lt; Back</a></li>
</ul>
</t>
</t>
</templates>