From ecf68a9e0bdd4e7068460af92140044581e8d47c Mon Sep 17 00:00:00 2001 From: Rhilip Date: Tue, 4 Jun 2019 22:13:50 +0800 Subject: [PATCH] feat(Auth): Add UserRecover Form 1. Add UserRecover Form, So that users can recover their password if they forget. 2. Add main recover function in UserConfirmForm. BREAKING CHANGE: Database Stucture of `users_confirms` change --- README.md | 2 +- apps/controllers/AuthController.php | 29 +++++++++-- apps/models/form/UserConfirmForm.php | 68 ++++++++++++++++++++----- apps/models/form/UserRecoverForm.php | 73 +++++++++++++++++++++++++++ apps/models/form/UserRegisterForm.php | 13 +++-- apps/views/auth/confirm_success.php | 10 +++- apps/views/auth/login.php | 2 +- apps/views/auth/recover.php | 61 ++++++++++++++++++++++ apps/views/auth/recover_next_step.php | 25 +++++++++ 9 files changed, 259 insertions(+), 24 deletions(-) create mode 100644 apps/models/form/UserRecoverForm.php create mode 100644 apps/views/auth/recover.php create mode 100644 apps/views/auth/recover_next_step.php diff --git a/README.md b/README.md index 88706eb..fdce8f1 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Or you can join our chat group on Telegram -- [@ridpt](https://t.me/ridpt) | Library | Docs | |:--|:--| -| [Zui](https://github.com/easysoft/zui): an HTML5 front UI framework | http://zui.sexy/ | +| [Zui](https://github.com/easysoft/zui): an HTML5 front UI framework | http://zui.sexy/ ( Chinese Version ) | | [FortAwesome](https://github.com/FortAwesome/Font-Awesome): The iconic SVG, font, and CSS toolkit | https://fontawesome.com/icons?d=gallery | | [flag-css](https://github.com/7kfpun/flag-css): CSS for SVG country flags respecting the original ratio. | https://kfpun.com/flag-css/ | | [zxcvbn](https://github.com/dropbox/zxcvbn): Low-Budget Password Strength Estimation | https://lowe.github.io/tryzxcvbn/ | diff --git a/apps/controllers/AuthController.php b/apps/controllers/AuthController.php index c5a402d..9654952 100644 --- a/apps/controllers/AuthController.php +++ b/apps/controllers/AuthController.php @@ -8,9 +8,10 @@ namespace apps\controllers; -use apps\models\form\UserConfirmForm; use apps\models\User; use apps\models\form\UserLoginForm; +use apps\models\form\UserConfirmForm; +use apps\models\form\UserRecoverForm; use apps\models\form\UserRegisterForm; use Rid\Http\Controller; @@ -59,13 +60,35 @@ public function actionConfirm() ]); } else { $confirm->flush(); - return $this->render('auth/confirm_success'); + return $this->render('auth/confirm_success', ['action' => $confirm->action]); } } public function actionRecover() { - // TODO User Recover Action + if (app()->request->isPost()) { + $form = new UserRecoverForm(); + $form->setData(app()->request->post()); + $success = $form->validate(); + if (!$success) { + return $this->render('auth/error', [ + 'title' => 'Action Failed', + 'msg' => $form->getError() + ]); + } else { + $flush = $form->flush(); + if ($flush === true) { + return $this->render('auth/recover_next_step'); + } else { + return $this->render('auth/error', [ + 'title' => 'Confirm Failed', + 'msg' => $flush + ]); + } + } + } else { + return $this->render('auth/recover'); + } } public function actionLogin() diff --git a/apps/models/form/UserConfirmForm.php b/apps/models/form/UserConfirmForm.php index f312401..33f8a52 100644 --- a/apps/models/form/UserConfirmForm.php +++ b/apps/models/form/UserConfirmForm.php @@ -9,20 +9,30 @@ namespace apps\models\form; use apps\components\User\UserInterface; +use Rid\Helpers\StringHelper; use Rid\Validators\Validator; class UserConfirmForm extends Validator { + public $secret; + public $action; + + protected static $action_list = ['register', 'recover']; + protected $id; private $uid; - public $secret; + private $user_status; public static function inputRules() { return [ 'secret' => 'Required', + 'action' => [ + ['Required'], + ['InList', ['list' => self::$action_list], 'Unknown confirm action.'] + ], ]; } @@ -31,36 +41,68 @@ public static function callbackRules() return ['validConfirmSecret']; } + /** + * Verity The confirm secret and action exist in table `users_confirm` or not + */ protected function validConfirmSecret() { $record = app()->pdo->createCommand( 'SELECT `users_confirm`.`id`,`users_confirm`.`uid`,`users`.`status` FROM `users_confirm` LEFT JOIN `users` ON `users`.`id` = `users_confirm`.`uid` - WHERE `serect` = :uid LIMIT 1;')->bindParams([ - 'uid' => $this->secret + WHERE `serect` = :serect AND `action` = :action AND used = 0 LIMIT 1;')->bindParams([ + 'serect' => $this->secret , 'action' => $this->action ])->queryOne(); if ($record == false) { // It means this confirm key is not exist - $this->buildCallbackFailMsg('confirm key', 'This confirm key is not exist'); + $this->buildCallbackFailMsg('confirm key', 'This confirm key is not exist'); // FIXME msg return; } - if ($record['status'] !== UserInterface::STATUS_PENDING) { - $this->buildCallbackFailMsg('User', 'User Already Confirmed'); - return; - } $this->uid = $record['uid']; $this->id = $record['id']; + $this->user_status = $record['status']; } - public function flush() - { + protected function update_confirm_status() { + app()->pdo->createCommand('UPDATE `users_confirm` SET `used` = 1 WHERE id = :id')->bindParams([ + 'id' => $this->id + ])->execute(); + } + + public function flush_register() { + if ($this->user_status !== UserInterface::STATUS_PENDING) { + return 'user status is not pending , they may already confirmed or banned'; // FIXME msg + } + app()->pdo->createCommand('UPDATE `users` SET `status` = :s WHERE `id` = :uid')->bindParams([ 's' => UserInterface::STATUS_CONFIRMED, 'uid' => $this->uid ])->execute(); - app()->pdo->createCommand('DELETE FROM `users_confirm` WHERE id = :id')->bindParams([ - 'id' => $this->id - ])->execute(); + $this->update_confirm_status(); app()->redis->del('User:content_' . $this->uid); + return true; + } + + public function flush_recover() { + if ($this->user_status !== UserInterface::STATUS_CONFIRMED) { + return 'user status is not confirmed , they may still in pending or banned'; // FIXME msg + } + + // generate new password + $new_password = StringHelper::getRandomString(10); + app()->pdo->createCommand('UPDATE `users` SET `password` = :new_password WHERE `id` = :uid')->bindParams([ + 'new_password'=> password_hash($new_password, PASSWORD_DEFAULT), 'uid'=>$this->uid + ])->execute(); + $this->update_confirm_status(); + + // TODO Send user email to tell his new password. + + + + + return true; + } + + public function flush() { + return $this->{'flush_' . $this->action}(); // Magic function call } } diff --git a/apps/models/form/UserRecoverForm.php b/apps/models/form/UserRecoverForm.php new file mode 100644 index 0000000..e2f3db1 --- /dev/null +++ b/apps/models/form/UserRecoverForm.php @@ -0,0 +1,73 @@ + 'required | email', + ]; + } + + public static function callbackRules() + { + return ['validateCaptcha']; + } + + // TODO Add rate limit for user only can recover once in a time interval + + /** + * Check email in our database and send recover link to that email + * Notice: if this email is not exist in our database , will also return bool(true) for security reason. + * However, We will not send recover-confirm-link email. + * @return bool|string bool(true) means flush success , + * any other value (string) performs like error msg + */ + public function flush() { + // Check this email is in our database or not? + $user_info = app()->pdo->createCommand('SELECT `id`,`status` FROM `users` WHERE `email` = :email;')->bindParams([ + 'email' => $this->email + ])->queryOne(); + if ($user_info !== false) { + if ($user_info['status'] !== UserInterface::STATUS_CONFIRMED) { + return 'std_user_account_unconfirmed'; + } + + // Send user email to get comfirm link + $confirm_key = StringHelper::getRandomString(32); + app()->pdo->createCommand('INSERT INTO `users_confirm` (`uid`,`serect`,`action`) VALUES (:uid,:serect,:action)')->bindParams([ + 'uid' => $user_info['id'], 'serect' => $confirm_key, 'action' => $this->_action + ])->execute(); + $confirm_url = app()->request->root() . '/auth/confirm?' . http_build_query([ + 'secret' => $confirm_key, + 'action' => $this->_action + ]); + + $mail_sender = \Rid\Libraries\Mailer::newInstanceByConfig('libraries.[swiftmailer]'); + $mail_sender->send([$this->email], 'Please confirm your action to recover your password', "Click this link $confirm_url to confirm."); // FIXME change to email template + } + return true; + } + + +} diff --git a/apps/models/form/UserRegisterForm.php b/apps/models/form/UserRegisterForm.php index 48f9d56..953c974 100644 --- a/apps/models/form/UserRegisterForm.php +++ b/apps/models/form/UserRegisterForm.php @@ -46,6 +46,8 @@ class UserRegisterForm extends Validator private $leechtime; private $bonus; + protected $_action = 'register'; + public function setData($config) { parent::setData($config); @@ -271,13 +273,16 @@ public function flush() if ($this->confirm_way == 'email') { $confirm_key = StringHelper::getRandomString(32); - app()->pdo->createCommand('INSERT INTO `users_confirm` (uid,serect) VALUES (:uid,:serect)')->bindParams([ - 'uid' => $this->id, 'serect' => $confirm_key + app()->pdo->createCommand('INSERT INTO `users_confirm` (`uid`,`serect`,`action`) VALUES (:uid,:serect,:action)')->bindParams([ + 'uid' => $this->id, 'serect' => $confirm_key, 'action' => $this->_action ])->execute(); - $confirm_url = app()->request->root() . '/auth/confirm?secret=' . urlencode($confirm_key); + $confirm_url = app()->request->root() . '/auth/confirm?' . http_build_query([ + 'secret' => $confirm_key, + 'action' => $this->_action + ]); $mail_sender = \Rid\Libraries\Mailer::newInstanceByConfig('libraries.[swiftmailer]'); - $mail_sender->send([$this->email], 'Please confirm your accent', "Click this link $confirm_url to confirm."); + $mail_sender->send([$this->email], 'Please confirm your accent', "Click this link $confirm_url to confirm."); // FIXME change to email template } Site::writeLog($log_text, Site::LOG_LEVEL_MOD); diff --git a/apps/views/auth/confirm_success.php b/apps/views/auth/confirm_success.php index a4a6ef0..9dd5192 100644 --- a/apps/views/auth/confirm_success.php +++ b/apps/views/auth/confirm_success.php @@ -1,11 +1,13 @@ @@ -13,8 +15,12 @@ start('container') ?>
+

Your account is success Confirmed.

-

Click Login Page to login

+ +

Your password has been reset and new password has been send to your email, Please find it and login.

+ +

Click Login Page to login, Or wait 5 seconds to auto redirect.

end(); ?> @@ -22,6 +28,6 @@ end(); ?> diff --git a/apps/views/auth/login.php b/apps/views/auth/login.php index de493db..62be3e1 100644 --- a/apps/views/auth/login.php +++ b/apps/views/auth/login.php @@ -38,7 +38,7 @@
- +
+ +layout('auth/base') ?> + +start('container') ?> +
+
+
+
Recover lost user name or password
+
+
+ 1. Enter Your Email + +
+
+ +
+ + +
+
The Email when your sign account.
+
+ +
+ +
+
+
+ + +
+
Case insensitive.
+
+
+ insert('layout/captcha') ?> +
+
+
+ +
+ +
+
+
+ +
2. Follow The Confirm Link to reset your password.
+
3. Get new generate password from Email and login.
+
4. Set your own password in user panel.
+
+
+
+
+end(); ?> diff --git a/apps/views/auth/recover_next_step.php b/apps/views/auth/recover_next_step.php new file mode 100644 index 0000000..87979cf --- /dev/null +++ b/apps/views/auth/recover_next_step.php @@ -0,0 +1,25 @@ + + +layout('auth/base') ?> + +start('container') ?> +
+
+
+
Find the email~
+
+ We will send your a email contains the username and a recover link to reset your password if your emails exist in our site. + Check your email (Sometimes in Trash can), and be patient that the email may delay for some minutes. + If you don't receive for long minutes or meet other problems, Please contact with our group ASAP. +
+
+
+
+end(); ?>