diff --git a/lib/User/Contracts/ServiceInterface.php b/lib/User/Contracts/ServiceInterface.php new file mode 100644 index 0000000..a5bf76b --- /dev/null +++ b/lib/User/Contracts/ServiceInterface.php @@ -0,0 +1,18 @@ + + */ +interface ServiceInterface +{ + /** + * @return void + */ + public function run(); +} diff --git a/lib/User/Helper/AuthHelper.php b/lib/User/Helper/AuthHelper.php index 0506329..50ca031 100644 --- a/lib/User/Helper/AuthHelper.php +++ b/lib/User/Helper/AuthHelper.php @@ -1,6 +1,7 @@ authManager) { - $roles = array_keys(Yii::$app->authManager->getRolesByUser($userId)); + if (Yii::$app->getAuthManager()) { + $roles = array_keys(Yii::$app->getAuthManager()->getRolesByUser($userId)); return in_array($role, $roles, true); } return false; } + + public function isAdmin($username) + { + /** @var Module $module */ + $module = Yii::$app->getModule('user'); + $hasAdministratorPermissionName = Yii::$app->getAuthManager() && $module->administratorPermissionName + ? Yii::$app->getUser()->can($module->administratorPermissionName) + : false; + + return $hasAdministratorPermissionName || in_array($username, $module->administrators); + } + } diff --git a/lib/User/Helper/SecurityHelper.php b/lib/User/Helper/SecurityHelper.php new file mode 100644 index 0000000..13e3d60 --- /dev/null +++ b/lib/User/Helper/SecurityHelper.php @@ -0,0 +1,60 @@ +security = $security; + } + + /** + * Generates a secure hash from a password and a random salt. + * + * @param string $password + * @param null|int $cost + * + * @return string + */ + public function generatePasswordHash($password, $cost = null) + { + return $this->security->generatePasswordHash($password, $cost); + } + + public function validatePassword($password, $hash) + { + return $this->security->validatePassword($password, $hash); + } + + public function generatePassword($length) + { + $sets = [ + 'abcdefghjkmnpqrstuvwxyz', + 'ABCDEFGHJKMNPQRSTUVWXYZ', + '23456789', + ]; + $all = ''; + $password = ''; + foreach ($sets as $set) { + $password .= $set[array_rand(str_split($set))]; + $all .= $set; + } + + $all = str_split($all); + for ($i = 0; $i < $length - count($sets); $i++) { + $password .= $all[array_rand($all)]; + } + + $password = str_shuffle($password); + + return $password; + } +} diff --git a/lib/User/Model/User.php b/lib/User/Model/User.php index 7bd889d..e49360b 100644 --- a/lib/User/Model/User.php +++ b/lib/User/Model/User.php @@ -1,7 +1,13 @@ Yii::t('user', 'Username'), + 'email' => Yii::t('user', 'Email'), + 'registration_ip' => Yii::t('user', 'Registration ip'), + 'unconfirmed_email' => Yii::t('user', 'New email'), + 'password' => Yii::t('user', 'Password'), + 'created_at' => Yii::t('user', 'Registration time'), + 'confirmed_at' => Yii::t('user', 'Confirmation time'), + ]; + } + + /** + * @inheritdoc + */ + public function scenarios() + { + return ArrayHelper::merge( + parent::scenarios(), + [ + 'register' => ['username', 'email', 'password'], + 'connect' => ['username', 'email'], + 'create' => ['username', 'email', 'password'], + 'update' => ['username', 'email', 'password'], + 'settings' => ['username', 'email', 'password'], + ] + ); + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + // username rules + 'usernameRequired' => ['username', 'required', 'on' => ['register', 'create', 'connect', 'update']], + 'usernameMatch' => ['username', 'match', 'pattern' => $this->usernameRegex], + 'usernameLength' => ['username', 'string', 'min' => 3, 'max' => 255], + 'usernameTrim' => ['username', 'trim'], + 'usernameUnique' => [ + 'username', + 'unique', + 'message' => Yii::t('user', 'This username has already been taken') + ], + + // email rules + 'emailRequired' => ['email', 'required', 'on' => ['register', 'connect', 'create', 'update']], + 'emailPattern' => ['email', 'email'], + 'emailLength' => ['email', 'string', 'max' => 255], + 'emailUnique' => [ + 'email', + 'unique', + 'message' => Yii::t('user', 'This email address has already been taken') + ], + 'emailTrim' => ['email', 'trim'], + + // password rules + 'passwordRequired' => ['password', 'required', 'on' => ['register']], + 'passwordLength' => ['password', 'string', 'min' => 6, 'max' => 72, 'on' => ['register', 'create']], + ]; + } + + /** + * @inheritdoc + */ + public function validateAuthKey($authKey) + { + return $this->getAttribute('auth_key') === $authKey; + } + + /** + * @inheritdoc + */ + public function getId() + { + return $this->getAttribute('id'); + } + + /** + * @inheritdoc + */ + public function getAuthKey() + { + return $this->getAttribute('auth_key'); + } + + /** + * @inheritdoc + */ + public static function findIdentity($id) + { + return static::findOne($id); + } + + /** + * @inheritdoc + */ + public static function findIdentityByAccessToken($token, $type = null) + { + throw new NotSupportedException('Method "' . __CLASS__ . '::' . __METHOD__ . '" is not implemented.'); + } + /** * @return bool whether is blocked or not. */ @@ -41,13 +173,50 @@ class User extends ActiveRecord implements IdentityInterface return $this->blocked_at !== null; } + /** + * @return bool whether the user is an admin or not + */ public function getIsAdmin() { - + return $this->getAuthHelper()->isAdmin($this->username); } + /** + * Checks whether a user has a specific role + * + * @param string $role + * + * @return bool + */ public function hasRole($role) { + return $this->getAuthHelper()->hasRole($this->id, $role); + } + /** + * @return \yii\db\ActiveQuery + */ + public function getProfile() + { + return $this->hasOne($this->getClassMapHelper()->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(); + + foreach($accounts as $account) { + $this->connectedAccounts[$account->provider] = $account; + } + } + return $this->connectedAccounts; } } diff --git a/lib/User/Module.php b/lib/User/Module.php index d78c5ef..fe5cb0f 100644 --- a/lib/User/Module.php +++ b/lib/User/Module.php @@ -10,6 +10,10 @@ class Module extends \yii\base\Module * @var bool whether to allow registration process or not. */ public $allowRegistration = 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. */ @@ -18,6 +22,10 @@ class Module extends \yii\base\Module * @var bool whether to allow login accounts with unconfirmed emails. */ public $allowUnconfirmedEmailLogin = false; + /** + * @var bool whether to enable password recovery or not. + */ + public $allowPasswordRecovery = true; /** * @var string the class name of the strategy class to handle user's email change. */ diff --git a/lib/User/Service/MailService.php b/lib/User/Service/MailService.php new file mode 100644 index 0000000..f561f84 --- /dev/null +++ b/lib/User/Service/MailService.php @@ -0,0 +1,12 @@ + + */ +class UserCreateService implements ServiceInterface +{ + protected $model; + protected $securityHelper; + protected $logger; + + public function __construct(User $model, SecurityHelper $securityHelper, Logger $logger) + { + $this->model = $model; + $this->securityHelper = $securityHelper; + $this->logger = $logger; + } + + /** + * @return bool + */ + public function run() + { + $model = $this->model; + + if ($model->getIsNewRecord() === false) { + throw new InvalidCallException('Cannot create a new user from an existing one.'); + } + + $transaction = $model->getDb()->beginTransaction(); + + try { + $model->confirmed_at = time(); + $model->password = $model->password !== null + ? $model->password + : $this->securityHelper->generatePassword(8); + + // TODO: Trigger BEFORE CREATE EVENT + + if (!$model->save()) { + $transaction->rollBack(); + + return false; + } + + // TODO: Send welcome message + + $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 new file mode 100644 index 0000000..5d3e156 --- /dev/null +++ b/lib/User/Traits/ContainerTrait.php @@ -0,0 +1,38 @@ +get('Da\User\Helper\AuthHelper'); + } + + /** + * @return \Da\User\Helper\ClassMapHelper + */ + public function getClassMapHelper() + { + return Yii::$container->get('Da\User\Helper\ClassMapHelper'); + } + +} diff --git a/lib/User/Traits/ModuleTrait.php b/lib/User/Traits/ModuleTrait.php new file mode 100644 index 0000000..05d3385 --- /dev/null +++ b/lib/User/Traits/ModuleTrait.php @@ -0,0 +1,21 @@ +getModule('user'); + } +}