Add forgot password feature

This commit is contained in:
Frederic Guillot
2016-01-09 17:28:31 -05:00
parent 03032c3190
commit 26e3996014
50 changed files with 997 additions and 33 deletions

View File

@@ -2,8 +2,6 @@
namespace Kanboard\Controller;
use Gregwar\Captcha\CaptchaBuilder;
/**
* Authentication controller
*
@@ -61,21 +59,6 @@ class Auth extends Base
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
/**
* Display captcha image
*
* @access public
*/
public function captcha()
{
$this->response->contentType('image/jpeg');
$builder = new CaptchaBuilder;
$builder->build();
$this->sessionStorage->captcha = $builder->getPhrase();
$builder->output();
}
/**
* Redirect the user after the authentication
*

View File

@@ -0,0 +1,29 @@
<?php
namespace Kanboard\Controller;
use Gregwar\Captcha\CaptchaBuilder;
/**
* Captcha Controller
*
* @package controller
* @author Frederic Guillot
*/
class Captcha extends Base
{
/**
* Display captcha image
*
* @access public
*/
public function image()
{
$this->response->contentType('image/jpeg');
$builder = new CaptchaBuilder;
$builder->build();
$this->sessionStorage->captcha = $builder->getPhrase();
$builder->output();
}
}

View File

@@ -40,6 +40,9 @@ class Config extends Base
$values = $this->request->getValues();
switch ($redirect) {
case 'application':
$values += array('password_reset' => 0);
break;
case 'project':
$values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'cfd_include_closed_tasks' => 0);
break;

View File

