From 26e3996014936268f4acbfa214fa881af9320ddd Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 9 Jan 2016 17:28:31 -0500 Subject: Add forgot password feature --- app/Controller/Auth.php | 17 ---- app/Controller/Captcha.php | 29 ++++++ app/Controller/Config.php | 3 + app/Controller/PasswordReset.php | 120 +++++++++++++++++++++++++ app/Controller/User.php | 14 +++ app/Core/Base.php | 2 + app/Helper/App.php | 12 +++ app/Locale/bs_BA/translations.php | 12 +++ app/Locale/cs_CZ/translations.php | 12 +++ app/Locale/da_DK/translations.php | 12 +++ app/Locale/de_DE/translations.php | 12 +++ app/Locale/es_ES/translations.php | 12 +++ app/Locale/fi_FI/translations.php | 12 +++ app/Locale/fr_FR/translations.php | 12 +++ app/Locale/hu_HU/translations.php | 12 +++ app/Locale/id_ID/translations.php | 12 +++ app/Locale/it_IT/translations.php | 12 +++ app/Locale/ja_JP/translations.php | 12 +++ app/Locale/nb_NO/translations.php | 12 +++ app/Locale/nl_NL/translations.php | 12 +++ app/Locale/pl_PL/translations.php | 12 +++ app/Locale/pt_BR/translations.php | 12 +++ app/Locale/pt_PT/translations.php | 12 +++ app/Locale/ru_RU/translations.php | 12 +++ app/Locale/sr_Latn_RS/translations.php | 12 +++ app/Locale/sv_SE/translations.php | 12 +++ app/Locale/th_TH/translations.php | 12 +++ app/Locale/tr_TR/translations.php | 12 +++ app/Locale/zh_CN/translations.php | 12 +++ app/Model/PasswordReset.php | 93 +++++++++++++++++++ app/Schema/Mysql.php | 24 ++++- app/Schema/Postgres.php | 24 ++++- app/Schema/Sqlite.php | 20 ++++- app/ServiceProvider/AuthenticationProvider.php | 4 +- app/ServiceProvider/ClassProvider.php | 4 + app/ServiceProvider/RouteProvider.php | 4 + app/Template/auth/index.php | 11 ++- app/Template/config/application.php | 10 ++- app/Template/password_reset/change.php | 16 ++++ app/Template/password_reset/create.php | 17 ++++ app/Template/password_reset/email.php | 6 ++ app/Template/user/password_reset.php | 26 ++++++ app/Template/user/sidebar.php | 3 + app/Validator/Base.php | 36 ++++++++ app/Validator/PasswordResetValidator.php | 98 ++++++++++++++++++++ 45 files changed, 825 insertions(+), 32 deletions(-) create mode 100644 app/Controller/Captcha.php create mode 100644 app/Controller/PasswordReset.php create mode 100644 app/Model/PasswordReset.php create mode 100644 app/Template/password_reset/change.php create mode 100644 app/Template/password_reset/create.php create mode 100644 app/Template/password_reset/email.php create mode 100644 app/Template/user/password_reset.php create mode 100644 app/Validator/Base.php create mode 100644 app/Validator/PasswordResetValidator.php (limited to 'app') 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 * @@ -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 * 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 @@ +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 @@ +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 @@ -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 * 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 @@ -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 * 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 @@ +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 @@ form->label(t('Enter the text below'), 'captcha') ?> - - form->text('captcha', $values, $errors, array('required')) ?> + + form->text('captcha', array(), $errors, array('required')) ?> - form->checkbox('remember_me', t('Remember Me'), 1, true) ?>
+ form->checkbox('remember_me', t('Remember Me'), 1, true) ?>
+ app->config('password_reset') == 1): ?> +
+ url->link(t('Forgot password?'), 'PasswordReset', 'create') ?> +
+ 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 @@ form->csrf() ?> form->label(t('Application URL'), 'application_url') ?> - form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?>
+ form->text('application_url', $values, $errors, array('placeholder="http://example.kanboard.net/"')) ?>

form->label(t('Language'), 'application_language') ?> - form->select('application_language', $languages, $values, $errors) ?>
+ form->select('application_language', $languages, $values, $errors) ?> form->label(t('Timezone'), 'application_timezone') ?> - form->select('application_timezone', $timezones, $values, $errors) ?>
+ form->select('application_timezone', $timezones, $values, $errors) ?> form->label(t('Date format'), 'application_date_format') ?> - form->select('application_date_format', $date_formats, $values, $errors) ?>
+ form->select('application_date_format', $date_formats, $values, $errors) ?>

+ form->checkbox('password_reset', t('Enable "Forget Password"'), 1, $values['password_reset'] == 1) ?> + form->label(t('Custom Stylesheet'), 'application_stylesheet') ?> form->textarea('application_stylesheet', $values, $errors) ?>
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 @@ +
+

+
+ form->csrf() ?> + + form->label(t('New password'), 'password') ?> + form->password('password', $values, $errors) ?>
+ + form->label(t('Confirmation'), 'confirmation') ?> + form->password('confirmation', $values, $errors) ?> + +
+ +
+
+
\ 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 @@ +
+

+
+ form->csrf() ?> + + form->label(t('Username'), 'username') ?> + form->text('username', $values, $errors, array('autofocus', 'required')) ?> + + form->label(t('Enter the text below'), 'captcha') ?> + + form->text('captcha', array(), $errors, array('required')) ?> + +
+ +
+
+
\ 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 @@ +

+ +

url->to('PasswordReset', 'change', array('token' => $token), '', true) ?>

+ +
+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 @@ + + + +

+ + + + + + + + + + + + + + + + + + +
e($token['ip']) ?>e($token['user_agent']) ?>
+ \ 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 @@
  • app->checkMenuSelection('user', 'sessions') ?>> url->link(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
  • +
  • app->checkMenuSelection('user', 'passwordReset') ?>> + url->link(t('Password reset history'), 'user', 'passwordReset', array('user_id' => $user['id'])) ?> +
  • 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 @@ +$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 @@ +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);; + } +} -- cgit v1.2.3