Added REST admin controller
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -52,3 +52,6 @@ codeception.yml
|
||||
# composer
|
||||
composer.lock
|
||||
|
||||
# visual studio code
|
||||
.vscode
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ There's a change in flash messages handling, please see #391
|
||||
- Enh #472: implement module viewPath in all views instead of static file reference (tonisormisson)
|
||||
- Fix: Clear 2FA auth key when feature is disabled by user
|
||||
- Fix: check user before accessing 2FA code
|
||||
- Enh: added `AdminController` REST controller (MatteoF96)
|
||||
|
||||
## 1.5.1 April 5, 2020
|
||||
|
||||
|
||||
@ -317,4 +317,37 @@ Possible array keys:
|
||||
- special: minimum number of special characters;
|
||||
- min: minimum number of characters (= minimum length).
|
||||
|
||||
#### authenticatorClass (type: `string`, default: `yii\filters\auth\QueryParamAuth`)
|
||||
|
||||
Which class to use as authenticator for REST API.
|
||||
Possible values ([official documentation](https://www.yiiframework.com/doc/guide/2.0/en/rest-authentication)):
|
||||
- `HttpBasicAuth`
|
||||
- `HttpBearerAuth`
|
||||
- `QueryParamAuth`.
|
||||
|
||||
Default value = `yii\filters\auth\QueryParamAuth` class, therefore access tokens are sent as query parameter; for instance: `https://example.com/users?access-token=xxxxxxxx`.
|
||||
|
||||
#### adminRestPrefix (type: `string`, default: `user/api/v1`)
|
||||
|
||||
Route prefix for REST admin controller.
|
||||
|
||||
#### adminRestRoutes (type `array`)
|
||||
|
||||
Routes for REST admin controller.
|
||||
|
||||
Default value:
|
||||
```php
|
||||
[
|
||||
'GET,HEAD users' => 'admin/index',
|
||||
'POST users' => 'admin/create',
|
||||
'PUT,PATCH users/<id>' => 'admin/update',
|
||||
'GET,HEAD users/<id>' => 'admin/view',
|
||||
'DELETE users/<id>' => 'admin/delete',
|
||||
'users/<action>/<id>' => 'admin/<action>',
|
||||
'users/<id>' => 'admin/options',
|
||||
'users' => 'admin/options',
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
© [2amigos](http://www.2amigos.us/) 2013-2019
|
||||
|
||||
@ -53,6 +53,7 @@ class Bootstrap implements BootstrapInterface
|
||||
if ($app instanceof WebApplication) {
|
||||
$this->initControllerNamespace($app);
|
||||
$this->initUrlRoutes($app);
|
||||
$this->initUrlRestRoutes($app);
|
||||
$this->initAuthCollection($app);
|
||||
$this->initAuthManager($app);
|
||||
} else {
|
||||
@ -277,6 +278,25 @@ class Bootstrap implements BootstrapInterface
|
||||
$app->getUrlManager()->addRules([$rule], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes web url for rest routes.
|
||||
* @param WebApplication $app
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
protected function initUrlRestRoutes(WebApplication $app)
|
||||
{
|
||||
/** @var Module $module */
|
||||
$module = $app->getModule('user');
|
||||
$rules = $module->adminRestRoutes;
|
||||
$config = [
|
||||
'class' => 'yii\web\GroupUrlRule',
|
||||
'prefix' => $module->adminRestPrefix,
|
||||
'rules' => $rules,
|
||||
];
|
||||
$rule = Yii::createObject($config);
|
||||
$app->getUrlManager()->addRules([$rule], false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures required mail parameters needed for the mail service.
|
||||
*
|
||||
|
||||
450
src/User/Controller/api/v1/AdminController.php
Normal file
450
src/User/Controller/api/v1/AdminController.php
Normal file
@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
namespace Da\User\Controller\api\v1;
|
||||
|
||||
use Da\User\Event\UserEvent;
|
||||
use Da\User\Factory\MailFactory;
|
||||
use Da\User\Model\Assignment;
|
||||
use Da\User\Model\Profile;
|
||||
use Da\User\Model\User;
|
||||
use Da\User\Query\UserQuery;
|
||||
use Da\User\Service\PasswordExpireService;
|
||||
use Da\User\Service\PasswordRecoveryService;
|
||||
use Da\User\Service\UserBlockService;
|
||||
use Da\User\Service\UserConfirmationService;
|
||||
use Da\User\Service\UserCreateService;
|
||||
use Da\User\Traits\ContainerAwareTrait;
|
||||
use Yii;
|
||||
use yii\base\Module;
|
||||
use yii\db\ActiveRecord;
|
||||
use yii\filters\Cors;
|
||||
use yii\rest\ActiveController;
|
||||
use yii\web\BadRequestHttpException;
|
||||
use yii\web\ForbiddenHttpException;
|
||||
use yii\web\NotFoundHttpException;
|
||||
use yii\web\ServerErrorHttpException;
|
||||
|
||||
/**
|
||||
* Controller that provides REST APIs to manage users.
|
||||
* This controller is equivalent to `Da\User\Controller\AdminController`.
|
||||
*
|
||||
* TODO:
|
||||
* - `Info` and `SwitchIdentity` actions were not developed yet.
|
||||
* - `Assignments` action implements only GET method (POST method not developed yet).
|
||||
*/
|
||||
class AdminController extends ActiveController
|
||||
{
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $modelClass = 'Da\User\Model\User';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $updateScenario = 'update';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $createScenario = 'create';
|
||||
|
||||
/**
|
||||
* @var UserQuery
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
// Set user properties for REST APIs
|
||||
\Yii::$app->user->enableSession = false;
|
||||
\Yii::$app->user->loginUrl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function actions()
|
||||
{
|
||||
// Get and then remove some default actions
|
||||
$actions = parent::actions();
|
||||
unset($actions['create']);
|
||||
unset($actions['update']);
|
||||
unset($actions['delete']);
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function verbs()
|
||||
{
|
||||
// Get parent verbs
|
||||
$verbs = parent::verbs();
|
||||
|
||||
// Add new verbs and return
|
||||
$verbs['update-profile'] = ['PUT', 'PATCH'];
|
||||
$verbs['assignments'] = ['GET'];
|
||||
$verbs['confirm'] = ['PUT', 'PATCH'];
|
||||
$verbs['block'] = ['PUT', 'PATCH'];
|
||||
$verbs['password-reset'] = ['PUT', 'PATCH'];
|
||||
$verbs['force-password-change'] = ['PUT', 'PATCH'];
|
||||
return $verbs;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function behaviors()
|
||||
{
|
||||
$behaviors = parent::behaviors();
|
||||
// Remove the (default) authentication filter
|
||||
unset($behaviors['authenticator']);
|
||||
|
||||
// Cors filter
|
||||
$behaviors['corsFilter'] = [
|
||||
'class' => Cors::class,
|
||||
];
|
||||
|
||||
// Re-add authentication filter
|
||||
$behaviors['authenticator'] = [
|
||||
'class' => $this->module->authenticatorClass, // Class depends on the module parameter
|
||||
'except' => ['options']
|
||||
];
|
||||
// Return
|
||||
return $behaviors;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkAccess($action, $model = null, $params = [])
|
||||
{
|
||||
// Access for admins only
|
||||
if (!Yii::$app->user->can('admin')) {
|
||||
throw new ForbiddenHttpException(Yii::t('usuario', 'User does not have sufficient permissions.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user.
|
||||
*/
|
||||
public function actionCreate()
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Create new user model
|
||||
/** @var User $user */
|
||||
$user = $this->make(User::class, [], ['scenario' => $this->createScenario]);
|
||||
|
||||
// Create event object
|
||||
/** @var UserEvent $event */
|
||||
$event = $this->make(UserEvent::class, [$user]);
|
||||
|
||||
// Save user model + response
|
||||
$user->load(Yii::$app->getRequest()->getBodyParams(), '');
|
||||
if ($user->validate()) {
|
||||
$this->trigger(UserEvent::EVENT_BEFORE_CREATE, $event);
|
||||
$mailService = MailFactory::makeWelcomeMailerService($user); // Welcome email
|
||||
if ($this->make(UserCreateService::class, [$user, $mailService])->run()) {
|
||||
$this->trigger(UserEvent::EVENT_AFTER_CREATE, $event);
|
||||
Yii::$app->getResponse()->setStatusCode(201); // 201 = Created
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
if (!$user->hasErrors()) {
|
||||
$this->throwServerError();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionUpdate($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
$user->setScenario($this->updateScenario);
|
||||
|
||||
// Create event object
|
||||
/** @var UserEvent $event */
|
||||
$event = $this->make(UserEvent::class, [$user]);
|
||||
|
||||
// Save user model + response
|
||||
$user->load(Yii::$app->getRequest()->getBodyParams(), '');
|
||||
if ($user->validate()) {
|
||||
$this->trigger(UserEvent::EVENT_BEFORE_ACCOUNT_UPDATE, $event);
|
||||
if ($user->save()) {
|
||||
$this->trigger(UserEvent::EVENT_AFTER_ACCOUNT_UPDATE, $event);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
if (!$user->hasErrors()) {
|
||||
$this->throwServerError();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionDelete($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Check ID parameter (whether own account)
|
||||
if ((int)$id === Yii::$app->user->getId()) {
|
||||
throw new BadRequestHttpException(Yii::t('usuario', 'You cannot remove your own account.'));
|
||||
}
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Create event object
|
||||
/** @var UserEvent $event */
|
||||
$event = $this->make(UserEvent::class, [$user]);
|
||||
|
||||
// Detele user model + response
|
||||
$this->trigger(ActiveRecord::EVENT_BEFORE_DELETE, $event);
|
||||
if ($user->delete()) {
|
||||
$this->trigger(ActiveRecord::EVENT_AFTER_DELETE, $event);
|
||||
Yii::$app->getResponse()->setStatusCode(204); // 204 = No Content
|
||||
}
|
||||
else {
|
||||
$this->throwServerError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user profile.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionUpdateProfile($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Get profile model
|
||||
/** @var Profile $profile */
|
||||
$profile = $user->profile;
|
||||
if ($profile === null) {
|
||||
$profile = $this->make(Profile::class);
|
||||
$profile->link('user', $user);
|
||||
}
|
||||
|
||||
// Create event object
|
||||
/** @var UserEvent $event */
|
||||
$event = $this->make(UserEvent::class, [$user]);
|
||||
|
||||
// Save profile model + response
|
||||
$profile->load(Yii::$app->getRequest()->getBodyParams(), '');
|
||||
$this->trigger(UserEvent::EVENT_BEFORE_PROFILE_UPDATE, $event);
|
||||
if ($profile->save() === false && !$profile->hasErrors()) {
|
||||
$this->throwServerError();
|
||||
}
|
||||
$this->trigger(UserEvent::EVENT_AFTER_PROFILE_UPDATE, $event);
|
||||
return $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignments of the specified user.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionAssignments($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Get assignments + response
|
||||
$assignments = $this->make(Assignment::class, [], ['user_id' => $user->id]);
|
||||
return $assignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the user.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionConfirm($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Create event object
|
||||
/** @var UserEvent $event */
|
||||
$event = $this->make(UserEvent::class, [$user]);
|
||||
|
||||
// Confirm user + response
|
||||
$this->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION, $event);
|
||||
if ($this->make(UserConfirmationService::class, [$user])->run() || $user->hasErrors()) {
|
||||
$this->trigger(UserEvent::EVENT_AFTER_CONFIRMATION, $event);
|
||||
return $user;
|
||||
}
|
||||
else {
|
||||
$this->throwServerError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block and unblock the user.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionBlock($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Check ID parameter (whether own account)
|
||||
if ((int)$id === Yii::$app->user->getId()) {
|
||||
throw new BadRequestHttpException(Yii::t('usuario', 'You cannot block your own account.'));
|
||||
}
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Create event object
|
||||
/** @var UserEvent $event */
|
||||
$event = $this->make(UserEvent::class, [$user]);
|
||||
|
||||
// Block user + response
|
||||
if ($this->make(UserBlockService::class, [$user, $event, $this])->run() || $user->hasErrors()) {
|
||||
return $user;
|
||||
}
|
||||
else {
|
||||
$this->throwServerError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionPasswordReset($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Confirm user + response
|
||||
$mailService = MailFactory::makeRecoveryMailerService($user->email);
|
||||
if ($this->make(PasswordRecoveryService::class, [$user->email, $mailService])->run()) {
|
||||
return $user;
|
||||
}
|
||||
else {
|
||||
$this->throwServerError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the user to change password at next login.
|
||||
* @param int $id ID of the user.
|
||||
*/
|
||||
public function actionForcePasswordChange($id)
|
||||
{
|
||||
// Check access
|
||||
$this->checkAccess($this->action);
|
||||
|
||||
// Get user model
|
||||
/** @var User $user */
|
||||
$user = $this->userQuery->where(['id' => $id])->one();
|
||||
if (empty($user)) { // Check user, so `$id` parameter
|
||||
$this->throwUser404();
|
||||
}
|
||||
|
||||
// Confirm user + response
|
||||
if ($this->make(PasswordExpireService::class, [$user])->run()) {
|
||||
return $user;
|
||||
}
|
||||
else {
|
||||
$this->throwServerError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle server error (with default Yii2 response).
|
||||
* @return void
|
||||
* @throws ServerErrorHttpException
|
||||
*/
|
||||
protected function throwServerError()
|
||||
{
|
||||
throw new ServerErrorHttpException('Failed to create the object for unknown reason.'); // Yii2 standard response for server errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 404 error for user (usually if the entered ID is not valid).
|
||||
* @return void
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
protected function throwUser404()
|
||||
{
|
||||
throw new NotFoundHttpException(Yii::t('usuario', 'User not found.'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -91,6 +91,16 @@ class User extends ActiveRecord implements IdentityInterface
|
||||
return '{{%user}}';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fields()
|
||||
{
|
||||
$fields = parent::fields();
|
||||
unset($fields['auth_key'], $fields['password_hash']);
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@ -233,7 +233,6 @@ class Module extends BaseModule
|
||||
* @var boolean whether to disable IP logging into user table
|
||||
*/
|
||||
public $disableIpLogging = false;
|
||||
|
||||
/**
|
||||
* @var array Minimum requirements when a new password is automatically generated.
|
||||
* Array structure: `requirement => minimum number characters`.
|
||||
@ -250,6 +249,29 @@ class Module extends BaseModule
|
||||
'digit' => 1,
|
||||
'upper' => 1,
|
||||
];
|
||||
/**
|
||||
* @var string Which class to use as authenticator for REST API.
|
||||
* Possible values: `HttpBasicAuth`, `HttpBearerAuth` or `QueryParamAuth`.
|
||||
* Default value = `yii\filters\auth\QueryParamAuth` class, therefore access tokens are sent as query parameter; for instance: `https://example.com/users?access-token=xxxxxxxx`.
|
||||
*/
|
||||
public $authenticatorClass = 'yii\filters\auth\QueryParamAuth';
|
||||
/**
|
||||
* @var string Route prefix for REST admin controller.
|
||||
*/
|
||||
public $adminRestPrefix = 'user/api/v1';
|
||||
/**
|
||||
* @var array Routes for REST admin controller.
|
||||
*/
|
||||
public $adminRestRoutes = [
|
||||
'GET,HEAD users' => 'admin/index',
|
||||
'POST users' => 'admin/create',
|
||||
'PUT,PATCH users/<id>' => 'admin/update',
|
||||
'GET,HEAD users/<id>' => 'admin/view',
|
||||
'DELETE users/<id>' => 'admin/delete',
|
||||
'users/<action>/<id>' => 'admin/<action>',
|
||||
'users/<id>' => 'admin/options',
|
||||
'users' => 'admin/options',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return string with the hit to be used with the give consent checkbox
|
||||
|
||||
@ -13,9 +13,11 @@ namespace Da\User\Service;
|
||||
|
||||
use Da\User\Contracts\ServiceInterface;
|
||||
use Da\User\Controller\AdminController;
|
||||
use Da\User\Controller\api\v1\AdminController as AdminControllerRest;
|
||||
use Da\User\Event\UserEvent;
|
||||
use Da\User\Helper\SecurityHelper;
|
||||
use Da\User\Model\User;
|
||||
use TypeError;
|
||||
|
||||
class UserBlockService implements ServiceInterface
|
||||
{
|
||||
@ -27,9 +29,13 @@ class UserBlockService implements ServiceInterface
|
||||
public function __construct(
|
||||
User $model,
|
||||
UserEvent $event,
|
||||
AdminController $controller,
|
||||
$controller,
|
||||
SecurityHelper $securityHelper
|
||||
) {
|
||||
if (!in_array(get_class($controller), [AdminController::class, AdminControllerRest::class])) {
|
||||
throw new TypeError('Argument controller must be either of type '
|
||||
. AdminController::class . ' or ' . AdminControllerRest::class . ', ' . get_class($controller) . ' given');
|
||||
}
|
||||
$this->model = $model;
|
||||
$this->event = $event;
|
||||
$this->controller = $controller;
|
||||
|
||||
Reference in New Issue
Block a user