@@ -0,0 +1,120 @@
<?php
namespace Kanboard\Controller;
/**
* Password Reset Controller
*
* @package controller
* @author Frederic Guillot
*/
class PasswordReset extends Base
{
/**
* Show the form to reset the password
*/
public function create(array $values = array(), array $errors = array())
{
$this->checkActivation();
$this->response->html($this->template->layout('password_reset/create', array(
'errors' => $errors,
'values' => $values,
'no_layout' => true,
)));
}
/**
* Validate and send the email
*/
public function save()
{
$this->checkActivation();
$values = $this->request->getValues();
list($valid, $errors) = $this->passwordResetValidator->validateCreation($values);
if ($valid) {
$this->sendEmail($values['username']);
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
$this->create($values, $errors);
}
/**
* Show the form to set a new password
*/
public function change(array $values = array(), array $errors = array())
{
$this->checkActivation();
$token = $this->request->getStringParam('token');
$user_id = $this->passwordReset->getUserIdByToken($token);
if ($user_id !== false) {
$this->response->html($this->template->layout('password_reset/change', array(
'token' => $token,
'errors' => $errors,
'values' => $values,
'no_layout' => true,
)));
}
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
/**
* Set the new password
*/
public function update(array $values = array(), array $errors = array())
{
$this->checkActivation();
$token = $this->request->getStringParam('token');
$values = $this->request->getValues();
list($valid, $errors) = $this->passwordResetValidator->validateModification($values);
if ($valid) {
$user_id = $this->passwordReset->getUserIdByToken($token);
if ($user_id !== false) {
$this->user->update(array('id' => $user_id, 'password' => $values['password']));
$this->passwordReset->disable($user_id);
}
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
$this->change($values, $errors);
}
/**
* Send the email
*/
private function sendEmail($username)
{
$token = $this->passwordReset->create($username);
if ($token !== false) {
$user = $this->user->getByUsername($username);
$this->emailClient->send(
$user['email'],
$user['name'] ?: $user['username'],
t('Password Reset for Kanboard'),
$this->template->render('password_reset/email', array('token' => $token))
);
}
}
/**
* Check feature availability
*/
private function checkActivation()
{
if ($this->config->get('password_reset', 0) == 0) {
$this->response->redirect($this->helper->url->to('auth', 'login'));
}
}
}

View File

@@ -172,6 +172,20 @@ class User extends Base
)));
}
/**
* Display last password reset
*
* @access public
*/
public function passwordReset()
{
$user = $this->getUser();
$this->response->html($this->layout('user/password_reset', array(
'tokens' => $this->passwordReset->getAll($user['id']),
'user' => $user,
)));
}
/**
* Display last connections
*

View File

@@ -69,6 +69,7 @@ use Pimple\Container;
* @property \Kanboard\Model\Link $link
* @property \Kanboard\Model\Notification $notification
* @property \Kanboard\Model\OverdueNotification $overdueNotification
* @property \Kanboard\Model\PasswordReset $passwordReset
* @property \Kanboard\Model\Project $project
* @property \Kanboard\Model\ProjectActivity $projectActivity
* @property \Kanboard\Model\ProjectAnalytic $projectAnalytic
@@ -112,6 +113,7 @@ use Pimple\Container;
* @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification
* @property \Kanboard\Model\UserMetadata $userMetadata
* @property \Kanboard\Model\Webhook $webhook
* @property \Kanboard\Validator\PasswordResetValidator $passwordResetValidator
* @property \Psr\Log\LoggerInterface $logger
* @property \PicoDb\Database $db
* @property \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher

View File

@@ -12,6 +12,18 @@ use Kanboard\Core\Base;
*/
class App extends Base
{
/**
* Get config variable
*
* @access public
* @param string $param
* @return mixed
*/
public function config($param)
{
return $this->config->get($param);
}
/**
* Make sidebar menu active
*

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1089,4 +1089,16 @@ return array(
'Disable two-factor authentication' => 'Désactiver l\'authentification à deux-facteurs',
'Enable two-factor authentication' => 'Activer l\'authentification à deux-facteurs',
'There is no integration registered at the moment.' => 'Il n\'y a aucune intégration enregistrée pour le moment.',
'Password Reset for Kanboard' => 'Réinitialisation du mot de passe pour Kanboard',
'Forgot password?' => 'Mot de passe oublié ?',
'Enable "Forget Password"' => 'Activer la fonctionnalité « Mot de passe oublié »',
'Password Reset' => 'Réinitialisation du mot de passe',
'New password' => 'Nouveau mot de passe',
'Change Password' => 'Changer de mot de passe',
'To reset your password click on this link:' => 'Pour réinitialiser votre mot de passe cliquer sur ce lien :',
'Last Password Reset' => 'Dernières réinitialisation de mot de passe',
'The password has never been reinitialized.' => 'Le mot de passe n\'a jamais été réinitialisé.',
'Creation' => 'Création',
'Expiration' => 'Expiration',
'Password reset history' => 'Historique de la réinitialisation du mot de passe',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -1086,4 +1086,16 @@ return array(
// 'Disable two-factor authentication' => '',
// 'Enable two-factor authentication' => '',
// 'There is no integration registered at the moment.' => '',
// 'Password Reset for Kanboard' => '',
// 'Forgot password?' => '',
// 'Enable "Forget Password"' => '',
// 'Password Reset' => '',
// 'New password' => '',
// 'Change Password' => '',
// 'To reset your password click on this link:' => '',
// 'Last Password Reset' => '',
// 'The password has never been reinitialized.' => '',
// 'Creation' => '',
// 'Expiration' => '',
// 'Password reset history' => '',
);

View File

@@ -0,0 +1,93 @@
<?php
namespace Kanboard\Model;
/**
* Password Reset Model
*
* @package model
* @author Frederic Guillot
*/
class PasswordReset extends Base
{
/**
* SQL table name
*
* @var string
*/
const TABLE = 'password_reset';
/**
* Token duration (30 minutes)
*
* @var string
*/
const DURATION = 1800;
/**
* Get all tokens
*
* @access public
* @param integer $user_id
* @return array
*/
public function getAll($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->desc('date_creation')->limit(100)->findAll();
}
/**
* Generate a new reset token for a user
*
* @access public
* @param string $username
* @param integer $expiration
* @return boolean|string
*/
public function create($username, $expiration = 0)
{
$user_id = $this->db->table(User::TABLE)->eq('username', $username)->neq('email', '')->notNull('email')->findOneColumn('id');
if (! $user_id) {
return false;
}
$token = $this->token->getToken();
$result = $this->db->table(self::TABLE)->insert(array(
'token' => $token,
'user_id' => $user_id,
'date_expiration' => $expiration ?: time() + self::DURATION,
'date_creation' => time(),
'ip' => $this->request->getIpAddress(),
'user_agent' => $this->request->getUserAgent(),
'is_active' => 1,
));
return $result ? $token : false;
}
/**
* Get user id from the token
*
* @access public
* @param string $token
* @return integer
*/
public function getUserIdByToken($token)
{
return $this->db->table(self::TABLE)->eq('token', $token)->eq('is_active', 1)->gte('date_expiration', time())->findOneColumn('user_id');
}
/**
* Disable all tokens for a user
*
* @access public
* @param integer $user_id
* @return boolean
*/
public function disable($user_id)
{
return $this->db->table(self::TABLE)->eq('user_id', $user_id)->update(array('is_active' => 0));
}
}

View File

@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
const VERSION = 100;
const VERSION = 101;
function version_101(PDO $pdo)
{
$pdo->exec("
CREATE TABLE password_reset (
token VARCHAR(80) PRIMARY KEY,
user_id INT NOT NULL,
date_expiration INT NOT NULL,
date_creation INT NOT NULL,
ip VARCHAR(45) NOT NULL,
user_agent VARCHAR(255) NOT NULL,
is_active TINYINT(1) NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
}
function version_100(PDO $pdo)
{
@@ -1063,7 +1081,7 @@ function version_12(PDO $pdo)
CREATE TABLE remember_me (
id INT NOT NULL AUTO_INCREMENT,
user_id INT,
ip VARCHAR(40),
ip VARCHAR(45),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
@@ -1079,7 +1097,7 @@ function version_12(PDO $pdo)
id INT NOT NULL AUTO_INCREMENT,
auth_type VARCHAR(25),
user_id INT,
ip VARCHAR(40),
ip VARCHAR(45),
user_agent VARCHAR(255),
date_creation INT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,

View File

@@ -6,7 +6,25 @@ use PDO;
use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
const VERSION = 80;
const VERSION = 81;
function version_81(PDO $pdo)
{
$pdo->exec("
CREATE TABLE password_reset (
token VARCHAR(80) PRIMARY KEY,
user_id INTEGER NOT NULL,
date_expiration INTEGER NOT NULL,
date_creation INTEGER NOT NULL,
ip VARCHAR(45) NOT NULL,
user_agent VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
$pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
}
function version_80(PDO $pdo)
{
@@ -983,7 +1001,7 @@ function version_1(PDO $pdo)
CREATE TABLE remember_me (
id SERIAL PRIMARY KEY,
user_id INTEGER,
ip VARCHAR(40),
ip VARCHAR(45),
user_agent VARCHAR(255),
token VARCHAR(255),
sequence VARCHAR(255),
@@ -996,7 +1014,7 @@ function version_1(PDO $pdo)
id SERIAL PRIMARY KEY,
auth_type VARCHAR(25),
user_id INTEGER,
ip VARCHAR(40),
ip VARCHAR(45),
user_agent VARCHAR(255),
date_creation INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE

View File

@@ -6,7 +6,25 @@ use Kanboard\Core\Security\Token;
use Kanboard\Core\Security\Role;
use PDO;
const VERSION = 92;
const VERSION = 93;
function version_93(PDO $pdo)
{
$pdo->exec("
CREATE TABLE password_reset (
token TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
date_expiration INTEGER NOT NULL,
date_creation INTEGER NOT NULL,
ip TEXT NOT NULL,
user_agent TEXT NOT NULL,
is_active INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
)
");
$pdo->exec("INSERT INTO settings VALUES ('password_reset', '1')");
}
function version_92(PDO $pdo)
{

View File

@@ -126,7 +126,9 @@ class AuthenticationProvider implements ServiceProviderInterface
$acl->setRoleHierarchy(Role::APP_USER, array(Role::APP_PUBLIC));
$acl->add('Oauth', array('google', 'github', 'gitlab'), Role::APP_PUBLIC);
$acl->add('Auth', array('login', 'check', 'captcha'), Role::APP_PUBLIC);
$acl->add('Auth', array('login', 'check'), Role::APP_PUBLIC);
$acl->add('Captcha', '*', Role::APP_PUBLIC);
$acl->add('PasswordReset', '*', Role::APP_PUBLIC);
$acl->add('Webhook', '*', Role::APP_PUBLIC);
$acl->add('Task', 'readonly', Role::APP_PUBLIC);
$acl->add('Board', 'readonly', Role::APP_PUBLIC);

View File

@@ -32,6 +32,7 @@ class ClassProvider implements ServiceProviderInterface
'Link',
'Notification',
'OverdueNotification',
'PasswordReset',
'Project',
'ProjectActivity',
'ProjectAnalytic',
@@ -84,6 +85,9 @@ class ClassProvider implements ServiceProviderInterface
'UserFilterAutoCompleteFormatter',
'GroupAutoCompleteFormatter',
),
'Validator' => array(
'PasswordResetValidator',
),
'Core' => array(
'DateParser',
'Helper',

View File

@@ -205,6 +205,10 @@ class RouteProvider implements ServiceProviderInterface
$container['route']->addRoute('oauth/gitlab', 'oauth', 'gitlab');
$container['route']->addRoute('login', 'auth', 'login');
$container['route']->addRoute('logout', 'auth', 'logout');
// PasswordReset
$container['route']->addRoute('forgot-password', 'PasswordReset', 'create');
$container['route']->addRoute('forgot-password/change/:token', 'PasswordReset', 'change');
}
return $container;

View File

@@ -19,17 +19,22 @@
<?php if (isset($captcha) && $captcha): ?>
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
<img src="<?= $this->url->href('auth', 'captcha') ?>"/>
<?= $this->form->text('captcha', $values, $errors, array('required')) ?>
<img src="<?= $this->url->href('Captcha', 'image') ?>"/>
<?= $this->form->text('captcha', array(), $errors, array('required')) ?>
<?php endif ?>
<?php if (REMEMBER_ME_AUTH): ?>
<?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br/>
<?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?><br>
<?php endif ?>
<div class="form-actions">
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div>
<?php if ($this->app->config('password_reset') == 1): ?>
<div class="reset-password">
<?= $this->url->link(t('Forgot password?'), 'PasswordReset', 'create') ?>
</div>
<?php endif ?>
</form>
<?php endif ?>

View File

@@ -7,19 +7,21 @@
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Application URL'), 'application_url') ?>
<?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?><br/>
<?= $this->form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?>
<p class="form-help"><?= t('Example: http://example.kanboard.net/ (used by email notifications)') ?></p>
<?= $this->form->label(t('Language'), 'application_language') ?>
<?= $this->form->select('application_language', $languages, $values, $errors) ?><br/>
<?= $this->form->select('application_language', $languages, $values, $errors) ?>
<?= $this->form->label(t('Timezone'), 'application_timezone') ?>
<?= $this->form->select('application_timezone', $timezones, $values, $errors) ?><br/>
<?= $this->form->select('application_timezone', $timezones, $values, $errors) ?>
<?= $this->form->label(t('Date format'), 'application_date_format') ?>
<?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?><br/>
<?= $this->form->select('application_date_format', $date_formats, $values, $errors) ?>
<p class="form-help"><?= t('ISO format is always accepted, example: "%s" and "%s"', date('Y-m-d'), date('Y_m_d')) ?></p>
<?= $this->form->checkbox('password_reset', t('Enable "Forget Password"'), 1, $values['password_reset'] == 1) ?>
<?= $this->form->label(t('Custom Stylesheet'), 'application_stylesheet') ?>
<?= $this->form->textarea('application_stylesheet', $values, $errors) ?><br/>

View File

@@ -0,0 +1,16 @@
<div class="form-login">
<h2><?= t('Password Reset') ?></h2>
<form method="post" action="<?= $this->url->href('PasswordReset', 'update', array('token' => $token)) ?>">
<?= $this->form->csrf() ?>
<?= $this->form->label(t('New password'), 'password') ?>
<?= $this->form->password('password', $values, $errors) ?><br/>
<?= $this->form->label(t('Confirmation'), 'confirmation') ?>
<?= $this->form->password('confirmation', $values, $errors) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/>
</div>
</form>
</div>

View File

@@ -0,0 +1,17 @@
<div class="form-login">
<h2><?= t('Password Reset') ?></h2>
<form method="post" action="<?= $this->url->href('PasswordReset', 'save') ?>">
<?= $this->form->csrf() ?>
<?= $this->form->label(t('Username'), 'username') ?>
<?= $this->form->text('username', $values, $errors, array('autofocus', 'required')) ?>
<?= $this->form->label(t('Enter the text below'), 'captcha') ?>
<img src="<?= $this->url->href('Captcha', 'image') ?>"/>
<?= $this->form->text('captcha', array(), $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="<?= t('Change Password') ?>" class="btn btn-blue"/>
</div>
</form>
</div>

View File

@@ -0,0 +1,6 @@
<p><?= t('To reset your password click on this link:') ?></p>
<p><?= $this->url->to('PasswordReset', 'change', array('token' => $token), '', true) ?></p>
<hr>
Kanboard

View File

@@ -0,0 +1,26 @@
<div class="page-header">
<h2><?= t('Last Password Reset') ?></h2>
</div>
<?php if (empty($tokens)): ?>
<p class="alert"><?= t('The password has never been reinitialized.') ?></p>
<?php else: ?>
<table class="table-small table-fixed">
<tr>
<th class="column-20"><?= t('Creation') ?></th>
<th class="column-20"><?= t('Expiration') ?></th>
<th class="column-5"><?= t('Active') ?></th>
<th class="column-15"><?= t('IP address') ?></th>
<th><?= t('User agent') ?></th>
</tr>
<?php foreach ($tokens as $token): ?>
<tr>
<td><?= dt('%B %e, %Y at %k:%M %p', $token['date_creation']) ?></td>
<td><?= dt('%B %e, %Y at %k:%M %p', $token['date_expiration']) ?></td>
<td><?= $token['is_active'] == 0 ? t('No') : t('Yes') ?></td>
<td><?= $this->e($token['ip']) ?></td>
<td><?= $this->e($token['user_agent']) ?></td>
</tr>
<?php endforeach ?>
</table>
<?php endif ?>

View File

@@ -19,6 +19,9 @@
<li <?= $this->app->checkMenuSelection('user', 'sessions') ?>>
<?= $this->url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
</li>
<li <?= $this->app->checkMenuSelection('user', 'passwordReset') ?>>
<?= $this->url->link(t('Password reset history'), 'user', 'passwordReset', array('user_id' => $user['id'])) ?>
</li>
<?php endif ?>
<?= $this->hook->render('template:user:sidebar:information') ?>

36
app/Validator/Base.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
namespace Kanboard\Validator;
/**
* Base Validator
*
* @package validator
* @author Frederic Guillot
*/
class Base extends \Kanboard\Core\Base
{
/**
* Execute multiple validators
*
* @access public
* @param array $validators List of validators
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function executeValidators(array $validators, array $values)
{
$result = false;
$errors = array();
foreach ($validators as $method) {
list($result, $errors) = $this->$method($values);
if (! $result) {
break;
}
}
return array($result, $errors);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Kanboard\Validator;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Gregwar\Captcha\CaptchaBuilder;
/**
* Password Reset Validator
*
* @package validator
* @author Frederic Guillot
*/
class PasswordResetValidator extends Base
{
/**
* Validate creation
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateCreation(array $values)
{
return $this->executeValidators(array('validateFields', 'validateCaptcha'), $values);
}
/**
* Validate modification
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateModification(array $values)
{
$v = new Validator($values, array(
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is %d characters', 6), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords don\'t match')),
));
return array(
$v->execute(),
$v->getErrors(),
);
}
/**
* Validate fields
*
* @access protected
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
protected function validateFields(array $values)
{
$v = new Validator($values, array(
new Validators\Required('captcha', t('This value is required')),
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
));
return array(
$v->execute(),
$v->getErrors(),
);
}
/**
* Validate captcha
*
* @access protected
* @param array $values Form values
* @return boolean
*/
protected function validateCaptcha(array $values)
{
$result = true;
$errors = array();
if (! isset($this->sessionStorage->captcha)) {
$result = false;
} else {
$builder = new CaptchaBuilder;
$builder->setPhrase($this->sessionStorage->captcha);
$result = $builder->testPhrase(isset($values['captcha']) ? $values['captcha'] : '');
if (! $result) {
$errors['captcha'] = array(t('Invalid captcha'));
}
}
return array($result, $errors);;
}
}