diff --git a/lib/User/Bootstrap.php b/lib/User/Bootstrap.php index de207d3..71f3dc8 100644 --- a/lib/User/Bootstrap.php +++ b/lib/User/Bootstrap.php @@ -3,6 +3,7 @@ namespace Da\User; use Da\User\Helper\ClassMapHelper; +use Da\User\Model\Profile; use Yii; use yii\authclient\Collection; use yii\base\Application; @@ -49,7 +50,7 @@ class Bootstrap implements BootstrapInterface $di->set(Strategy\InsecureEmailChangeStrategy::class); $di->set(Strategy\SecureEmailChangeStrategy::class); - // models + active query classes + // models + classMap $modelClassMap = []; foreach ($map as $class => $definition) { @@ -57,17 +58,37 @@ class Bootstrap implements BootstrapInterface $model = is_array($definition) ? $definition['class'] : $definition; $name = (substr($class, strrpos($class, '\\') + 1)); $modelClassMap[$name] = $model; - - if (in_array($name, ['User', 'Profile', 'Token', 'Account'])) { - $di->set( - $name . 'Query', - function () use ($model) { - return $model->find(); - } - ); - } } + // query classes + $di->set( + Query\ProfileQuery::class, + function () { + return Model\Profile::find(); + } + ); + $di->set( + Query\SocialNetworkAccountQuery::class, + function () { + return Model\SocialNetworkAccount::find(); + } + ); + $di->set( + Query\TokenQuery::class, + function () { + return Model\Token::find(); + } + ); + $di->set( + Query\UserQuery::class, + function () { + return Model\User::find(); + } + ); + + // search class + $di->set(Search\UserSearch::class, [$di->get('UserQuery')]); + // helpers $di->set(Helper\AuthHelper::class); $di->setSingleton(ClassMapHelper::class, ClassMapHelper::class, [$modelClassMap]); @@ -86,6 +107,8 @@ class Bootstrap implements BootstrapInterface // services $di->set(Service\UserCreateService::class); + $di->set(Service\UserRegisterService::class); + $di->set(Service\UserConfirmationService::class); // events $di->set(Event\FormEvent::class); @@ -147,6 +170,7 @@ class Bootstrap implements BootstrapInterface protected function initMailServiceConfiguration(Application $app, Module $module) { $defaults = [ + 'fromEmail' => 'no-reply@example.com', 'welcomeMailSubject' => Yii::t('user', 'Welcome to {0}', $app->name), 'confirmationMailSubject' => Yii::t('user', 'Confirm account on {0}', $app->name), 'reconfirmationMailSubject' => Yii::t('user', 'Confirm email change on {0}', $app->name), diff --git a/lib/User/Controller/AdminController.php b/lib/User/Controller/AdminController.php index cd6f515..87c67cc 100644 --- a/lib/User/Controller/AdminController.php +++ b/lib/User/Controller/AdminController.php @@ -2,21 +2,62 @@ namespace Da\User\Controller; use Da\User\Event\UserEvent; +use Da\User\Factory\MailFactory; use Da\User\Filter\AccessRuleFilter; +use Da\User\Model\Profile; use Da\User\Model\User; +use Da\User\Query\UserQuery; +use Da\User\Search\UserSearch; +use Da\User\Service\UserBlockService; +use Da\User\Service\UserConfirmationService; use Da\User\Service\UserCreateService; use Da\User\Traits\ContainerTrait; +use Da\User\Traits\ModuleTrait; use Da\User\Validator\AjaxRequestModelValidator; +use Yii; +use yii\base\Module; +use yii\db\ActiveRecord; use yii\filters\AccessControl; use yii\filters\VerbFilter; +use yii\helpers\Url; use yii\web\Controller; -use Yii; class AdminController extends Controller { + use ModuleTrait; use ContainerTrait; + protected $userQuery; + + /** + * AdminController constructor. + * + * @param string $id + * @param Module $module + * @param UserQuery $userQuery + * @param array $config + */ + public function __construct($id, Module $module, UserQuery $userQuery, array $config) + { + $this->userQuery = $userQuery; + parent::__construct($id, $module, $config); + } + + /** + * @param \yii\base\Action $action + * + * @return bool + */ + public function beforeAction($action) + { + if (in_array($action->id, ['index', 'update', 'update-profile', 'info', 'assingments'])) { + Url::remember('', 'actions-redirect'); + } + + return parent::beforeAction($action); + } + /** * @inheritdoc */ @@ -46,6 +87,20 @@ class AdminController extends Controller ]; } + public function actionIndex() + { + $searchModel = $this->make(UserSearch::class); + $dataProvider = $searchModel->search(Yii::$app->request->get()); + + return $this->render( + 'index', + [ + 'dataProvider' => $dataProvider, + 'searchModel' => $searchModel + ] + ); + } + public function actionCreate() { /** @var User $user */ @@ -58,18 +113,156 @@ class AdminController extends Controller $this->trigger(UserEvent::EVENT_BEFORE_CREATE, $event); - if($user->load(Yii::$app->request->post())) { - /** @var UserCreateService $userCreateService */ - $userCreateService = $this->make(UserCreateService::class, [$user]); - $userCreateService->run(); + if ($user->load(Yii::$app->request->post())) { + + $mailService = MailFactory::makeWelcomeMailerService($user); + + $this->make(UserCreateService::class, [$user, $mailService])->run(); $this->trigger(UserEvent::EVENT_AFTER_CREATE, $event); return $this->redirect(['update', 'id' => $user->id]); } - return $this->render('create', [ - 'user' => $user, - ]); + return $this->render('create', ['user' => $user]); + } + + public function actionUpdate($id) + { + $user = $this->userQuery->where(['id' => $id])->one(); + $user->setScenario('update'); + /** @var UserEvent $event */ + $event = $this->make(UserEvent::class, [$user]); + + $this->make(AjaxRequestModelValidator::class, [$user])->validate(); + + $this->trigger(ActiveRecord::EVENT_BEFORE_UPDATE, $event); + + if ($user->load(Yii::$app->request->post()) && $user->save()) { + Yii::$app->getSession()->setFlash('success', Yii::t('user', 'Account details have been updated')); + $this->trigger(ActiveRecord::EVENT_AFTER_UPDATE, $event); + + return $this->refresh(); + } + + return $this->render('_account', ['user' => $user]); + } + + public function actionUpdateProfile($id) + { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + $profile = $user->profile; + if ($profile === null) { + $profile = $this->make(Profile::class); + $profile->link($user); + } + /** @var UserEvent $event */ + $event = $this->make(UserEvent::class, [$user]); + $this->make(AjaxRequestModelValidator::class, [$user])->validate(); + $this->trigger(UserEvent::EVENT_BEFORE_PROFILE_UPDATE, $event); + + if ($profile->load(Yii::$app->request->post()) && $profile->save()) { + Yii::$app->getSession()->setFlash('success', Yii::t('user', 'Profile details have been updated')); + $this->trigger(UserEvent::EVENT_AFTER_PROFILE_UPDATE, $event); + + return $this->refresh(); + } + + return $this->render( + '_profile', + [ + 'user' => $user, + 'profile' => $profile + ] + ); + } + + public function actionInfo($id) + { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + + return $this->render( + '_info', + [ + 'user' => $user, + ] + ); + } + + public function actionAssignments($id) + { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + + return $this->render( + '_assignments', + [ + 'user' => $user, + ] + ); + } + + public function actionConfirm($id) + { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + /** @var UserEvent $event */ + $event = $this->make(UserEvent::class, [$user]); + $this->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION, $event); + if ($this->make(UserConfirmationService::class, [$user])->run()) { + Yii::$app->getSession()->setFlash('success', Yii::t('user', 'User has been confirmed')); + $this->trigger(UserEvent::EVENT_AFTER_CONFIRMATION, $event); + } else { + Yii::$app->getSession()->setFlash('warning', Yii::t('user', 'Unable to confirm user. Please, try again.')); + } + + return $this->redirect(Url::previous('actions-redirect')); + } + + public function actionDelete($id) + { + if ($id === Yii::$app->user->getId()) { + Yii::$app->getSession()->setFlash('danger', Yii::t('user', 'You cannot remove your own account')); + } else { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + /** @var UserEvent $event */ + $event = $this->make(UserEvent::class, [$user]); + $this->trigger(ActiveRecord::EVENT_BEFORE_DELETE, $event); + if ($user->delete()) { + Yii::$app->getSession()->setFlash('success', \Yii::t('user', 'User has been deleted')); + $this->trigger(ActiveRecord::EVENT_AFTER_DELETE, $event); + } else { + Yii::$app->getSession()->setFlash( + 'warning', + Yii::t('user', 'Unable to delete user. Please, try again later.') + ); + } + } + + return $this->redirect(['index']); + } + + public function actionBlock($id) + { + if ($id === Yii::$app->user->getId()) { + Yii::$app->getSession()->setFlash('danger', Yii::t('user', 'You cannot remove your own account')); + } else { + /** @var User $user */ + $user = $this->userQuery->where(['id' => $id])->one(); + /** @var UserEvent $event */ + $event = $this->make(UserEvent::class, [$user]); + + if ($this->make(UserBlockService::class, [$user, $event])->run()) { + Yii::$app->getSession()->setFlash('success', Yii::t('user', 'User block status has been updated.')); + } else { + Yii::$app->getSession()->setFlash('danger', Yii::t('user', 'Unable to update block status.')); + } + } + + return $this->redirect(Url::previous('actions-redirect')); } } + diff --git a/lib/User/Event/UserEvent.php b/lib/User/Event/UserEvent.php index 6b98f52..636508c 100644 --- a/lib/User/Event/UserEvent.php +++ b/lib/User/Event/UserEvent.php @@ -8,6 +8,12 @@ class UserEvent extends Event { const EVENT_BEFORE_CREATE = 'beforeCreate'; const EVENT_AFTER_CREATE = 'afterCreate'; + const EVENT_BEFORE_REGISTER = 'beforeRegister'; + const EVENT_AFTER_REGISTER = 'afterRegister'; + const EVENT_BEFORE_PROFILE_UPDATE = 'beforeProfileUpdate'; + const EVENT_AFTER_PROFILE_UPDATE = 'afterProfileUpdate'; + const EVENT_BEFORE_CONFIRMATION = 'beforeConfirmation'; + const EVENT_AFTER_CONFIRMATION = 'afterConfirmation'; protected $user; diff --git a/lib/User/Factory/MailFactory.php b/lib/User/Factory/MailFactory.php new file mode 100644 index 0000000..4323438 --- /dev/null +++ b/lib/User/Factory/MailFactory.php @@ -0,0 +1,44 @@ +getModule('user'); + $to = $user->email; + $from = $module->mailParams['fromEmail']; + $subject = $module->mailParams['welcomeMailSubject']; + $params = [ + 'user' => $user, + 'token' => null, + 'module' => $module, + 'showPassword' => false + ]; + + return static::makeMailerService($from, $to, $subject, 'welcome', $params); + } + + /** + * Builds a MailerService + * + * @param string $from + * @param string $to + * @param string $subject + * @param string $view + * @param array $params + * + * @return MailService + */ + public static function makeMailerService($from, $to, $subject, $view, array $params = []) + { + return Yii::$container->get(MailService::class, [$from, $to, $subject, $view, $params]); + } +} diff --git a/lib/User/Helper/AuthHelper.php b/lib/User/Helper/AuthHelper.php index 50ca031..5266c53 100644 --- a/lib/User/Helper/AuthHelper.php +++ b/lib/User/Helper/AuthHelper.php @@ -32,6 +32,11 @@ class AuthHelper return false; } + /** + * @param $username + * + * @return bool + */ public function isAdmin($username) { /** @var Module $module */ diff --git a/lib/User/Model/Profile.php b/lib/User/Model/Profile.php index 8d3df40..e336024 100644 --- a/lib/User/Model/Profile.php +++ b/lib/User/Model/Profile.php @@ -1,15 +1,17 @@ - */ -class Profile -{ +use Da\User\Query\ProfileQuery; +use yii\db\ActiveRecord; + +class Profile extends ActiveRecord +{ + /** + * @return ProfileQuery + */ + public static function find() + { + return new ProfileQuery(static::class); + } } diff --git a/lib/User/Model/SocialNetworkAccount.php b/lib/User/Model/SocialNetworkAccount.php index 099f2ab..5a5050e 100644 --- a/lib/User/Model/SocialNetworkAccount.php +++ b/lib/User/Model/SocialNetworkAccount.php @@ -1,15 +1,16 @@ - */ -class SocialNetworkAccount -{ +use Da\User\Query\SocialNetworkAccountQuery; +use yii\db\ActiveRecord; +class SocialNetworkAccount extends ActiveRecord +{ + /** + * @return SocialNetworkAccountQuery + */ + public static function find() + { + return new SocialNetworkAccountQuery(static::class); + } } diff --git a/lib/User/Model/Token.php b/lib/User/Model/Token.php index 4d3d3de..4be910d 100644 --- a/lib/User/Model/Token.php +++ b/lib/User/Model/Token.php @@ -1,15 +1,22 @@ - */ -class Token -{ +use Da\User\Query\TokenQuery; +use yii\db\ActiveRecord; + +class Token extends ActiveRecord +{ + const TYPE_CONFIRMATION = 0; + const TYPE_RECOVERY = 1; + const TYPE_CONFIRM_NEW_EMAIL = 2; + const TYPE_CONFIRM_OLD_EMAIL = 3; + + /** + * @return TokenQuery + */ + public static function find() + { + return new TokenQuery(static::class); + } } diff --git a/lib/User/Model/User.php b/lib/User/Model/User.php index e49360b..c349f74 100644 --- a/lib/User/Model/User.php +++ b/lib/User/Model/User.php @@ -1,15 +1,15 @@ getAuthHelper()->isAdmin($this->username); + return $this->getAuth()->isAdmin($this->username); } /** @@ -190,7 +194,7 @@ class User extends ActiveRecord implements IdentityInterface */ public function hasRole($role) { - return $this->getAuthHelper()->hasRole($this->id, $role); + return $this->getAuth()->hasRole($this->id, $role); } /** @@ -198,20 +202,17 @@ class User extends ActiveRecord implements IdentityInterface */ public function getProfile() { - return $this->hasOne($this->getClassMapHelper()->get('Profile'), ['user_id' => 'id']); + return $this->hasOne($this->getClassMap()->get('Profile'), ['user_id' => 'id']); } - protected $connectedAccounts; - /** * @return SocialNetworkAccount[] social connected accounts [ 'providerName' => socialAccountModel ] */ public function getSocialNetworkAccounts() { if ($this->connectedAccounts == null) { - $accounts = $this->connectedAccounts = $this - ->hasMany($this->getClassMapHelper()->get('Account'), ['user_id' => 'id']) - ->all(); + /** @var SocialNetworkAccount[] $accounts */ + $accounts = $this->hasMany($this->getClassMap()->get('Account'), ['user_id' => 'id'])->all(); foreach($accounts as $account) { $this->connectedAccounts[$account->provider] = $account; @@ -219,4 +220,12 @@ class User extends ActiveRecord implements IdentityInterface } return $this->connectedAccounts; } + + /** + * @return UserQuery + */ + public static function find() + { + return new UserQuery(static::class); + } } diff --git a/lib/User/Module.php b/lib/User/Module.php index a89109e..48895f8 100644 --- a/lib/User/Module.php +++ b/lib/User/Module.php @@ -9,15 +9,15 @@ class Module extends \yii\base\Module /** * @var bool whether to allow registration process or not. */ - public $allowRegistration = true; + public $enableRegistration = true; + /** + * @var bool whether to force email confirmation to. + */ + public $enableEmailConfirmation = true; /** * @var bool whether to generate passwords automatically and remove the password field from the registration form. */ public $generatePasswords = false; - /** - * @var bool whether to force email confirmation to. - */ - public $forceEmailConfirmation = true; /** * @var bool whether to allow login accounts with unconfirmed emails. */ @@ -50,12 +50,6 @@ class Module extends \yii\base\Module * @var string the administrator permission name */ public $administratorPermissionName; - /** - * @var array the class map used by the module. - * - * @see Bootstrap - */ - public $classmap = []; /** * @var string the route prefix */ diff --git a/lib/User/Query/AccountQuery.php b/lib/User/Query/AccountQuery.php deleted file mode 100644 index 130c105..0000000 --- a/lib/User/Query/AccountQuery.php +++ /dev/null @@ -1,15 +0,0 @@ - - */ -class AccountQuery -{ - -} diff --git a/lib/User/Query/ProfileQuery.php b/lib/User/Query/ProfileQuery.php index 4755055..13a28a9 100644 --- a/lib/User/Query/ProfileQuery.php +++ b/lib/User/Query/ProfileQuery.php @@ -1,15 +1,9 @@ - */ -class ProfileQuery +use yii\db\ActiveQuery; + +class ProfileQuery extends ActiveQuery { } diff --git a/lib/User/Query/SocialNetworkAccountQuery.php b/lib/User/Query/SocialNetworkAccountQuery.php new file mode 100644 index 0000000..1df7978 --- /dev/null +++ b/lib/User/Query/SocialNetworkAccountQuery.php @@ -0,0 +1,9 @@ + - */ -class TokenQuery +use yii\db\ActiveQuery; + +class TokenQuery extends ActiveQuery { } diff --git a/lib/User/Query/UserQuery.php b/lib/User/Query/UserQuery.php index e21015b..53661b2 100644 --- a/lib/User/Query/UserQuery.php +++ b/lib/User/Query/UserQuery.php @@ -1,15 +1,10 @@ - */ -class UserQuery +use yii\db\ActiveQuery; + +class UserQuery extends ActiveQuery { } diff --git a/lib/User/Search/UserSearch.php b/lib/User/Search/UserSearch.php new file mode 100644 index 0000000..ed812d8 --- /dev/null +++ b/lib/User/Search/UserSearch.php @@ -0,0 +1,98 @@ +query = $query; + parent::__construct($config); + } + + /** + * @return array + */ + public function rules() + { + return [ + 'safeFields' => [['username', 'email', 'registration_ip', 'created_at'], 'safe'], + 'createdDefault' => ['created_at', 'default', 'value' => null], + ]; + } + + /** + * @return array + */ + public function attributeLabels() + { + return [ + 'username' => Yii::t('user', 'Username'), + 'email' => Yii::t('user', 'Email'), + 'created_at' => Yii::t('user', 'Registration time'), + 'registration_ip' => Yii::t('user', 'Registration ip'), + ]; + } + + /** + * @param $params + * + * @return ActiveDataProvider + */ + public function search($params) + { + $query = $this->query; + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + + if (!($this->load($params) && $this->validate())) { + return $dataProvider; + } + + if ($this->created_at !== null) { + $date = strtotime($this->created_at); + $query->andFilterWhere(['between', 'created_at', $date, $date + 3600 * 24]); + } + + $query + ->andFilterWhere(['like', 'username', $this->username]) + ->andFilterWhere(['like', 'email', $this->email]) + ->andFilterWhere(['registration_ip' => $this->registration_ip]); + + return $dataProvider; + } +} diff --git a/lib/User/Service/UserBlockService.php b/lib/User/Service/UserBlockService.php new file mode 100644 index 0000000..774acc2 --- /dev/null +++ b/lib/User/Service/UserBlockService.php @@ -0,0 +1,23 @@ +model = $model; + $this->event = $event; + } + + public function run() + { + + } +} diff --git a/lib/User/Service/UserConfirmationService.php b/lib/User/Service/UserConfirmationService.php new file mode 100644 index 0000000..c6d9b5c --- /dev/null +++ b/lib/User/Service/UserConfirmationService.php @@ -0,0 +1,25 @@ +model = $model; + } + + public function run() + { + $this->model->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION); + $result = (bool) $this->model->updateAttributes(['confirmed_at' => time()]); + $this->model->trigger(UserEvent::EVENT_AFTER_CONFIRMATION); + + return $result; + } +} diff --git a/lib/User/Service/UserRegisterService.php b/lib/User/Service/UserRegisterService.php new file mode 100644 index 0000000..a8a6a56 --- /dev/null +++ b/lib/User/Service/UserRegisterService.php @@ -0,0 +1,73 @@ +model = $model; + $this->mailService = $mailService; + $this->securityHelper = $securityHelper; + $this->logger = $logger; + } + + public function run() + { + $model = $this->model; + + if ($model->getIsNewRecord() === false) { + throw new InvalidCallException('Cannot register user from an existing one.'); + } + + $transaction = $model->getDb()->beginTransaction(); + + try { + $model->confirmed_at = $this->model->module->enableEmailConfirmation ? null : time(); + $model->password = $this->model->module->generatePasswords + ? $this->securityHelper->generatePassword(8) + : $model->password; + + $model->trigger(UserEvent::EVENT_BEFORE_REGISTER); + + if(!$model->save()) { + $transaction->rollBack(); + return false; + } + + if($model->module->enableEmailConfirmation) { + $token = $model->make(Token::class, ['type' => Token::TYPE_CONFIRMATION]); + $token->link('user', $model); + } + + $this->mailService->run(); + + $model->trigger(UserEvent::EVENT_AFTER_REGISTER); + + $transaction->commit(); + + return true; + + } catch(Exception $e) { + $transaction->rollBack(); + $this->logger->log($e->getMessage(), Logger::LEVEL_WARNING); + + return false; + } + } + +} diff --git a/lib/User/Traits/ContainerTrait.php b/lib/User/Traits/ContainerTrait.php index a342f30..35fe32e 100644 --- a/lib/User/Traits/ContainerTrait.php +++ b/lib/User/Traits/ContainerTrait.php @@ -8,8 +8,11 @@ use Yii; use yii\di\Container; /** + * * @property-read Container $di - * @property-ready Da\User\Helper\AuthHelper $authHelper + * @property-ready Da\User\Helper\AuthHelper $auth + * @property-ready Da\User\Helper\ClassMapHelper $classMap + * */ trait ContainerTrait { @@ -38,17 +41,17 @@ trait ContainerTrait /** * @return \Da\User\Helper\AuthHelper */ - public function getAuthHelper() + public function getAuth() { - return Yii::$container->get(AuthHelper::class); + return $this->getDi()->get(AuthHelper::class); } /** * @return \Da\User\Helper\ClassMapHelper */ - public function getClassMapHelper() + public function getClassMap() { - return Yii::$container->get(ClassMapHelper::class); + return $this->getDi()->get(ClassMapHelper::class); } }