change password 1

This commit is contained in:
Yuri Kuznetsov
2014-11-19 15:16:34 +02:00
parent 83916cacd9
commit 9b42ae41e7
13 changed files with 387 additions and 18 deletions
+20 -3
View File
@@ -25,11 +25,12 @@ namespace Espo\Controllers;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\NotFound;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\BadRequest;
class User extends \Espo\Core\Controllers\Record
{
{
public function actionAcl($params, $data, $request)
{
{
$userId = $request->get('id');
if (empty($userId)) {
throw new Error();
@@ -46,12 +47,28 @@ class User extends \Espo\Core\Controllers\Record
$acl = new \Espo\Core\Acl($user, $this->getConfig(), $this->getContainer()->get('fileManager'), $this->getMetadata());
return $acl->toArray();
return $acl->toArray();
}
public function actionChangeOwnPassword($params, $data)
{
return $this->getService('User')->changePassword($this->getUser()->id, $data['password']);
}
public function actionPasswordChangeRequest($params, $data, $request)
{
if (!$request->isPost()) {
throw new Forbidden();
}
if (empty($data['userName']) || empty($data['emailAddress'])) {
throw new BadRequest();
}
$userName = $data['userName'];
$emailAddress = $data['emailAddress'];
return $this->getService('User')->passwordChangeRequest($userName, $emailAddress);
}
}
@@ -0,0 +1,29 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
************************************************************************/
namespace Espo\Entities;
class PasswordChangeRequest extends \Espo\Core\ORM\Entity
{
}
@@ -129,7 +129,8 @@
"See more": "See more",
"Today": "Today",
"Tomorrow": "Tomorrow",
"Yesterday": "Yesterday"
"Yesterday": "Yesterday",
"Submit": "Submit"
},
"messages": {
"notModified": "You have not modified the record",
@@ -23,7 +23,13 @@
"Access": "Access",
"Preferences": "Preferences",
"Change Password": "Change Password",
"Teams and Access Control": "Teams and Access Control"
"Teams and Access Control": "Teams and Access Control",
"Forgot Password?": "Forgot Password?",
"Password Change Request": "Password Change Request",
"Username": "Username",
"Email Address": "Email Address",
"Password": "Password",
"Login": "Login"
},
"tooltips": {
"defaultTeam": "All records created by this user will be related to this team by default.",
@@ -36,6 +42,8 @@
"passwordWillBeSent": "Password will be sent to user's email address.",
"accountInfoEmailSubject": "Account info",
"accountInfoEmailBody": "Your account information:\n\nUsername: {userName}\nPassword: {password}\n\n{siteUrl}",
"passwordChangeLinkEmailSubject": "EspoCRM: Change Password Request",
"passwordChangeLinkEmailBody": "Open {link} to change your password. This unique url will be exprired soon.",
"passwordChanged": "Password has been changed"
}
}
@@ -1 +1 @@
["teams","title"]
["teams", "emailAddress", "title"]
@@ -0,0 +1,24 @@
{
"fields": {
"requestId": {
"type": "varchar",
"maxLength": 24,
"index": true
},
"user": {
"type": "link",
"readOnly": true,
"index": true
},
"createdAt": {
"type": "datetime",
"readOnly": true
}
},
"links": {
"user": {
"type": "belongsTo",
"entity": "User"
}
}
}
@@ -0,0 +1,7 @@
{
"entity": true,
"layouts": false,
"tab": false,
"acl": false,
"customizable": false
}
+12
View File
@@ -49,6 +49,18 @@
}
},
{
"route":"/PasswordChangeRequest",
"method": "post",
"params": {
"controller":"User",
"action": "passwordChangeRequest"
},
"conditions": {
"auth":false
}
},
{
"route":"/Stream",
"method":"get",
+106 -4
View File
@@ -30,22 +30,28 @@ use \Espo\ORM\Entity;
class User extends Record
{
const PASSWORD_CHANGE_REQUEST_LIFETIME = 360; // minutes
protected function init()
{
$this->dependencies[] = 'mailSender';
$this->dependencies[] = 'language';
$this->dependencies[] = 'container';
}
protected $internalFields = array('password');
protected function getMailSender()
{
return $this->injections['mailSender'];
return $this->getContainer()->get('mailSender');
}
protected function getLanguage()
{
return $this->injections['language'];
return $this->getContainer()->get('language');
}
protected function getContainer()
{
return $this->injections['container'];
}
public function getEntity($id)
@@ -91,6 +97,73 @@ class User extends Record
return true;
}
public function passwordChangeRequest($userName, $emailAddress)
{
$user = $this->getEntityManager()->getRepository('User')->where(array(
'userName' => $userName,
'emailAddress' => $emailAddress
))->findOne();
if (empty($user)) {
throw new NotFound();
}
$userId = $user->id;
$passwordChangeRequest = $this->getEntityManager()->getRepository('PasswordChangeRequest')->where(array(
'userId' => $userId
))->findOne();
if ($passwordChangeRequest) {
throw new Forbidden();
}
$requestId = uniqid();
$passwordChangeRequest = $this->getEntityManager()->getEntity('PasswordChangeRequest');
$passwordChangeRequest->set(array(
'userId' => $userId,
'requestId' => $requestId
));
$this->sendChangePasswordLink($requestId, $emailAddress);
$this->getEntityManager()->saveEntity($passwordChangeRequest);
if (!$passwordChangeRequest->id) {
throw new Error();
}
$dt = new \DateTime();
$dt->add(\DateTimeInterval('P'. self::PASSWORD_CHANGE_REQUEST_LIFETIME . 'i'));
$job->set(array(
'serviceName' => 'User',
'method' => 'removeChangePasswordRequestJob',
'data' => json_encode(array(
'id' => $passwordChangeRequest->id,
)),
'executeTime' => $dt->format('Y-m-d H:i:s') ,
));
$this->getEntityManager()->saveEntity($job);
return true;
}
public function removeChangePasswordRequestJob($data)
{
$id = $data->id;
if (empty($id)) {
return;
}
$p = $this->getEntityManager()->getEntity('PasswordChangeRequest', $data->id);
if ($p) {
$this->getEntityManager()->removeEntity($p);
}
return true;
}
protected function hashPassword($password)
{
$config = $this->getConfig();
@@ -168,6 +241,35 @@ class User extends Record
$this->getMailSender()->send($email);
}
protected function sendChangePasswordLink($requestId, $emailAddress, Entity $user = null)
{
if (empty($emailAddress)) {
return;
}
$email = $this->getEntityManager()->getEntity('Email');
if (!$this->getConfig()->get('smtpServer')) {
return;
}
$subject = $this->getLanguage()->translate('passwordChangeLinkEmailSubject', 'messages', 'User');
$body = $this->getLanguage()->translate('passwordChangeLinkEmailBody', 'messages', 'User');
$link = $this->getConfig()->get('siteUrl') . '?entryPoint=changePassword&id=' . $requestId;
$body = str_replace('{link}', $this->getConfig()->get('siteUrl'), $body);
$email->set(array(
'subject' => $subject,
'body' => $body,
'isHtml' => false,
'to' => $emailAddress
));
$this->getMailSender()->send($email);
}
public function deleteEntity($id)
{
if ($id == 'system') {
+5 -4
View File
@@ -1,5 +1,5 @@
<div class="container content">
<div class="col-md-4 col-md-offset-4 col-sm-8 col-sm-offset-2">
<div class="col-md-4 col-md-offset-4 col-sm-8 col-sm-offset-2">
<div id="login" class="panel panel-default">
<div class="panel-heading">
<img src="{{logoSrc}}"></img>
@@ -8,15 +8,16 @@
<div>
<form id="login-form" onsubmit="return false;">
<div class="form-group">
<label for="field-username">{{translate 'Username'}}</label>
<label for="field-username">{{translate 'Username' scope='User'}}</label>
<input type="text" name="username" id="field-userName" class="form-control" autocapitalize="off" autocorrect="off">
</div>
<div class="form-group">
<label for="login">{{translate 'Password'}}</label>
<label for="login">{{translate 'Password' scope='User'}}</label>
<input type="password" name="password" id="field-password" class="form-control">
</div>
<div>
<button type="submit" class="btn btn-primary" id="btn-login">{{translate 'Login'}}</button>
<a href="javascript:" class="btn btn-link pull-right" data-action="passwordChangeRequest">{{translate 'Forgot Password?' scope='User'}}</a>
<button type="submit" class="btn btn-primary" id="btn-login">{{translate 'Login' scope='User'}}</button>
</div>
</form>
</div>
@@ -0,0 +1,15 @@
<div class="cell cell-userName form-group">
<label class="field-label-userName control-label">{{translate 'Username' scope='User'}}</label>
<div class="field field-userName">
<input type="text" name="userName" class="form-control" autocomplete="off" autocorrect="off" autocapitalize="off">
</div>
</div>
<div class="cell cell-emailAddress form-group">
<label class="field-label-emailAddress control-label">{{translate 'Email Address' scope='User'}}</label>
<div class="field field-emailAddress">
<input type="text" name="emailAddress" class="form-control" autocomplete="off" autocorrect="off" autocapitalize="off">
</div>
</div>
<div class="msg-box hidden"></div>
+16 -4
View File
@@ -36,6 +36,9 @@ Espo.define('Views.Login', 'View', function (Dep) {
'submit #login-form': function (e) {
this.login();
return false;
},
'click a[data-action="passwordChangeRequest"]': function (e) {
this.showPasswordChangeRequest();
}
},
@@ -62,17 +65,17 @@ Espo.define('Views.Login', 'View', function (Dep) {
if (userName == '') {
var $el = $("#field-userName");
var message = this.getLanguage().translate('Username can not be empty!');
var message = this.getLanguage().translate('Username can not be empty', 'labels', 'User');
$el.popover({
placement: 'bottom',
content: message,
trigger: 'manual',
}).popover('show');
var cell = $el.closest('.form-group');
cell.addClass('has-error');
var $cell = $el.closest('.form-group');
$cell.addClass('has-error');
this.$el.one('mousedown click', function () {
cell.removeClass('has-error');
$cell.removeClass('has-error');
$el.popover('destroy');
});
return;
@@ -118,6 +121,15 @@ Espo.define('Views.Login', 'View', function (Dep) {
});
this.notify('Wrong username/password', 'error');
},
showPasswordChangeRequest: function () {
this.notify('Please wait...');
this.createView('passwordChangeRequest', 'Modals.PasswordChangeRequest', {
}, function (view) {
view.render();
view.notify(false);
});
}
});
});
@@ -0,0 +1,141 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
************************************************************************/
Espo.define('Views.Modals.PasswordChangeRequest', 'Views.Modal', function (Dep) {
return Dep.extend({
cssName: 'password-change-request',
template: 'modals.password-change-request',
setup: function () {
this.buttons = [
{
name: 'submit',
label: 'Submit',
style: 'danger',
onClick: function (dialog) {
this.submit();
}.bind(this)
},
{
name: 'cancel',
label: 'Cancel',
onClick: function (dialog) {
dialog.close();
}
}
];
this.header = this.translate('Password Change Request', 'labels', 'User');
},
submit: function () {
var $userName = this.$el.find('input[name="userName"]');
var $emailAddress = this.$el.find('input[name="emailAddress"]');
$userName.popover('destroy');
$emailAddress.popover('destroy');
var userName = $userName.val();
var emailAddress = $emailAddress.val();
var isValid = true;
if (userName == '') {
isValid = false;
var message = this.getLanguage().translate('Username can not be empty', 'labels', 'User');
$userName.popover({
placement: 'bottom',
content: message,
trigger: 'manual',
}).popover('show');
var $cellUserName = $userName.closest('.form-group');
$cellUserName.addClass('has-error');
$userName.one('mousedown click', function () {
$cellUserName.removeClass('has-error');
$userName.popover('destroy');
});
}
var isValid = true;
if (emailAddress == '') {
isValid = false;
var message = this.getLanguage().translate('Email Address can not be empty', 'labels', 'User');
$emailAddress.popover({
placement: 'bottom',
content: message,
trigger: 'manual',
}).popover('show');
var $cellEmailAddress = $emailAddress.closest('.form-group');
$cellEmailAddress.addClass('has-error');
$emailAddress.one('mousedown click', function () {
$cellEmailAddress.removeClass('has-error');
$emailAddress.popover('destroy');
});
}
if (!isValid) return;
$submit = this.$el.find('.msg-box');
$submit.addClass('disabled');
this.notify('Please wait...');
$.ajax({
url: 'PasswordChangeRequest',
type: 'POST',
data: JSON.stringify({
userName: userName,
emailAddress: emailAddress
}),
error: function (xhr) {
if (xhr.status == 404) {
this.notify(this.translate('Username/Email Address not found', 'labels', 'User'), 'error');
xhr.errorIsHandled = true;
}
$submit.removeClass('disabled');
}.bind(this)
}).done(function () {
var msg = this.translate('The unique link has been sent to the specified email address.', 'labels', 'User');
this.$el.find('.cell-userName').addClass('hidden');
this.$el.find('.cell-emailAddress').addClass('hidden');
$submit.addClass('hidden');
this.$el.find('.msg-box').removeClass('hidden');
this.$el.find('.msg-box').html('<span class="text-success">' + msg + '</span>');
}.bind(this));
}
});
});