Yii2: Validate email after registration

Goals:
  1. Verify user email
    1. Deny login for unverified email
    2. Resend verification for expired/lost verification
    3. Verify within certain time frame
  2.  Recover lost password
    1. Recover within certain time frame
    2. Resend recovery for expired/lost recovery
 You may want to read this first.

Part 1: Configuration
  1. Edit config\db.php to point to your DB.
  2. 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`)
    );
  3. Using Gii, create their models. For Users, generate CRUD too.

  4. In config/web.php:
    1. Change the user identity class
      'components' => [
              'user' => [
                  'identityClass' => 'app\models\Users',
                  'enableAutoLogin' => false,
                 ],
    2. 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
                 ],
              ],
      
    3. Use Pretty URLs
      '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>',
                  ],
              ],
      Don't forget to create .htaccess file in web root folder:
      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
  5. 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
    ];
  6. 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; } }
  7. 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;
        }
    }
  8. 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">&copy; My Company <?= date('Y') ?></p> <p class="pull-right"><?= Yii::powered() ?></p> </div> </footer> <?php $this->endBody() ?> </body> </html> <?php $this->endPage() ?>
  9. 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>
  10. 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>
Part 2: Verifying User
  1. 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, ]); }
  2. 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,
            ]);
        }
  3. 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;
            }
    
        }
    }
  4. Add this alias in Users Controller:
    use app\models\ResendForm;
  5. 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); }
    }
  6. 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); }
    }
  7. 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>
    
  8. 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() ?>
  9. 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>
  10. 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>
    
By now you should be able to verify user's email in your Yii app. Give it a try!

Part 3: Recover Lost Password
  1. 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';
        }
    }
    
  2. Add this alias in Users Controller:
    use app\models\RecoveryForm;
  3. 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,
            ]);
        }
  4. Add this alias in Users Controller:
    use yii\helpers\Html;
  5. 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); } }
  6. Add this alias in Users Controller:
    use app\models\Token;
  7. 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); } }
  8. 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>
    
  9. 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>
  10. 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>
Credit:
https://www.cloudways.com/blog/user-authentication-yii2/
https://github.com/dektrium

Comments

Popular posts from this blog

Useful aliases

Enable Search Engine Friendly (Pretty URLs) in Yii2