Yii2: Validate email after registration
Goals:
Part 1: Configuration
Part 3: Recover Lost Password
https://www.cloudways.com/blog/user-authentication-yii2/
https://github.com/dektrium
- Verify user email
- Deny login for unverified email
- Resend verification for expired/lost verification
- Verify within certain time frame
- Recover lost password
- Recover within certain time frame
- Resend recovery for expired/lost recovery
Part 1: Configuration
- Edit config\db.php to point to your DB.
- Create table for users and verification token.
CREATE TABLE `users` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `email` varchar(255) NOT NULL, `password_hash` varchar(255) NOT NULL, `confirmed_at` datetime NOT NULL, `registration_ip` varchar(255) NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP, `last_login_at` datetime NOT NULL, `last_login_ip` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `email` (`email`) USING BTREE );CREATE TABLE `token` ( `uid` bigint unsigned NOT NULL, `code` varchar(32) COLLATE utf8_unicode_ci NOT NULL, `created_at` datetime NOT NULL, `type` smallint NOT NULL, PRIMARY KEY (`uid`), UNIQUE KEY `token_unique` (`uid`,`code`,`type`) ); -
Using Gii, create their models. For Users, generate CRUD too.
- In config/web.php:
-
Change the user identity class
'components' => [ 'user' => [ 'identityClass' => 'app\models\Users', 'enableAutoLogin' => false, ], -
Setup mailer. You can use Gmail for this.
'components' => [ 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', 'useFileTransport' => false, 'transport' => [ 'class' => 'Swift_SmtpTransport', 'host' => 'your_smtp_server', 'username' => 'your_user_name', 'password' => 'your_password', 'port' => '587', //smtp port 'encryption' => 'tls', //connection ], ], -
Use Pretty URLs
Don't forget to create .htaccess file in web root folder:'components' => [ 'urlManager' => [ 'class' => 'yii\web\UrlManager', // Hide index.php 'showScriptName' => false, // Use pretty URLs 'enablePrettyUrl' => true, 'rules' => [ 'gii' => 'gii', 'gii/<controller:\w+>' => 'gii/<controller>', 'gii/<controller:\w+>/<action:\w+>' => 'gii/<controller>/<action>', ], ],
RewriteEngine on # If a directory or a file exists, use the request directly RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Otherwise forward the request to index.php RewriteRule . index.php -
Modify config/params.php
<?php return [ 'adminEmail' => 'admin@example.com',/** The time before a confirmation token becomes invalid. */ 'confirmWithin' => 86400, // 24 hours /** @var int The time you want the user will be remembered without asking for credentials. */ 'rememberFor' => 1209600, // two weeks /** @var int The time before a recovery token becomes invalid. */ 'recoverWithin' => 21600, // 6 hours]; -
Modify model LoginForm (see this first to understand more).
<?php namespace app\models; use Yii; use yii\base\Model;use yii\helpers\Html;/** * LoginForm is the model behind the login form. * * @property User|null $user This property is read-only. * */ class LoginForm extends Model {public $email;public $password; public $rememberMe = true; private $_user = false; /** * @return array the validation rules. */ public function rules() { return [// email and password are both required [['email', 'password'], 'required'],// rememberMe must be a boolean value ['rememberMe', 'boolean'], // password is validated by validatePassword() ['password', 'validatePassword'],['email', 'email'], 'confirmationValidate' => [ 'email', function ($attribute) { if ($this->user !== null) { if (!$this->user->getIsConfirmed()) { $this->addError($attribute, 'You need to confirm your email address. '.Html::a('Click here', ['/users/resend']).' to resend the confirmation email'); } } } ],]; } /** * Validates the password. * This method serves as the inline validation for password. * * @param string $attribute the attribute currently being validated * @param array $params the additional name-value pairs given in the rule */ public function validatePassword($attribute, $params) { if (!$this->hasErrors()) { $user = $this->getUser(); if (!$user || !$user->validatePassword($this->password)) {$this->addError($attribute, 'Incorrect email or password.');} } } /** * Logs in a user using the provided email and password. * @return bool whether the user is logged in successfully */ public function login() { if ($this->validate()) {$this->user->last_login_at = date("Y-m-d H:i:s"); $this->user->last_login_ip = \Yii::$app->request->userIP; $this->user->save(0);return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0); } return false; } /** * Finds user by [[email]] * * @return User|null */ public function getUser() { if ($this->_user === false) {$this->_user = Users::findByEmail($this->email);} return $this->_user; } } -
Create component components/Mailer.php (create new folder components in the root of your application)
<?php namespace app\components; use Yii; use app\models\Token; use app\models\Users; use yii\base\Component; class Mailer extends Component { /** @var \yii\mail\BaseMailer Default: `Yii::$app->mailer` */ public $mailerComponent; /** @var string */ public $viewPath = '@app/views/mail'; /** @var string */ protected $welcomeSubject; /** @var string|array Default: `Yii::$app->params['adminEmail']` OR `no-reply@example.com` */ public $sender; /** @var string */ protected $recoverySubject; /** * Sends an email to a user after registration. * * @param User $user * @param Token $token * * @return bool */ public function sendWelcomeMessage(Users $user, Token $token = null) { return $this->sendMessage( $user->email, $this->getWelcomeSubject(), 'welcome', ['user' => $user, 'token' => $token] ); } /** * @param string $to * @param string $subject * @param string $view * @param array $params * * @return bool */ protected function sendMessage($to, $subject, $view, $params = []) { $mailer = $this->mailerComponent === null ? Yii::$app->mailer : Yii::$app->get($this->mailerComponent); $mailer->viewPath = $this->viewPath; if ($this->sender === null) { $this->sender = isset(Yii::$app->params['adminEmail']) ? Yii::$app->params['adminEmail'] : 'no-reply@example.com'; } return $mailer->compose($view, $params) ->setTo($to) ->setFrom($this->sender) ->setSubject($subject) ->send(); } /** * @return string */ public function getWelcomeSubject() { if ($this->welcomeSubject == null) { $this->setWelcomeSubject('Welcome to '. Yii::$app->name); } return $this->welcomeSubject; } /** * @param string $welcomeSubject */ public function setWelcomeSubject($welcomeSubject) { $this->welcomeSubject = $welcomeSubject; } /** * Sends an email to a user with recovery link. * * @param User $user * @param Token $token * * @return bool */ public function sendRecoveryMessage(Users $user, Token $token) { return $this->sendMessage( $user->email, $this->getRecoverySubject(), 'recovery', ['user' => $user, 'token' => $token] ); } /** * @return string */ public function getRecoverySubject() { if ($this->recoverySubject == null) { $this->setRecoverySubject('Complete password reset on '. Yii::$app->name); } return $this->recoverySubject; } /** * @param string $recoverySubject */ public function setRecoverySubject($recoverySubject) { $this->recoverySubject = $recoverySubject; } } - Modify views/layouts/main.php To have Sign Up menu
<?php /* @var $this \yii\web\View */ /* @var $content string */ use app\widgets\Alert; use yii\helpers\Html; use yii\bootstrap\Nav; use yii\bootstrap\NavBar; use yii\widgets\Breadcrumbs; use app\assets\AppAsset; AppAsset::register($this); ?> <?php $this->beginPage() ?> <!DOCTYPE html> <html lang="<?= Yii::$app->language ?>"> <head> <meta charset="<?= Yii::$app->charset ?>"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <?= Html::csrfMetaTags() ?> <title><?= Html::encode($this->title) ?></title> <?php $this->head() ?> </head> <body> <?php $this->beginBody() ?> <div class="wrap"> <?php$navItems=[ ['label' => 'Home', 'url' => ['/site/index']], ['label' => 'Status', 'url' => ['/status/index']], ['label' => 'About', 'url' => ['/site/about']], ['label' => 'Contact', 'url' => ['/site/contact']] ]; if (Yii::$app->user->isGuest) { array_push($navItems,['label' => 'Sign In', 'url' => ['/site/login']],['label' => 'Sign Up', 'url' => ['/users/create']]); } else { array_push($navItems,['label' => 'Logout (' . Yii::$app->user->identity->email . ')', 'url' => ['/site/logout'], 'linkOptions' => ['data-method' => 'post']] ); } echo Nav::widget([ 'options' => ['class' => 'navbar-nav navbar-right'], 'items' => $navItems, ]);?> <div class="container"> <?= Breadcrumbs::widget([ 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], ]) ?> <?= Alert::widget() ?> <?= $content ?> </div> </div> <footer class="footer"> <div class="container"> <p class="pull-left">© My Company <?= date('Y') ?></p> <p class="pull-right"><?= Yii::powered() ?></p> </div> </footer> <?php $this->endBody() ?> </body> </html> <?php $this->endPage() ?> -
Modify views/site/login.php to login using email.
<?php /* @var $this yii\web\View */ /* @var $form yii\bootstrap\ActiveForm */ /* @var $model app\models\LoginForm */ use yii\helpers\Html;use yii\bootstrap\ActiveForm;$this->title = 'Login'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="site-login"> <h1><?= Html::encode($this->title) ?></h1> <p>Please fill out the following fields to login:</p> <?php $form = ActiveForm::begin([ 'id' => 'login-form', 'layout' => 'horizontal', 'fieldConfig' => [ 'template' => "{label}\n<div class=\"col-lg-3\">{input}</div>\n<div class=\"col-lg-8\">{error}</div>", 'labelOptions' => ['class' => 'col-lg-1 control-label'], ], ]); ?><?= $form->field($model, 'email', ['errorOptions' => ['class' => 'help-block' ,'encode' => false]])->textInput(['autofocus' => true]) ?><?= $form->field($model, 'password')->passwordInput() ?> <?= $form->field($model, 'rememberMe')->checkbox([ 'template' => "<div class=\"col-lg-offset-1 col-lg-3\">{input} {label}</div>\n<div class=\"col-lg-8\">{error}</div>", ]) ?> <div class="form-group"> <div class="col-lg-offset-1 col-lg-11"> <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?><?= Html::a('Forget Password', ['/users/request'], ['class'=>'btn btn-primary']) ?></div> </div> <?php ActiveForm::end(); ?> <div class="col-lg-offset-1" style="color:#999;"> You may login with <strong>admin/admin</strong> or <strong>demo/demo</strong>.<br> To modify the email/password, please check out the code <code>app\models\User::$users</code>. </div> </div> -
Update view views/users/create.php to register new user
<?php use yii\helpers\Html;use yii\widgets\ActiveForm;/* @var $this yii\web\View */ /* @var $model app\models\Users */ /* @var $form ActiveForm */$this->title = 'Sign up'; $this->params['breadcrumbs'][] = $this->title;?><div class="row"> <div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"><?= Html::encode($this->title) ?></h3> </div> <div class="panel-body"> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'email') ?> <?= $form->field($model, 'password')->passwordInput() ?> <?= $form->field($model, 'password_repeat')->passwordInput() ?> <?= Html::submitButton('Sign up', ['class' => 'btn btn-success btn-block']) ?> <?php ActiveForm::end(); ?> </div> </div> <p class="text-center"> <?= Html::a('Already registered? Sign in!', ['/site/login']) ?> </p> </div> </div>
-
Modify action Create in Users Controller
/** * Creates a new Users model. * If creation is successful, the browser will be redirected to the 'view' page. * @return mixed */ public function actionCreate() { $model = new Users();if (!empty(Yii::$app->request->post())) { if ($model->register(Yii::$app->request->post())) { return $this->render('welcome', ['email' => $model->email]); Yii::$app->end(); } else { return $this->render('create', [ 'model' => $model, ]); Yii::$app->end(); }} return $this->render('create', [ 'model' => $model, ]); } -
Add these actions in Users Controller
/** * Confirms user's account. If confirmation was successful logs the user and shows success message. Otherwise * shows error message. * * @param int $id * @param string $code * * @return string * @throws \yii\web\HttpException */ public function actionConfirm($id, $code) { $user = Users::findOne($id); $user->attemptConfirmation($id, $code); return $this->redirect(['site/index']); } /** * Displays page where user can request new confirmation token. If resending was successful, displays message. * * @return string * @throws \yii\web\HttpException */ public function actionResend() { /** @var ResendForm $model */ $model = new ResendForm; if (!empty(Yii::$app->request->post())) { if ($model->load(\Yii::$app->request->post()) && $model->resend()) { return $this->redirect(['site/index']); Yii::$app->end(); } } return $this->render('resend', [ 'model' => $model, ]); } -
Create model models/ResendForm.php.
<?php namespace app\models; use app\components\Mailer; use app\models\Token; use yii\base\Model; /** * ResendForm gets user email address and if user with given email is registered it sends new confirmation message * to him in case he did not validate his email. * */ class ResendForm extends Model { /** * @var string */ public $email; /** * @inheritdoc */ public function rules() { return [ ['email', 'trim'], [['email'], 'required'], ['email', 'email'], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'email' => 'Email', ]; } /** * @inheritdoc */ public function formName() { return 'resend-form'; } /** * Creates new confirmation token and sends it to the user. * * @return bool */ public function resend() { if (!$this->validate()) { return false; } $user = Users::find()->where(['email' => $this->email])->one(); if ($user && !$user->getIsConfirmed()) { /** @var Token $token */ $token = new Token; $token->uid = $user->id; $token->type = Token::TYPE_CONFIRMATION; $token->save(false); $mailer = new Mailer; $mailer->sendWelcomeMessage($user, $token); \Yii::$app->session->setFlash( 'info', 'A message has been sent to your email address. It contains a confirmation link that you must click to complete registration.' ); return true; } else { \Yii::$app->session->setFlash('danger', 'Email not found'); return false; } } } -
Add this alias in Users Controller:
use app\models\ResendForm; -
Modify model Token
<?php namespace app\models; use Yii;use yii\db\ActiveRecord; use yii\helpers\Url;/** * This is the model class for table "token". * * @property string $uid * @property string $code * @property string $created_at * @property int $type */ class Token extends \yii\db\ActiveRecord {const TYPE_CONFIRMATION = 0;/** * {@inheritdoc} */ public static function tableName() { return 'token'; } /** * {@inheritdoc} */ public function rules() { return [[['uid', 'code', 'type'], 'required'],[['uid', 'type'], 'integer'], [['created_at'], 'safe'], [['code'], 'string', 'max' => 32], [['uid', 'code', 'type'], 'unique', 'targetAttribute' => ['uid', 'code', 'type']],[['uid'], 'exist', 'skipOnError' => true, 'targetClass' => Users::className(), 'targetAttribute' => ['uid' => 'id']],]; } /** * {@inheritdoc} */ public function attributeLabels() { return [ 'uid' => 'Uid', 'code' => 'Code', 'created_at' => 'Created At', 'type' => 'Type', ]; }/** * @return \yii\db\ActiveQuery */ public function getUsers() { return $this->hasOne(Users::className(), ['id' => 'uid']); } /** * @return bool Whether token has expired. */ public function isExpired() { switch ($this->type) { case self::TYPE_CONFIRMATION: $expirationTime = Yii::$app->params['confirmWithin']; break; default: throw new \RuntimeException(); } return (strtotime($this->created_at) + $expirationTime) < time(); } /** * @return string */ public function getUrl() { switch ($this->type) { case self::TYPE_CONFIRMATION: $route = '/users/confirm'; break; default: throw new \RuntimeException(); } return Url::to([$route, 'id' => $this->uid, 'code' => $this->code], true); } public function beforeSave($insert) { if ($insert) { static::deleteAll(['uid' => $this->uid, 'type' => $this->type]); $this->setAttribute('created_at', date('Y-m-d H:i:s')); $this->setAttribute('code', Yii::$app->security->generateRandomString()); } return parent::beforeSave($insert); }} -
Modify model Users
<?php namespace app\models; use Yii;use yii\base\NotSupportedException; use yii\db\ActiveRecord; use yii\web\IdentityInterface; use yii\helpers\Security; use yii\behaviors\TimestampBehavior; use yii\db\Expression; use yii\web\Application as WebApplication; use yii\helpers\Html; use app\components\Mailer;/** * This is the model class for table "users". * * @property string $id * @property string $email * @property string $password_hash */class Users extends \yii\db\ActiveRecord implements IdentityInterface{public $password; public $password_repeat;/** * {@inheritdoc} */ public static function tableName() { return 'users'; }public function behaviors() { return [ [ 'class' => TimestampBehavior::className(), 'createdAtAttribute' => 'created_at', 'updatedAtAttribute' => 'updated_at', 'value' => new Expression('NOW()'), ], ]; }/** * {@inheritdoc} */ public function rules() { return [[['email', 'password_hash'], 'string', 'max' => 255], ['password_hash', 'safe'], ['password', 'string', 'min' => 6], [['email', 'password'], 'required'], ['email', 'email'], ['email', 'unique'], ['password_repeat', 'compare', 'compareAttribute'=>'password', 'skipOnEmpty' => false, 'message'=>"Passwords do not match" ],]; } /** * {@inheritdoc} */ public function attributeLabels() { return [ 'id' => 'ID', 'email' => 'Email','password' => 'Password', 'password_repeat' => 'Repeat Password',]; }public static function findIdentity($id) { return static::findOne($id); } public static function findIdentityByAccessToken($token, $type = null) { return static::findOne(['access_token' => $token]); } public function getId() { return $this->id; } public function getAuthKey() { return $this->authKey; } public function validateAuthKey($authKey) { return $this->authKey === $authKey; } /** * Validates password * * @param string $password password to validate * @return bool if password provided is valid for current user */ public function validatePassword($password) { return password_verify($password, $this->password_hash); } /** * Finds user by email * * @param string $email * @return static|null */ public static function findByEmail($email) { return static::findOne(['email' => $email]); } public function register($post) { $transaction = $this->getDb()->beginTransaction(); try { $this->load($post); if ($this->validate()) { if ($this->save()) { $token = \Yii::createObject(['class' => Token::className(), 'type' => Token::TYPE_CONFIRMATION]); $token->link('users', $this); $transaction->commit(); $mailer = new Mailer; $mailer->sendWelcomeMessage($this, isset($token) ? $token : null); return true; } else { $transaction->rollBack(); return false; } } else { $transaction->rollBack(); $msg = ''; return false; } } catch (\Exception $e) { $transaction->rollBack(); \Yii::warning($e->getMessage()); throw $e; } } /** * @return bool Whether the user is confirmed or not. */ public function getIsConfirmed() { return $this->confirmed_at != '0000-00-00 00:00:00'; } /** * Attempts user confirmation. * * @param string $code Confirmation code. * * @return boolean */ public function attemptConfirmation($id, $code) { $token = Token::find()->where(['uid' =>$id, 'code' => $code, 'type' => Token::TYPE_CONFIRMATION])->one(); if ($token && !$token->isExpired()) { $token->delete(); if (($success = $this->confirm())) { \Yii::$app->user->login($this, Yii::$app->params['rememberFor']); $this->last_login_at = date("Y-m-d H:i:s"); $this->last_login_ip = \Yii::$app->request->userIP; $this->save(0); $message = 'Thank you, registration is now complete.'; } else { $message = 'Something went wrong and your account has not been confirmed.'; } } else { $success = false; $message = 'The confirmation link is invalid or expired. Please try '.Html::a('requesting', ['/users/resend']).' a new one.'; } Yii::$app->session->setFlash($success ? 'success' : 'danger', $message); return $success; } /** * Confirms the user by setting 'confirmed_at' field to current time. */ public function confirm() { $result = (bool) $this->updateAttributes(['confirmed_at' => date('Y-m-d H:i:s')]); return $result; } /** * Displays page where user can request new confirmation token. If resending was successful, displays message. * * @return string * @throws \yii\web\HttpException */ public function actionResend() { /** @var ResendForm $model */ $model = new ResendForm; if ($model->load(\Yii::$app->request->post()) && $model->resend()) { Yii::$app->session->setFlash('info', 'A new confirmation link has been sent'); return $this->redirect(['site/index']); } return $this->render('resend', [ 'model' => $model, ]); } public function beforeSave($insert) { if ($insert) { $this->setAttribute('registration_ip', \Yii::$app->request->userIP); } if (!empty($this->password)) { $this->setAttribute('password_hash', password_hash($this->password, PASSWORD_DEFAULT)); } return parent::beforeSave($insert); }} -
Create view views/mail/welcome.php
<?php use yii\helpers\Html; ?> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> Hello, </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> Your account on <?php echo Yii::$app->name; ?> has been created. </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> In order to complete your registration, please click the link below. </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> <?php echo Html::a(Html::encode($token->url), $token->url); ?> </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> If you cannot click the link, please try pasting the text into your browser. </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> If you did not make this request you can ignore this email. </p> -
You also need to create layout view for email: views/mail/layouts/html.php
<?php $this->beginPage() ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"> <head> <meta name="viewport" content="width=device-width" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <?php $this->head() ?> </head> <body bgcolor="#f6f6f6" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; margin: 0; padding: 0;"> <table class="body-wrap" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; width: 100%; margin: 0; padding: 20px;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"> <td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"></td> <td class="container" bgcolor="#FFFFFF" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0; border: 1px solid #f0f0f0;"> <div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"> <table style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; width: 100%; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"> <td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"> <?php $this->beginBody() ?> <?= $content ?> <?php $this->endBody() ?> </td> </tr> </table> </div> </td> <td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"></td> </tr> </table> <table class="footer-wrap" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; width: 100%; clear: both !important; margin: 0; padding: 0;"><tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"><td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"></td> <td class="container" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0;"> <div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; max-width: 600px; display: block; margin: 0 auto; padding: 20px;"> <table style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; width: 100%; margin: 0; padding: 0;"> <tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"> <td align="center" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.6; color: #666; font-weight: normal; margin: 0 0 10px; padding: 0;"> © <?= Yii::$app->name ?> <?= date('Y') ?>. </p> </td> </tr> </table> </div> </td> <td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 100%; line-height: 1.6; margin: 0; padding: 0;"></td> </tr> </table> </body> </html> <?php $this->endPage() ?> - Create view views/users/resend.php
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; $this->title = 'Request new confirmation message'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="row"> <div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"><?php echo Html::encode($this->title) ?></h3> </div> <div class="panel-body"> <?php $form = ActiveForm::begin([ 'id' => 'resend-form', 'enableAjaxValidation' => false, 'enableClientValidation' => true, ]); ?> <?php echo $form->field($model, 'email')->textInput(['autofocus' => true]); ?> <?php echo Html::submitButton('Continue', ['class' => 'btn btn-primary btn-block']); ?><br> <?php ActiveForm::end(); ?> </div> </div> </div> </div> -
Create view views/users/welcome.php
<?php use yii\bootstrap\Alert; $this->title = 'Welcome'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="row"> <div class="col-xs-12"> Your account has been created and a message with further instructions has been sent to <?php echo $email; ?>. </div> </div>
Part 3: Recover Lost Password
-
Create model models/RecoveryForm.php.
<?php namespace app\models; use app\components\Mailer; use yii\base\Model; /** * Model for collecting data on password recovery. * * @author Dmitry Erofeev <dmeroff@gmail.com> */ class RecoveryForm extends Model { const SCENARIO_REQUEST = 'request'; const SCENARIO_RESET = 'reset'; /** * @var string */ public $email; /** * @var string */ public $password; public $password_repeat; /** * @var Mailer */ protected $mailer; /** * @inheritdoc */ public function attributeLabels() { return [ 'email' => 'Email', 'password' => 'Password', 'password_repeat' => 'Repeat Password', ]; } /** * @inheritdoc */ public function scenarios() { return [ self::SCENARIO_REQUEST => ['email'], self::SCENARIO_RESET => ['password', 'password_repeat'], ]; } /** * @inheritdoc */ public function rules() { return [ ['email', 'trim'], ['email', 'required'], ['email', 'email'], ['password', 'required'], ['password', 'string', 'min' => 6], ['password_repeat', 'compare', 'compareAttribute'=>'password', 'skipOnEmpty' => false, 'message'=>"Passwords do not match"], ]; } /** * Sends recovery message. * * @return bool */ public function sendRecoveryMessage() { if (!$this->validate()) { return false; } $user = Users::find()->where(['email' => $this->email])->one(); if ($user) { /** @var Token $token */ $token = new Token; $token->uid = $user->id; $token->type = Token::TYPE_RECOVERY; $token->save(false); $mailer = new Mailer; if (!$mailer->sendRecoveryMessage($user, $token)) { return false; } \Yii::$app->session->setFlash( 'info', 'An email has been sent with instructions for resetting your password' ); return true; } else { \Yii::$app->session->setFlash('danger', 'Email not found'); return false; } } /** * Resets user's password. * * @param Token $token * * @return bool */ public function resetPassword(Token $token) { if (!$this->validate() || $token->users === null) { return false; } if ($token->users->resetPassword($this->password)) { \Yii::$app->session->setFlash('success', 'Your password has been changed successfully.'); $token->delete(); } else { \Yii::$app->session->setFlash( 'danger', 'An error occurred and your password has not been changed. Please try again later.' ); } return true; } /** * @inheritdoc */ public function formName() { return 'recovery-form'; } } -
Add this alias in Users Controller:
use app\models\RecoveryForm; -
Add these actions in Users Controller
/** * Shows page where user can request password recovery. * * @return string */ public function actionRequest() { /** @var RecoveryForm $model */ $model = new RecoveryForm; $model->scenario = RecoveryForm::SCENARIO_REQUEST; if (!empty(Yii::$app->request->post())) { if ($model->load(\Yii::$app->request->post()) && $model->sendRecoveryMessage()) { return $this->redirect(['site/index']); Yii::$app->end(); } } return $this->render('request', [ 'model' => $model, ]); } /** * Displays page where user can reset password. * * @param int $id * @param string $code * * @return string * @throws \yii\web\NotFoundHttpException */ public function actionReset($id, $code) { /** @var Token $token */ $token = Token::find()->where(['uid' => $id, 'code' => $code, 'type' => Token::TYPE_RECOVERY])->one(); if (!$token || $token->isExpired()) { \Yii::$app->session->setFlash( 'danger', 'Recovery link is invalid or expired. Please try '.Html::a('requesting', ['/users/request']).' a new one.' ); return $this->redirect(['site/login']); Yii::$app->end(); } /** @var RecoveryForm $model */ $model = new RecoveryForm; $model->scenario = RecoveryForm::SCENARIO_RESET; if (!empty(Yii::$app->request->post())) { if ($model->load(\Yii::$app->getRequest()->post()) && $model->resetPassword($token)) { \Yii::$app->session->setFlash( 'success', 'Password has been changed' ); return $this->redirect(['site/login']); Yii::$app->end(); } } return $this->render('reset', [ 'model' => $model, ]); } -
Add this alias in Users Controller:
use yii\helpers\Html; -
Modify model Token
<?php namespace app\models; use Yii; use yii\db\ActiveRecord; use yii\helpers\Url; /** * This is the model class for table "token". * * @property string $uid * @property string $code * @property string $created_at * @property int $type */ class Token extends \yii\db\ActiveRecord { const TYPE_CONFIRMATION = 0;const TYPE_RECOVERY = 1;/** * {@inheritdoc} */ public static function tableName() { return 'token'; } /** * {@inheritdoc} */ public function rules() { return [ [['uid', 'code', 'type'], 'required'], [['uid', 'type'], 'integer'], [['created_at'], 'safe'], [['code'], 'string', 'max' => 32], [['uid', 'code', 'type'], 'unique', 'targetAttribute' => ['uid', 'code', 'type']], [['uid'], 'exist', 'skipOnError' => true, 'targetClass' => Users::className(), 'targetAttribute' => ['uid' => 'id']], ]; } /** * {@inheritdoc} */ public function attributeLabels() { return [ 'uid' => 'Uid', 'code' => 'Code', 'created_at' => 'Created At', 'type' => 'Type', ]; } /** * @return \yii\db\ActiveQuery */ public function getUsers() { return $this->hasOne(Users::className(), ['id' => 'uid']); } /** * @return bool Whether token has expired. */ public function isExpired() { switch ($this->type) { case self::TYPE_CONFIRMATION: $expirationTime = Yii::$app->params['confirmWithin']; break;case self::TYPE_RECOVERY: $expirationTime = Yii::$app->params['recoverWithin']; break;default: throw new \RuntimeException(); } return (strtotime($this->created_at) + $expirationTime) < time(); } /** * @return string */ public function getUrl() { switch ($this->type) { case self::TYPE_CONFIRMATION: $route = '/users/confirm'; break; case self::TYPE_RECOVERY: $route = '/users/reset'; break; default: throw new \RuntimeException(); } return Url::to([$route, 'id' => $this->uid, 'code' => $this->code], true); } public function beforeSave($insert) { if ($insert) { static::deleteAll(['uid' => $this->uid, 'type' => $this->type]); $this->setAttribute('created_at', date('Y-m-d H:i:s')); $this->setAttribute('code', Yii::$app->security->generateRandomString()); } return parent::beforeSave($insert); } } -
Add this alias in Users Controller:
use app\models\Token; -
Modify model Users
<?php namespace app\models; use Yii; use yii\base\NotSupportedException; use yii\db\ActiveRecord; use yii\web\IdentityInterface; use yii\helpers\Security; use yii\behaviors\TimestampBehavior; use yii\db\Expression; use yii\web\Application as WebApplication; use yii\helpers\Html; use app\components\Mailer; /** * This is the model class for table "users". * * @property string $id * @property string $email * @property string $password_hash */ class Users extends \yii\db\ActiveRecord implements IdentityInterface { public $password; public $password_repeat; /** * {@inheritdoc} */ public static function tableName() { return 'users'; } public function behaviors() { return [ [ 'class' => TimestampBehavior::className(), 'createdAtAttribute' => 'created_at', 'updatedAtAttribute' => 'updated_at', 'value' => new Expression('NOW()'), ], ]; } /** * {@inheritdoc} */ public function rules() { return [ [['email', 'password_hash'], 'string', 'max' => 255], ['password_hash', 'safe'], ['password', 'string', 'min' => 6], [['email', 'password'], 'required'], ['email', 'email'], ['email', 'unique'], ['password_repeat', 'compare', 'compareAttribute'=>'password', 'skipOnEmpty' => false, 'message'=>"Passwords do not match" ], ]; } /** * {@inheritdoc} */ public function attributeLabels() { return [ 'id' => 'ID', 'email' => 'Email', 'password' => 'Password', 'password_repeat' => 'Repeat Password', ]; } public static function findIdentity($id) { return static::findOne($id); } public static function findIdentityByAccessToken($token, $type = null) { return static::findOne(['access_token' => $token]); } public function getId() { return $this->id; } public function getAuthKey() { return $this->authKey; } public function validateAuthKey($authKey) { return $this->authKey === $authKey; } /** * Validates password * * @param string $password password to validate * @return bool if password provided is valid for current user */ public function validatePassword($password) { return password_verify($password, $this->password_hash); } /** * Finds user by email * * @param string $email * @return static|null */ public static function findByEmail($email) { return static::findOne(['email' => $email]); } public function register($post) { $transaction = $this->getDb()->beginTransaction(); try { $this->load($post); if ($this->validate()) { if ($this->save()) { $token = \Yii::createObject(['class' => Token::className(), 'type' => Token::TYPE_CONFIRMATION]); $token->link('users', $this); $transaction->commit(); $mailer = new Mailer; $mailer->sendWelcomeMessage($this, isset($token) ? $token : null); return true; } else { $transaction->rollBack(); return false; } } else { $transaction->rollBack(); $msg = ''; return false; } } catch (\Exception $e) { $transaction->rollBack(); \Yii::warning($e->getMessage()); throw $e; } } /** * @return bool Whether the user is confirmed or not. */ public function getIsConfirmed() { return $this->confirmed_at != '0000-00-00 00:00:00'; } /** * Attempts user confirmation. * * @param string $code Confirmation code. * * @return boolean */ public function attemptConfirmation($id, $code) { $token = Token::find()->where(['uid' =>$id, 'code' => $code, 'type' => Token::TYPE_CONFIRMATION])->one(); if ($token && !$token->isExpired()) { $token->delete(); if (($success = $this->confirm())) { \Yii::$app->user->login($this, Yii::$app->params['rememberFor']); $this->last_login_at = date("Y-m-d H:i:s"); $this->last_login_ip = \Yii::$app->request->userIP; $this->save(0); $message = 'Thank you, registration is now complete.'; } else { $message = 'Something went wrong and your account has not been confirmed.'; } } else { $success = false; $message = 'The confirmation link is invalid or expired. Please try '.Html::a('requesting', ['/users/resend']).' a new one.'; } Yii::$app->session->setFlash($success ? 'success' : 'danger', $message); return $success; } /** * Confirms the user by setting 'confirmed_at' field to current time. */ public function confirm() { $result = (bool) $this->updateAttributes(['confirmed_at' => date('Y-m-d H:i:s')]); return $result; } /** * Displays page where user can request new confirmation token. If resending was successful, displays message. * * @return string * @throws \yii\web\HttpException */ public function actionResend() { /** @var ResendForm $model */ $model = new ResendForm; if ($model->load(\Yii::$app->request->post()) && $model->resend()) { Yii::$app->session->setFlash('info', 'A new confirmation link has been sent'); return $this->redirect(['site/index']); } return $this->render('resend', [ 'model' => $model, ]); }/** * Resets password. * * @param string $password * * @return bool */ public function resetPassword($password) { return (bool)$this->updateAttributes(['password_hash' => password_hash($password, PASSWORD_DEFAULT)]); }public function beforeSave($insert) { if ($insert) { $this->setAttribute('registration_ip', \Yii::$app->request->userIP); } if (!empty($this->password)) { $this->setAttribute('password_hash', password_hash($this->password, PASSWORD_DEFAULT)); } return parent::beforeSave($insert); } } -
Create view views/mail/recovery.php
<?php use yii\helpers\Html; ?> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> Hello </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> We have received a request to reset the password for your account on <?php echo Yii::$app->name; ?> Please click the link below to complete your password reset </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> <?php echo Html::a(Html::encode($token->url), $token->url); ?> </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> If you cannot click the link, please try pasting the text into your browser. </p> <p style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; font-weight: normal; margin: 0 0 10px; padding: 0;"> If you did not make this request you can ignore this email'. </p> -
Create view views/users/request.php
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; $this->title = 'Recover your password'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="row"> <div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"><?= Html::encode($this->title) ?></h3> </div> <div class="panel-body"> <?php $form = ActiveForm::begin([ 'id' => 'password-recovery-form', 'enableAjaxValidation' => false, 'enableClientValidation' => true, ]); ?> <?= $form->field($model, 'email')->textInput(['autofocus' => true]) ?> <?= Html::submitButton('Continue', ['class' => 'btn btn-primary btn-block']) ?><br> <?php ActiveForm::end(); ?> </div> </div> </div> </div> -
Create view views/users/reset.php
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; $this->title = 'Reset your password'; $this->params['breadcrumbs'][] = $this->title; ?> <div class="row"> <div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"><?= Html::encode($this->title) ?></h3> </div> <div class="panel-body"> <?php $form = ActiveForm::begin(); ?> <?= $form->field($model, 'password')->passwordInput() ?> <?= $form->field($model, 'password_repeat')->passwordInput() ?> <?= Html::submitButton('Finish', ['class' => 'btn btn-success btn-block']) ?><br> <?php ActiveForm::end(); ?> </div> </div> </div> </div>
https://www.cloudways.com/blog/user-authentication-yii2/
https://github.com/dektrium




Comments
Post a Comment