Provide ability for admins to manage user organisation membership & admin rights
- Required overwriting a bunch of base code to slot in the handling - Admins are shown with yellow text in their organisation label
This commit is contained in:
294
src/Controller/UserController.php
Normal file
294
src/Controller/UserController.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* AVSDev UF Organisations (https://avsdev.uk)
|
||||
*
|
||||
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
|
||||
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
|
||||
*/
|
||||
|
||||
namespace UserFrosting\Sprinkle\Organisations\Controller;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
|
||||
use UserFrosting\Fortress\RequestDataTransformer;
|
||||
use UserFrosting\Fortress\RequestSchema;
|
||||
use UserFrosting\Fortress\ServerSideValidator;
|
||||
use UserFrosting\Sprinkle\Account\Database\Models\User;
|
||||
use UserFrosting\Sprinkle\Account\Facades\Password;
|
||||
use UserFrosting\Sprinkle\Core\Controller\SimpleController;
|
||||
use UserFrosting\Sprinkle\Core\Mail\EmailRecipient;
|
||||
use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage;
|
||||
use UserFrosting\Support\Exception\BadRequestException;
|
||||
use UserFrosting\Support\Exception\ForbiddenException;
|
||||
use UserFrosting\Support\Exception\NotFoundException;
|
||||
use UserFrosting\Sprinkle\Admin\Controller\UserController as UFUserController;
|
||||
|
||||
/**
|
||||
* Controller class for extending user-related requests, including joining/leaving organisations, etc.
|
||||
*
|
||||
* @author Craig Williams (https://avsdev.uk)
|
||||
*/
|
||||
class UserController extends UFUserController
|
||||
{
|
||||
/**
|
||||
* Renders the modal form for editing a user's organisations.
|
||||
*
|
||||
* This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
|
||||
* This page requires authentication.
|
||||
*
|
||||
* Request type: GET
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string[] $args
|
||||
*
|
||||
* @throws NotFoundException If user is not found
|
||||
* @throws ForbiddenException If user is not authorized to access page
|
||||
*/
|
||||
public function getModalEditOrganisations(Request $request, Response $response, array $args)
|
||||
{
|
||||
// GET parameters
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
$user = $this->getUserFromParams($params);
|
||||
|
||||
// If the user doesn't exist, return 404
|
||||
if (!$user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
// Access-controlled resource - check that currentUser has permission to edit "organisations" field for this user
|
||||
if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
|
||||
'user' => $user,
|
||||
'fields' => ['organisations'],
|
||||
])) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return $this->ci->view->render($response, 'modals/user-manage-organisations.html.twig', [
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns organisations associated with a single user.
|
||||
*
|
||||
* This page requires authentication.
|
||||
* Request type: GET
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param string[] $args
|
||||
*
|
||||
* @throws NotFoundException If user is not found
|
||||
* @throws ForbiddenException If user is not authorized to access page
|
||||
*/
|
||||
public function getOrganisations(Request $request, Response $response, array $args)
|
||||
{
|
||||
$user = $this->getUserFromParams($args);
|
||||
|
||||
// If the user doesn't exist, return 404
|
||||
if (!$user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
|
||||
$classMapper = $this->ci->classMapper;
|
||||
|
||||
// GET parameters
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
// Access-controlled page
|
||||
if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
|
||||
'user' => $user,
|
||||
'property' => 'organisations',
|
||||
])) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
$sprunje = $classMapper->createInstance('organisation_sprunje', $classMapper, $params);
|
||||
$sprunje->extendQuery(function ($query) use ($user) {
|
||||
return $query->forUser($user->id);
|
||||
});
|
||||
|
||||
// Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content.
|
||||
// For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating).
|
||||
return $sprunje->toResponse($response);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateField(Request $request, Response $response, array $args)
|
||||
{
|
||||
// Get the username from the URL
|
||||
$user = $this->getUserFromParams($args);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Get key->value pair from URL and request body
|
||||
$fieldName = $args['field'];
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
// Access-controlled resource - check that currentUser has permission to edit the specified field for this user
|
||||
if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
|
||||
'user' => $user,
|
||||
'fields' => [$fieldName],
|
||||
])) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
/** @var \UserFrosting\Support\Repository\Repository $config */
|
||||
$config = $this->ci->config;
|
||||
|
||||
// Only the master account can edit the master account!
|
||||
if (
|
||||
($user->id == $config['reserved_user_ids.master']) &&
|
||||
($currentUser->id != $config['reserved_user_ids.master'])
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
// Get PUT parameters: value
|
||||
$put = $request->getParsedBody();
|
||||
|
||||
// Make sure data is part of $_PUT data, default to empty value otherwise
|
||||
if (isset($put[$fieldName])) {
|
||||
$fieldData = $put[$fieldName];
|
||||
} else {
|
||||
if ($fieldName == 'roles' || $fieldName == 'organisations') {
|
||||
$fieldData = [];
|
||||
} else {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
// Create and validate key -> value pair
|
||||
$params = [
|
||||
$fieldName => $fieldData,
|
||||
];
|
||||
|
||||
// Load the request schema
|
||||
$schema = new RequestSchema('schema://requests/user/edit-field.yaml');
|
||||
$schema->set('password.validators.length.min', $config['site.password.length.min']);
|
||||
$schema->set('password.validators.length.max', $config['site.password.length.max']);
|
||||
|
||||
// Whitelist and set parameter defaults
|
||||
$transformer = new RequestDataTransformer($schema);
|
||||
$data = $transformer->transform($params);
|
||||
|
||||
// Validate, and throw exception on validation errors.
|
||||
$validator = new ServerSideValidator($schema, $this->ci->translator);
|
||||
if (!$validator->validate($data)) {
|
||||
// TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException
|
||||
$e = new BadRequestException();
|
||||
foreach ($validator->errors() as $idx => $field) {
|
||||
foreach ($field as $eidx => $error) {
|
||||
$e->addUserMessage($error);
|
||||
}
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Get validated and transformed value
|
||||
$fieldValue = $data[$fieldName];
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
|
||||
$ms = $this->ci->alerts;
|
||||
|
||||
// Special checks and transformations for certain fields
|
||||
if ($fieldName == 'flag_enabled') {
|
||||
// Check that we are not disabling the master account
|
||||
if (
|
||||
($user->id == $config['reserved_user_ids.master']) &&
|
||||
($fieldValue == '0')
|
||||
) {
|
||||
$e = new BadRequestException();
|
||||
$e->addUserMessage('DISABLE_MASTER');
|
||||
|
||||
throw $e;
|
||||
} elseif (
|
||||
($user->id == $currentUser->id) &&
|
||||
($fieldValue == '0')
|
||||
) {
|
||||
$e = new BadRequestException();
|
||||
$e->addUserMessage('DISABLE_SELF');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} elseif ($fieldName == 'password') {
|
||||
$fieldValue = Password::hash($fieldValue);
|
||||
}
|
||||
|
||||
// Begin transaction - DB will be rolled back if an exception occurs
|
||||
Capsule::transaction(function () use ($fieldName, $fieldValue, $user, $currentUser) {
|
||||
if ($fieldName == 'roles') {
|
||||
$newRoles = collect($fieldValue)->pluck('role_id')->all();
|
||||
$user->roles()->sync($newRoles);
|
||||
} else if ($fieldName == 'organisations') {
|
||||
$newOrganisations = [];
|
||||
foreach ($fieldValue as $field) {
|
||||
$newOrganisations[$field['organisation_id']] = ['flag_admin' => $field['flag_admin'] == 1];
|
||||
}
|
||||
$user->organisations()->sync($newOrganisations);
|
||||
} else {
|
||||
$user->$fieldName = $fieldValue;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// Create activity record
|
||||
$this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for user {$user->user_name}.", [
|
||||
'type' => 'account_update_field',
|
||||
'user_id' => $currentUser->id,
|
||||
]);
|
||||
});
|
||||
|
||||
// Add success messages
|
||||
if ($fieldName == 'flag_enabled') {
|
||||
if ($fieldValue == '1') {
|
||||
$ms->addMessageTranslated('success', 'ENABLE_SUCCESSFUL', [
|
||||
'user_name' => $user->user_name,
|
||||
]);
|
||||
} else {
|
||||
$ms->addMessageTranslated('success', 'DISABLE_SUCCESSFUL', [
|
||||
'user_name' => $user->user_name,
|
||||
]);
|
||||
}
|
||||
} elseif ($fieldName == 'flag_verified') {
|
||||
$ms->addMessageTranslated('success', 'MANUALLY_ACTIVATED', [
|
||||
'user_name' => $user->user_name,
|
||||
]);
|
||||
} else {
|
||||
$ms->addMessageTranslated('success', 'DETAILS_UPDATED', [
|
||||
'user_name' => $user->user_name,
|
||||
]);
|
||||
}
|
||||
|
||||
return $response->withJson([], 200);
|
||||
}
|
||||
}
|
||||
@@ -254,4 +254,20 @@ class Organisation extends Model implements OrganisationInterface
|
||||
$join->on('admin_counts.organisation_id', '=', 'organisations.id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope to get all organisations a specific user is a member of.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param int $userId
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function scopeForUser($query, $userId)
|
||||
{
|
||||
return $query->join('organisation_members', function ($join) use ($userId) {
|
||||
$join->on('organisation_members.organisation_id', 'organisations.id')
|
||||
->where('user_id', $userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class User extends UFUser
|
||||
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
|
||||
$classMapper = static::$ci->classMapper;
|
||||
|
||||
return $this->belongsToMany($classMapper->getClassMapping('organisation'), 'organisation_members', 'user_id', 'organisation_id');
|
||||
return $this->belongsToMany($classMapper->getClassMapping('organisation'), 'organisation_members', 'user_id', 'organisation_id')->withPivot('flag_admin');;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user