diff --git a/src/User/Bootstrap.php b/src/User/Bootstrap.php index 0f36307..b1b69ac 100644 --- a/src/User/Bootstrap.php +++ b/src/User/Bootstrap.php @@ -24,6 +24,10 @@ use yii\console\Application as ConsoleApplication; use yii\i18n\PhpMessageSource; use yii\web\Application as WebApplication; +use yii\base\Event; +use Da\User\Event\FormEvent; +use Da\User\Controller\SecurityController; + /** * Bootstrap class of the yii2-usuario extension. Configures container services, initializes translations, * builds class map, and does the other setup actions participating in the application bootstrap process. @@ -88,6 +92,7 @@ class Bootstrap implements BootstrapInterface // services $di->set(Service\AccountConfirmationService::class); $di->set(Service\EmailChangeService::class); + $di->set(Service\PasswordExpireService::class); $di->set(Service\PasswordRecoveryService::class); $di->set(Service\ResendConfirmationService::class); $di->set(Service\ResetPasswordService::class); @@ -141,6 +146,19 @@ class Bootstrap implements BootstrapInterface $di->set(Search\RoleSearch::class); } + // Attach an event to check if the password has expired + Event::on(SecurityController::class, FormEvent::EVENT_AFTER_LOGIN, function (FormEvent $event) { + if (is_null(Yii::$app->getModule('user')->maxPasswordAge)) { + return; + } + $user = $event->form->user; + if ($user->password_age >= Yii::$app->getModule('user')->maxPasswordAge) { + // Force password change + Yii::$app->session->setFlash('warning', Yii::t('usuario', 'Your password has expired, you must change it now')); + Yii::$app->response->redirect(['/user/settings/account'])->send(); + } + }); + if ($app instanceof WebApplication) { // override Yii $di->set( diff --git a/src/User/Controller/AdminController.php b/src/User/Controller/AdminController.php index f0ddf23..9af1113 100644 --- a/src/User/Controller/AdminController.php +++ b/src/User/Controller/AdminController.php @@ -18,6 +18,7 @@ use Da\User\Model\Profile; use Da\User\Model\User; use Da\User\Query\UserQuery; use Da\User\Search\UserSearch; +use Da\User\Service\PasswordExpireService; use Da\User\Service\PasswordRecoveryService; use Da\User\Service\SwitchIdentityService; use Da\User\Service\UserBlockService; @@ -329,4 +330,20 @@ class AdminController extends Controller return $this->redirect(['index']); } + + /** + * Forces the user to change password at next login + * @param integer $id + */ + public function actionForcePasswordChange($id) + { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + if ($this->make(PasswordExpireService::class, [$user])->run()) { + Yii::$app->session->setFlash("success", Yii::t('usuario', 'User will be required to change password at next login')); + } else { + Yii::$app->session->setFlash("danger", Yii::t('usuario', 'There was an error in saving user')); + } + $this->redirect(['index']); + } } diff --git a/src/User/Migration/m000000_000007_enable_password_expiration.php b/src/User/Migration/m000000_000007_enable_password_expiration.php new file mode 100644 index 0000000..dcee448 --- /dev/null +++ b/src/User/Migration/m000000_000007_enable_password_expiration.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Da\User\Migration; + +use yii\db\Migration; + +class m000000_000007_enable_password_expiration extends Migration +{ + public function safeUp() + { + $this->addColumn('{{%user}}', 'last_password_change', $this->timestamp()->null()); + } + + public function safeDown() + { + $this->dropColumn('{{%user}}', 'last_password_change'); + } +} diff --git a/src/User/Model/User.php b/src/User/Model/User.php index 7fc3771..b85147c 100644 --- a/src/User/Model/User.php +++ b/src/User/Model/User.php @@ -19,6 +19,7 @@ use Yii; use yii\base\NotSupportedException; use yii\behaviors\TimestampBehavior; use yii\db\ActiveRecord; +use yii\db\Expression; use yii\helpers\ArrayHelper; use yii\web\Application; use yii\web\IdentityInterface; @@ -46,6 +47,8 @@ use yii\web\IdentityInterface; * @property int $created_at * @property int $updated_at * @property int $last_login_at + * @property int $last_password_change + * @property int $password_age * * Defined relations: * @property SocialNetworkAccount[] $socialNetworkAccounts @@ -88,6 +91,7 @@ class User extends ActiveRecord implements IdentityInterface 'password_hash', $security->generatePasswordHash($this->password, $this->getModule()->blowfishCost) ); + $this->last_password_change = new Expression("NOW()"); } return parent::beforeSave($insert); @@ -138,6 +142,8 @@ class User extends ActiveRecord implements IdentityInterface 'created_at' => Yii::t('usuario', 'Registration time'), 'confirmed_at' => Yii::t('usuario', 'Confirmation time'), 'last_login_at' => Yii::t('usuario', 'Last login'), + 'last_password_change' => Yii::t('usuario', 'Last password change'), + 'password_age' => Yii::t('usuario', 'Password age'), ]; } @@ -312,4 +318,17 @@ class User extends ActiveRecord implements IdentityInterface { throw new NotSupportedException('Method "' . __CLASS__ . '::' . __METHOD__ . '" is not implemented.'); } + + /** + * Returns password age in days + * @return integer + */ + public function getPassword_age() + { + if (is_null($this->last_password_change)) { + return $this->getModule()->maxPasswordAge; + } + $d = new \DateTime($this->last_password_change); + return $d->diff(new \DateTime(), true)->format("%a"); + } } diff --git a/src/User/Module.php b/src/User/Module.php index 53b54b5..f6ee68f 100644 --- a/src/User/Module.php +++ b/src/User/Module.php @@ -128,4 +128,8 @@ class Module extends BaseModule * @var string the session key name to impersonate users. Please, modify it for security reasons! */ public $switchIdentitySessionKey = 'yuik_usuario'; + /** + * @var integer If != NULL sets a max password age in days + */ + public $maxPasswordAge = null; } diff --git a/src/User/Service/PasswordExpireService.php b/src/User/Service/PasswordExpireService.php new file mode 100644 index 0000000..6a7e75f --- /dev/null +++ b/src/User/Service/PasswordExpireService.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Da\User\Service; + +use Da\User\Contracts\ServiceInterface; +use Da\User\Model\User; + +class PasswordExpireService implements ServiceInterface +{ + protected $model; + + public function __construct(User $model) + { + $this->model = $model; + } + + public function run() + { + return $this->model->updateAttributes(['last_login_at' => time()]); + } +} diff --git a/src/User/resources/views/admin/index.php b/src/User/resources/views/admin/index.php index cf2bf3c..8827ecc 100644 --- a/src/User/resources/views/admin/index.php +++ b/src/User/resources/views/admin/index.php @@ -92,6 +92,7 @@ $module = Yii::$app->getModule('user'); 'format' => 'raw', 'visible' => Yii::$app->getModule('user')->enableEmailConfirmation, ], + 'password_age', [ 'header' => Yii::t('usuario', 'Block status'), 'value' => function ($model) { @@ -121,7 +122,7 @@ $module = Yii::$app->getModule('user'); ], [ 'class' => 'yii\grid\ActionColumn', - 'template' => '{switch} {reset} {update} {delete}', + 'template' => '{switch} {reset} {force-password-change} {update} {delete}', 'buttons' => [ 'switch' => function ($url, $model) use ($module) { if ($model->id != Yii::$app->user->id && $module->enableSwitchIdentities) { @@ -158,7 +159,24 @@ $module = Yii::$app->getModule('user'); } return null; - } + }, + 'force-password-change' => function ($url, $model) use ($module) { + if (is_null($module->maxPasswordAge)) { + return null; + } + return Html::a( + '', + ['/user/admin/force-password-change', 'id' => $model->id], + [ + 'title' => Yii::t('usuario', 'Force password change at next login'), + 'data-confirm' => Yii::t( + 'usuario', + 'Are you sure you wish the user to change their password at next login?' + ), + 'data-method' => 'POST', + ] + ); + }, ] ], ],