diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf631d..f63da3e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ v0.9 --------------------- * Upgrade app folder structure: moved controllers from web to root, moved commands to console, * Moved migration component/views custom configuration to a separate module. +* Admin panel: Added user "Roles" fields to edit form. +* Issue #13: Registration is absent +* Issue #14: Reset password v0.8.6 --------------------- diff --git a/app/controllers/AuthController.php b/app/controllers/AuthController.php old mode 100644 new mode 100755 index 1494a0a..19aabaf --- a/app/controllers/AuthController.php +++ b/app/controllers/AuthController.php @@ -4,10 +4,13 @@ use Yii; use yii\filters\AccessControl; +use yii\web\NotFoundHttpException; use yii\web\Response; use yii\filters\VerbFilter; use app\forms\LoginForm; -use app\forms\ContactForm; +use app\forms\PasswordRequestForm; +use app\forms\PasswordUpdateForm; +use app\models\User; class AuthController extends Controller { @@ -44,7 +47,7 @@ public function behaviors() */ public function actionLogin() { - if ( ! Yii::$app->user->isGuest) { + if (! Yii::$app->user->isGuest) { return $this->goHome(); } @@ -71,4 +74,87 @@ public function actionLogout() return $this->goHome(); } + /** + * Register action. + * + * @return string|Response + */ + public function actionRegister() + { + if (!Yii::$app->user->isGuest) { + return $this->goHome(); + } + + $model = new RegisterForm(); + + if ($model->load(Yii::$app->request->post()) && $model->register()) { + Yii::$app->session->addFlash('success', 'You have been successfully registered'); + + return $this->goBack(); + } + + return $this->render('register', [ + 'model' => $model, + ]); + } + + /** + * Password request action + * + * @return string|Response + */ + public function actionPasswordRequest() + { + if (!Yii::$app->user->isGuest) { + return $this->goHome(); + } + + $model = new PasswordRequestForm(); + + if ($model->load(Yii::$app->request->post()) && $model->request()) { + Yii::$app->session->addFlash( + 'success', + 'If the email address is registered in the system, we would send a letter there shortly.' + ); + + return $this->goBack(); + } + + return $this->render('password-request', [ + 'model' => $model, + ]); + } + + /** + * Password update action + * + * @param string $token Password reset token. + * + * @return string|Response + * @throws NotFoundHttpException If token not found in DB. + */ + public function actionPasswordUpdate($token) + { + if (!Yii::$app->user->isGuest) { + return $this->goHome(); + } + + if (!User::isPasswordResetTokenValid($token)) { + throw new NotFoundHttpException('Page not found.'); + } + + $model = new PasswordUpdateForm(); + $model->resetToken = $token; + + if ($model->load(Yii::$app->request->post()) && $model->update()) { + Yii::$app->session->addFlash('success', 'Your password has been successfully updated!'); + + return $this->goBack(); + } + + return $this->render('password-update', [ + 'model' => $model, + ]); + } + } diff --git a/app/forms/PasswordRequestForm.php b/app/forms/PasswordRequestForm.php new file mode 100644 index 0000000..54f62c1 --- /dev/null +++ b/app/forms/PasswordRequestForm.php @@ -0,0 +1,70 @@ +validate()) { + $user = User::findByUsername($this->email); + + if (empty($user)) { + return true; //needed for security reasons + } + + $user->generatePasswordResetToken(); + $user->save(); + + Yii::$app->mailer->compose() + ->setTo($user->email) + ->setFrom(settings()->app->systemFriendlyEmail) + ->setSubject('Restore your password on ' . Yii::$app->name) + ->setTextBody($this->getMessageBody($user->password_reset_token)) + ->send(); + + return true; + } + + return false; + } + + /** + * Render a message with reset token + * + * @param string $resetToken + * + * @return string + */ + protected function getMessageBody($resetToken) + { + return 'To restore your password, please, follow this link ' . Url::to([ + 'auth/password-update', + 'token' => $resetToken, + ], true); + } +} \ No newline at end of file diff --git a/app/forms/PasswordUpdateForm.php b/app/forms/PasswordUpdateForm.php new file mode 100644 index 0000000..c721ce3 --- /dev/null +++ b/app/forms/PasswordUpdateForm.php @@ -0,0 +1,54 @@ + 'newPassword'], + ]; + } + + /** + * Updates user's password if $resetToken is valid + * + * @return bool + */ + public function update() + { + $user = User::findByPasswordResetToken($this->resetToken); + + if (empty($user)) { + return false; + } + + if ($this->validate()) { + $user->setPassword($this->newPassword); + $user->removePasswordResetToken(); + + if (!$user->save()) { + $this->addErrors($user->errors); + + return false; + } + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/app/forms/RegisterForm.php b/app/forms/RegisterForm.php new file mode 100644 index 0000000..e8ddf73 --- /dev/null +++ b/app/forms/RegisterForm.php @@ -0,0 +1,70 @@ + User::className()], + ['passwordRepeat', 'compare', 'compareAttribute' => 'password'], + ]; + } + + /** + * @return array customized attribute labels + */ + public function attributeLabels() + { + return [ + 'passwordRepeat' => 'Repeat Password', + ]; + } + + /** + * Registers a user + * + * @return bool whether the model passes validation + */ + public function register() + { + if ($this->validate()) { + $user = new User(); + $user->email = $user->username = $this->email; + $user->first_name = $this->firstName; + $user->last_name = $this->lastName; + + $user->setPassword($this->password); + $user->generateAuthKey(); + + if (!$user->save()) { + $this->addErrors($user->errors); + + return false; + } + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/app/models/User.php b/app/models/User.php index 24367ef..3247dcf 100755 --- a/app/models/User.php +++ b/app/models/User.php @@ -11,25 +11,25 @@ * User model * * @property integer $id - * @property string $username - * @property string $password_hash - * @property string $password_reset_token - * @property string $email - * @property string $first_name - * @property string $last_name - * @property string $auth_key + * @property string $username + * @property string $password_hash + * @property string $password_reset_token + * @property string $email + * @property string $first_name + * @property string $last_name + * @property string $auth_key * @property integer $status * @property integer $created_at * @property integer $updated_at - * @property string $password write-only password + * @property string $password write-only password */ class User extends ActiveRecord implements IdentityInterface { use WithStatus; - + const STATUS_ACTIVE = 10; const STATUS_BLOCKED = 0; - + /** * @inheritdoc */ @@ -37,7 +37,7 @@ public static function tableName() { return '{{%user}}'; } - + /** * @inheritdoc */ @@ -52,7 +52,7 @@ public function rules() ['email', 'email'], ]; } - + /** * User full name * (as first/last name) @@ -63,7 +63,7 @@ public function getFullName() { return "{$this->first_name} {$this->last_name}"; } - + /** * List of user status aliases * @@ -72,11 +72,21 @@ public function getFullName() public static function getStatusesList() { return [ - static::STATUS_ACTIVE => 'Active', + static::STATUS_ACTIVE => 'Active', static::STATUS_BLOCKED => 'Blocked', ]; } - + + /** + * @return array + */ + public static function getRolesList() + { + $roles = array_keys(Yii::$app->authManager->getRoles()); + + return array_combine($roles, $roles); + } + /** * Assign a role to user * @@ -86,14 +96,16 @@ public static function getStatusesList() */ public function assignRole($role) { - if (! Yii::$app->authManager->checkAccess($this->id, $role)) { + if (!Yii::$app->authManager->checkAccess($this->id, $role)) { $authRole = Yii::$app->authManager->getRole($role); Yii::$app->authManager->assign($authRole, $this->id); + return true; } + return false; } - + /** * @inheritdoc */ @@ -101,7 +113,7 @@ public static function findIdentity($id) { return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]); } - + /** * @inheritdoc */ @@ -109,7 +121,7 @@ public static function findIdentityByAccessToken($token, $type = null) { throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.'); } - + /** * Finds user by username * @@ -121,7 +133,7 @@ public static function findByUsername($username) { return static::findOne(['username' => $username, 'status' => self::STATUS_ACTIVE]); } - + /** * Finds user by password reset token * @@ -131,16 +143,16 @@ public static function findByUsername($username) */ public static function findByPasswordResetToken($token) { - if ( ! static::isPasswordResetTokenValid($token)) { + if (!static::isPasswordResetTokenValid($token)) { return null; } - + return static::findOne([ 'password_reset_token' => $token, 'status' => self::STATUS_ACTIVE, ]); } - + /** * Finds out if password reset token is valid * @@ -153,13 +165,13 @@ public static function isPasswordResetTokenValid($token) if (empty($token)) { return false; } - + $timestamp = (int)substr($token, strrpos($token, '_') + 1); - $expire = settings()->app->passwordResetToken; - + $expire = settings()->app->passwordResetToken; + return $timestamp + $expire >= time(); } - + /** * @inheritdoc */ @@ -167,7 +179,7 @@ public function getId() { return $this->getPrimaryKey(); } - + /** * @inheritdoc */ @@ -175,7 +187,7 @@ public function getAuthKey() { return $this->auth_key; } - + /** * @inheritdoc */ @@ -183,7 +195,7 @@ public function validateAuthKey($authKey) { return $this->getAuthKey() === $authKey; } - + /** * Validates password * @@ -195,7 +207,7 @@ public function validatePassword($password) { return Yii::$app->security->validatePassword($password, $this->password_hash); } - + /** * Generates password hash from password and sets it to the model * @@ -205,7 +217,7 @@ public function setPassword($password) { $this->password_hash = Yii::$app->security->generatePasswordHash($password); } - + /** * Generates "remember me" authentication key */ @@ -213,7 +225,7 @@ public function generateAuthKey() { $this->auth_key = Yii::$app->security->generateRandomString(); } - + /** * Generates new password reset token */ @@ -221,7 +233,7 @@ public function generatePasswordResetToken() { $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time(); } - + /** * Removes password reset token */ @@ -229,5 +241,5 @@ public function removePasswordResetToken() { $this->password_reset_token = null; } - + } diff --git a/app/modules/admin/controllers/UsersController.php b/app/modules/admin/controllers/UsersController.php index f8fac0b..8408125 100755 --- a/app/modules/admin/controllers/UsersController.php +++ b/app/modules/admin/controllers/UsersController.php @@ -2,11 +2,11 @@ namespace app\modules\admin\controllers; -use Yii; -use app\modules\admin\forms\UserForm; -use app\traits\controllers\FindModelOrFail; use app\models\User; +use app\modules\admin\forms\UserForm; use app\modules\admin\models\UserSearch; +use app\traits\controllers\FindModelOrFail; +use Yii; use yii\filters\VerbFilter; /** @@ -15,7 +15,7 @@ class UsersController extends Controller { use FindModelOrFail; - + /** * @inheritdoc */ @@ -24,7 +24,7 @@ public function init() parent::init(); $this->modelClass = UserForm::className(); } - + /** * @inheritdoc */ @@ -39,7 +39,7 @@ public function behaviors() ], ]; } - + /** * Lists all User models. * @@ -47,15 +47,15 @@ public function behaviors() */ public function actionIndex() { - $searchModel = new UserSearch(); + $searchModel = new UserSearch(); $dataProvider = $searchModel->search(Yii::$app->request->queryParams); - + return $this->render('index', [ 'searchModel' => $searchModel, 'dataProvider' => $dataProvider, ]); } - + /** * Creates a new User model. * If creation is successful, the browser will be redirected to the 'view' page. @@ -65,18 +65,18 @@ public function actionIndex() public function actionCreate() { $model = new UserForm(); - + $model->on(User::EVENT_BEFORE_INSERT, [$model, 'generateAuthKey']); - + if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['update', 'id' => $model->id]); } - + return $this->render('create', [ 'model' => $model, ]); } - + /** * Updates an existing User model. * If update is successful, the browser will be redirected to the 'view' page. @@ -87,17 +87,20 @@ public function actionCreate() */ public function actionUpdate($id) { + /** + * @var UserForm $model + */ $model = $this->findModel($id); - + if ($model->load(Yii::$app->request->post()) && $model->save()) { return $this->redirect(['update', 'id' => $model->id]); } - + return $this->render('update', [ 'model' => $model, ]); } - + /** * Deletes an existing User model. * If deletion is successful, the browser will be redirected to the 'index' page. @@ -109,8 +112,8 @@ public function actionUpdate($id) public function actionDelete($id) { $this->findModel($id)->delete(); - + return $this->redirect(['index']); } - + } diff --git a/app/modules/admin/forms/UserForm.php b/app/modules/admin/forms/UserForm.php index 53a804d..6cd579d 100755 --- a/app/modules/admin/forms/UserForm.php +++ b/app/modules/admin/forms/UserForm.php @@ -3,6 +3,7 @@ namespace app\modules\admin\forms; use app\models\User; +use Yii; class UserForm extends User { @@ -10,12 +11,17 @@ class UserForm extends User * @var string */ public $password; - + /** * @var string */ - public $password_repeat; - + public $passwordRepeat; + + /** + * @var string + */ + public $roles; + /** * @inheritdoc * @return array @@ -23,11 +29,20 @@ class UserForm extends User public function rules() { return array_merge(parent::rules(), [ - ['password', 'compare'], - ['password_repeat', 'safe'], + ['password', 'safe'], + [ + 'passwordRepeat', + 'required', + 'when' => function () { + return !empty($this->password); + }, + 'whenClient' => 'function() { return $.trim($("#userform-password").val()).length || false; }', + ], + ['passwordRepeat', 'compare', 'compareAttribute' => 'password'], + ['roles', 'in', 'range' => array_keys(static::getRolesList()), 'allowArray' => true], ]); } - + /** * @inheritdoc * @return bool @@ -37,6 +52,37 @@ public function beforeSave($insert) if ($this->password) { $this->setPassword($this->password); } + + $this->updateUserRoles($this->roles); + return parent::beforeSave($insert); } + + /** + * @inheritdoc + */ + public function afterFind() + { + $roles = array_keys(Yii::$app->authManager->getRolesByUser($this->id)); + $roles = array_combine($roles, $roles); + $this->roles = $roles; + + parent::afterFind(); + } + + /** + * @param array|string $roles + */ + protected function updateUserRoles($roles) + { + if (empty($roles)) { + return; + } + + Yii::$app->authManager->revokeAll($this->id); + + foreach ($roles as $role) { + $this->assignRole($role); + } + } } \ No newline at end of file diff --git a/app/modules/admin/views/users/_form.php b/app/modules/admin/views/users/_form.php index 1c05b50..6280664 100755 --- a/app/modules/admin/views/users/_form.php +++ b/app/modules/admin/views/users/_form.php @@ -1,40 +1,44 @@
Please fill out the following fields to login:
- + 'login-form', 'layout' => 'horizontal', @@ -24,11 +24,11 @@ 'labelOptions' => ['class' => 'col-lg-1 control-label'], ], ]); ?> - + = $form->field($model, 'username')->textInput(['autofocus' => true]) ?> - + = $form->field($model, 'password')->passwordInput() ?> - + = $form->field($model, 'rememberMe')->checkbox([ 'template' => "Don't have an account? = Html::a('Register!', ['auth/register']) ?>
+app\models\User::$users
.
+ Forgot your password? = Html::a('Restore!', ['auth/password-request']) ?>
+Enter your email to restore the password:
+ + 'forgot-password-form', + 'layout' => 'horizontal', + 'fieldConfig' => [ + 'template' => "{label}\nPlease fill out the following fields to login:
+ + 'register-form', + 'layout' => 'horizontal', + ]); ?> + + = $form->field($model, 'email')->textInput([ + 'autofocus' => true, + 'type' => 'email', + ]) ?> + = $form->field($model, 'firstName')->textInput() ?> + = $form->field($model, 'lastName')->textInput() ?> + = $form->field($model, 'password')->passwordInput() ?> + = $form->field($model, 'passwordRepeat')->passwordInput() ?> + +Already have an account? = Html::a('Login!', ['auth/login']) ?>
+