diff options
author | Frederic Guillot <fred@kanboard.net> | 2016-01-09 17:28:31 -0500 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2016-01-09 17:28:31 -0500 |
commit | 26e3996014936268f4acbfa214fa881af9320ddd (patch) | |
tree | 5f7fa2c1b73e4443ce75e8919383bdf775492304 /app | |
parent | 03032c3190a27408d60e27f486a4ca472448e9dc (diff) |
Add forgot password feature
Diffstat (limited to 'app')
45 files changed, 825 insertions, 32 deletions
diff --git a/app/Controller/Auth.php b/app/Controller/Auth.php index cd1dd167..07e66070 100644 --- a/app/Controller/Auth.php +++ b/app/Controller/Auth.php @@ -2,8 +2,6 @@ namespace Kanboard\Controller; -use Gregwar\Captcha\CaptchaBuilder; - /** * Authentication controller * @@ -62,21 +60,6 @@ class Auth extends Base } /** - * 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 * * @access private diff --git a/app/Controller/Captcha.php b/app/Controller/Captcha.php new file mode 100644 index 00000000..fcf081ea --- /dev/null +++ b/app/Controller/Captcha.php @@ -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(); + } +} diff --git a/app/Controller/Config.php b/app/Controller/Config.php index c7097da3..4aee8553 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -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; diff --git a/app/Controller/PasswordReset.php b/app/Controller/PasswordReset.php new file mode 100644 index 00000000..ebc1f77a --- /dev/null +++ b/app/Controller/PasswordReset.php @@ -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')); + } + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index 8b6df44c..2a811219 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -173,6 +173,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 * * @access public diff --git a/app/Core/Base.php b/app/Core/Base.php index 905c7375..f32c1442 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -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 diff --git a/app/Helper/App.php b/app/Helper/App.php index 2015d896..0593795f 100644 --- a/app/Helper/App.php +++ b/app/Helper/App.php @@ -13,6 +13,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 * * @access public diff --git a/app/Locale/bs_BA/translations.php b/app/Locale/bs_BA/translations.php index 777810cd..dc8fc548 100644 --- a/app/Locale/bs_BA/translations.php +++ b/app/Locale/bs_BA/translations.php @@ -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' => '', ); diff --git a/app/Locale/cs_CZ/translations.php b/app/Locale/cs_CZ/translations.php index 70ea873b..8ed054fc 100644 --- a/app/Locale/cs_CZ/translations.php +++ b/app/Locale/cs_CZ/translations.php @@ -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' => '', ); diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 37504d66..35ae3036 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -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' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 69efb309..264b8aa7 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -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' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index e28e1757..e091d1ec 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -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' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 76f312fd..3543963f 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -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' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index f2e7dda2..016c16d7 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -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', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 54c4ba40..bbca9914 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -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' => '', ); diff --git a/app/Locale/id_ID/translations.php b/app/Locale/id_ID/translations.php index ab296437..57479050 100644 --- a/app/Locale/id_ID/translations.php +++ b/app/Locale/id_ID/translations.php @@ -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' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 7d8df85f..6b1d0b0b 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -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' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index f1d20c49..22783e1e 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -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' => '', ); diff --git a/app/Locale/nb_NO/translations.php b/app/Locale/nb_NO/translations.php index 67dac98c..cff7e11b 100644 --- a/app/Locale/nb_NO/translations.php +++ b/app/Locale/nb_NO/translations.php @@ -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' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index c52c20ea..e4f64cfb 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -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' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index dc95fbe1..5e70633d 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -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' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 7b291f26..fd994e6c 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -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' => '', ); diff --git a/app/Locale/pt_PT/translations.php b/app/Locale/pt_PT/translations.php index 44003b1b..b1652dd9 100644 --- a/app/Locale/pt_PT/translations.php +++ b/app/Locale/pt_PT/translations.php @@ -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' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 4ad5b593..8964b46f 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -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' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 07efed98..fe2401b7 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -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' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 46159d8b..a8746d67 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -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' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 0706163d..2e29348d 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -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' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 3b576485..89b3b33b 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -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' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index f5de4725..161d8d2f 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -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' => '', ); diff --git a/app/Model/PasswordReset.php b/app/Model/PasswordReset.php new file mode 100644 index 00000000..c2d7dde9 --- /dev/null +++ b/app/Model/PasswordReset.php @@ -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)); + } +} diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index b42e9661..c98e083e 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -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, diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 65128eb5..961d8f4d 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -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 diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index f430c00b..f1be0cf1 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -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) { diff --git a/app/ServiceProvider/AuthenticationProvider.php b/app/ServiceProvider/AuthenticationProvider.php index 6e7c49d9..7617ba95 100644 --- a/app/ServiceProvider/AuthenticationProvider.php +++ b/app/ServiceProvider/AuthenticationProvider.php @@ -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); diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 2e207aa5..e206ef68 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -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', diff --git a/app/ServiceProvider/RouteProvider.php b/app/ServiceProvider/RouteProvider.php index e58d50c7..ce66090b 100644 --- a/app/ServiceProvider/RouteProvider.php +++ b/app/ServiceProvider/RouteProvider.php @@ -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; diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php index 2f75b113..a1059d6f 100644 --- a/app/Template/auth/index.php +++ b/app/Template/auth/index.php @@ -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 ?> diff --git a/app/Template/config/application.php b/app/Template/config/application.php index 7d4c811d..ec7d8462 100644 --- a/app/Template/config/application.php +++ b/app/Template/config/application.php @@ -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/> diff --git a/app/Template/password_reset/change.php b/app/Template/password_reset/change.php new file mode 100644 index 00000000..310f0f97 --- /dev/null +++ b/app/Template/password_reset/change.php @@ -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>
\ No newline at end of file diff --git a/app/Template/password_reset/create.php b/app/Template/password_reset/create.php new file mode 100644 index 00000000..ef958011 --- /dev/null +++ b/app/Template/password_reset/create.php @@ -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>
\ No newline at end of file diff --git a/app/Template/password_reset/email.php b/app/Template/password_reset/email.php new file mode 100644 index 00000000..62788b49 --- /dev/null +++ b/app/Template/password_reset/email.php @@ -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
\ No newline at end of file diff --git a/app/Template/user/password_reset.php b/app/Template/user/password_reset.php new file mode 100644 index 00000000..b4c9a0c4 --- /dev/null +++ b/app/Template/user/password_reset.php @@ -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 ?>
\ No newline at end of file diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php index 7756126e..9f745568 100644 --- a/app/Template/user/sidebar.php +++ b/app/Template/user/sidebar.php @@ -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') ?> diff --git a/app/Validator/Base.php b/app/Validator/Base.php new file mode 100644 index 00000000..6c56e2fd --- /dev/null +++ b/app/Validator/Base.php @@ -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); + } +} diff --git a/app/Validator/PasswordResetValidator.php b/app/Validator/PasswordResetValidator.php new file mode 100644 index 00000000..6f21cbca --- /dev/null +++ b/app/Validator/PasswordResetValidator.php @@ -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);; + } +} |