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:
2022-02-08 18:10:20 +00:00
parent 609454def2
commit 9d62749914
14 changed files with 763 additions and 1 deletions

View 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);
}
}

View File

@@ -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);
});
}
}

View File

@@ -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');;
}
/**