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:
@@ -18,6 +18,7 @@
|
|||||||
"js/pages/organisation": {
|
"js/pages/organisation": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"userfrosting/js/widgets/users.js",
|
"userfrosting/js/widgets/users.js",
|
||||||
|
"avsdev/js/widgets/users.js",
|
||||||
"avsdev/js/widgets/organisations.js",
|
"avsdev/js/widgets/organisations.js",
|
||||||
"avsdev/js/pages/organisation.js"
|
"avsdev/js/pages/organisation.js"
|
||||||
]
|
]
|
||||||
@@ -27,6 +28,32 @@
|
|||||||
"avsdev/js/widgets/organisations.js",
|
"avsdev/js/widgets/organisations.js",
|
||||||
"avsdev/js/pages/organisations.js"
|
"avsdev/js/pages/organisations.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"js/pages/user": {
|
||||||
|
"scripts": [
|
||||||
|
"userfrosting/js/widgets/users.js",
|
||||||
|
"userfrosting/js/pages/user.js",
|
||||||
|
"avsdev/js/widgets/users.js",
|
||||||
|
"avsdev/js/pages/user.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"js/pages/users": {
|
||||||
|
"scripts": [
|
||||||
|
"userfrosting/js/widgets/users.js",
|
||||||
|
"userfrosting/js/pages/users.js",
|
||||||
|
"avsdev/js/widgets/users.js",
|
||||||
|
"avsdev/js/pages/users.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"css/admin": {
|
||||||
|
"styles": [
|
||||||
|
"font-starcraft/css/font-starcraft.css",
|
||||||
|
"vendor/tablesorter/dist/css/theme.bootstrap.min.css",
|
||||||
|
"vendor/tablesorter/dist/css/jquery.tablesorter.pager.min.css",
|
||||||
|
"userfrosting/css/tablesorter-reflow.css",
|
||||||
|
"userfrosting/css/tablesorter-custom.css",
|
||||||
|
"avsdev/css/organisations.css"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
assets/avsdev/css/organisations.css
Normal file
3
assets/avsdev/css/organisations.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.organisation-admin {
|
||||||
|
color: #ffc107 !important;
|
||||||
|
}
|
||||||
@@ -20,5 +20,6 @@ $(document).ready(function() {
|
|||||||
// Bind user table buttons
|
// Bind user table buttons
|
||||||
$("#widget-organisation-members").on("pagerComplete.ufTable", function () {
|
$("#widget-organisation-members").on("pagerComplete.ufTable", function () {
|
||||||
bindUserButtons($(this));
|
bindUserButtons($(this));
|
||||||
|
bindUserButtonsExtra($(this));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
13
assets/avsdev/js/pages/user.js
Normal file
13
assets/avsdev/js/pages/user.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
|
||||||
|
* example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
|
||||||
|
*
|
||||||
|
* This script depends on uf-table.js, moment.js, handlebars-helpers.js
|
||||||
|
*
|
||||||
|
* Target page: /users/u/{user_name}
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Control buttons
|
||||||
|
bindUserButtonsExtra($("#view-user"), { delete_redirect: page.delete_redirect });
|
||||||
|
});
|
||||||
15
assets/avsdev/js/pages/users.js
Normal file
15
assets/avsdev/js/pages/users.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template.
|
||||||
|
* example: {{ assets.js('js/pages/sign-in-or-register') | raw }}
|
||||||
|
*
|
||||||
|
* This script depends on widgets/users.js, uf-table.js, moment.js, handlebars-helpers.js
|
||||||
|
*
|
||||||
|
* Target page: /users
|
||||||
|
*/
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Bind table buttons
|
||||||
|
$("#widget-users").on("pagerComplete.ufTable", function () {
|
||||||
|
bindUserButtonsExtra($(this));
|
||||||
|
});
|
||||||
|
});
|
||||||
56
assets/avsdev/js/widgets/users.js
Normal file
56
assets/avsdev/js/widgets/users.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link extra user action buttons in addition to the base ones.
|
||||||
|
* @param {module:jQuery} el jQuery wrapped element to target.
|
||||||
|
* @param {{delete_redirect: string}} options Options used to modify behaviour of button actions.
|
||||||
|
*/
|
||||||
|
function bindUserButtonsExtra(el, options) {
|
||||||
|
// Manage user organisations button
|
||||||
|
el.find('.js-user-organisations').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var userName = $(this).data('user_name');
|
||||||
|
$("body").ufModal({
|
||||||
|
sourceUrl: site.uri.public + "/modals/users/organisations",
|
||||||
|
ajaxParams: {
|
||||||
|
user_name: userName
|
||||||
|
},
|
||||||
|
msgTarget: $("#alerts-page")
|
||||||
|
});
|
||||||
|
|
||||||
|
$("body").on('renderSuccess.ufModal', function(data) {
|
||||||
|
var modal = $(this).ufModal('getModal');
|
||||||
|
var form = modal.find('.js-form');
|
||||||
|
|
||||||
|
// Set up collection widget
|
||||||
|
var organisationWidget = modal.find('.js-form-organisations');
|
||||||
|
organisationWidget.ufCollection({
|
||||||
|
dropdown: {
|
||||||
|
ajax: {
|
||||||
|
url: site.uri.public + '/api/organisations'
|
||||||
|
},
|
||||||
|
placeholder: "Select an organisation"
|
||||||
|
},
|
||||||
|
dropdownTemplate: modal.find('#user-organisations-select-option').html(),
|
||||||
|
rowTemplate: modal.find('#user-organisations-row').html()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current organisations and add to widget
|
||||||
|
$.getJSON(site.uri.public + '/api/users/u/' + userName + '/organisations')
|
||||||
|
.done(function(data) {
|
||||||
|
$.each(data.rows, function(idx, organisation) {
|
||||||
|
organisation.text = organisation.name;
|
||||||
|
organisationWidget.ufCollection('addRow', organisation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up form for submission
|
||||||
|
form.ufForm()
|
||||||
|
.on("submitSuccess.ufForm", function() {
|
||||||
|
// Reload page on success
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@ return [
|
|||||||
'ADMIN_COUNT' => '# Admins',
|
'ADMIN_COUNT' => '# Admins',
|
||||||
|
|
||||||
'SELF' => 'My Organisations',
|
'SELF' => 'My Organisations',
|
||||||
|
'MANAGE' => 'Manage Organisations',
|
||||||
|
'ASSIGN_NEW' => 'Assign to organisation',
|
||||||
|
|
||||||
'NAME' => [
|
'NAME' => [
|
||||||
1 => 'Organisation name',
|
1 => 'Organisation name',
|
||||||
|
|||||||
23
routes/users.php
Normal file
23
routes/users.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?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)
|
||||||
|
*/
|
||||||
|
|
||||||
|
use UserFrosting\Sprinkle\Core\Util\NoCache;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extra routes for administrative user management.
|
||||||
|
*/
|
||||||
|
$app->group('/api/users', function () {
|
||||||
|
$this->get('/u/{user_name}/organisations', 'UserFrosting\Sprinkle\Organisations\Controller\UserController:getOrganisations');
|
||||||
|
|
||||||
|
$this->put('/u/{user_name}/{field}', 'UserFrosting\Sprinkle\Organisations\Controller\UserController:updateField');
|
||||||
|
})->add('authGuard')->add(new NoCache());
|
||||||
|
|
||||||
|
$app->group('/modals/users', function () {
|
||||||
|
$this->get('/organisations', 'UserFrosting\Sprinkle\Organisations\Controller\UserController:getModalEditOrganisations');
|
||||||
|
})->add('authGuard')->add(new NoCache());
|
||||||
64
schema/requests/user/edit-field.yaml
Normal file
64
schema/requests/user/edit-field.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
first_name:
|
||||||
|
validators:
|
||||||
|
length:
|
||||||
|
label: "&FIRST_NAME"
|
||||||
|
min: 1
|
||||||
|
max: 20
|
||||||
|
message: VALIDATE.LENGTH_RANGE
|
||||||
|
last_name:
|
||||||
|
validators:
|
||||||
|
length:
|
||||||
|
label: "&LAST_NAME"
|
||||||
|
min: 1
|
||||||
|
max: 30
|
||||||
|
message: VALIDATE.LENGTH_RANGE
|
||||||
|
email:
|
||||||
|
validators:
|
||||||
|
length:
|
||||||
|
label: "&EMAIL"
|
||||||
|
min: 1
|
||||||
|
max: 150
|
||||||
|
message: VALIDATE.LENGTH_RANGE
|
||||||
|
email:
|
||||||
|
message: VALIDATE.INVALID_EMAIL
|
||||||
|
locale:
|
||||||
|
validators:
|
||||||
|
length:
|
||||||
|
label: "&LOCALE"
|
||||||
|
min: 1
|
||||||
|
max: 10
|
||||||
|
message: VALIDATE.LENGTH_RANGE
|
||||||
|
group_id:
|
||||||
|
validators:
|
||||||
|
integer:
|
||||||
|
message: VALIDATE.INTEGER
|
||||||
|
flag_enabled:
|
||||||
|
validators:
|
||||||
|
member_of:
|
||||||
|
values:
|
||||||
|
- '0'
|
||||||
|
- '1'
|
||||||
|
message: VALIDATE.BOOLEAN
|
||||||
|
flag_verified:
|
||||||
|
validators:
|
||||||
|
member_of:
|
||||||
|
values:
|
||||||
|
- '0'
|
||||||
|
- '1'
|
||||||
|
message: VALIDATE.BOOLEAN
|
||||||
|
password:
|
||||||
|
validators:
|
||||||
|
length:
|
||||||
|
label: "&PASSWORD"
|
||||||
|
min: 12
|
||||||
|
max: 100
|
||||||
|
message: VALIDATE.LENGTH_RANGE
|
||||||
|
roles:
|
||||||
|
validators:
|
||||||
|
array:
|
||||||
|
message: VALIDATE.ARRAY
|
||||||
|
organisations:
|
||||||
|
validators:
|
||||||
|
array:
|
||||||
|
message: VALIDATE.ARRAY
|
||||||
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');
|
$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 */
|
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
|
||||||
$classMapper = static::$ci->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');;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
80
templates/modals/user-manage-organisations.html.twig
Normal file
80
templates/modals/user-manage-organisations.html.twig
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{% extends "modals/modal.html.twig" %}
|
||||||
|
|
||||||
|
{% block modal_title %}{{translate("ORGANISATION.MANAGE")}}{% endblock %}
|
||||||
|
|
||||||
|
{% block modal_body %}
|
||||||
|
<form class="js-form" method="PUT" action="{{site.uri.public}}/api/users/u/{{user.user_name}}/organisations">
|
||||||
|
{% include "forms/csrf.html.twig" %}
|
||||||
|
<div class="js-form-alerts">
|
||||||
|
</div>
|
||||||
|
<div class="js-form-organisations">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{translate("NAME")}}</th>
|
||||||
|
<th>{{translate("DESCRIPTION")}}</th>
|
||||||
|
<th>{{translate("REMOVE")}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="padding-bottom">
|
||||||
|
<label>{{translate("ORGANISATION.ASSIGN_NEW")}}:</label>
|
||||||
|
<select class="form-control js-select-new" type="text">
|
||||||
|
<option></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-8 col-sm-4">
|
||||||
|
<button type="submit" class="btn btn-block btn-lg btn-success">{{translate("UPDATE")}}</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4 col-sm-3 pull-right">
|
||||||
|
<button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate('CANCEL')}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{# This contains a series of <script> blocks, each of which is a client-side Handlebars template.
|
||||||
|
# Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag,
|
||||||
|
# so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates.
|
||||||
|
#
|
||||||
|
# These templates require handlebars-helpers.js, moment.js
|
||||||
|
#}
|
||||||
|
{% verbatim %}
|
||||||
|
<script id="user-organisations-select-option" type="text/x-handlebars-template">
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{{name}}
|
||||||
|
</strong>
|
||||||
|
<br>
|
||||||
|
{{description}}
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="user-organisations-row" type="text/x-handlebars-template">
|
||||||
|
<tr class="uf-collection-row">
|
||||||
|
<td>
|
||||||
|
{{name}}
|
||||||
|
<input type="hidden" name="organisations[{{ rownum }}][organisation_id]" value="{{id}}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{description}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="organisations[{{ rownum }}][flag_admin]" {{#if flag_admin}}checked{{/if}}>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-link btn-trash js-delete-row pull-right" title="Delete"> <i class="fas fa-trash"></i> </button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
{% endverbatim %}
|
||||||
|
|
||||||
|
<!-- Include validation rules -->
|
||||||
|
<script>
|
||||||
|
{% include "pages/partials/page.js.twig" %}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
168
templates/tables/users.html.twig
Normal file
168
templates/tables/users.html.twig
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
{# This partial template renders a table of users, to be populated with rows via an AJAX request.
|
||||||
|
# This extends a generic template for paginated tables.
|
||||||
|
#
|
||||||
|
# Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used
|
||||||
|
# to render the table cells with the data from the AJAX request.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% extends "tables/table-paginated.html.twig" %}
|
||||||
|
|
||||||
|
{% block table %}
|
||||||
|
<table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="{{table.sortlist}}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sorter-metatext" data-column-name="name" data-column-template="#user-table-column-info" data-priority="1">{{translate('USER')}} <i class="fas fa-sort"></i></th>
|
||||||
|
<th class="filter-metatext" data-column-name="organisations" data-column-template="#user-table-column-organisations" data-priority="2">{{translate("ORGANISATION", 2)}} <i class="fas fa-sort"></i></th>
|
||||||
|
{% if 'last_activity' in table.columns %}
|
||||||
|
<th class="sorter-metanum" data-column-name="last_activity" data-column-template="#user-table-column-last-activity" data-priority="3">{{translate("ACTIVITY.LAST")}} <i class="fas fa-sort"></i></th>
|
||||||
|
{% endif %}
|
||||||
|
{% if 'via_roles' in table.columns %}
|
||||||
|
<th data-column-template="#user-table-column-via-roles" data-sorter="false" data-filter="false" data-priority="1">{{translate('PERMISSION.VIA_ROLES')}}</th>
|
||||||
|
{% endif %}
|
||||||
|
<th class="filter-select filter-metatext" data-column-name="status" data-column-template="#user-table-column-status" data-priority="2">{{translate("STATUS")}} <i class="fas fa-sort"></i></th>
|
||||||
|
<th data-column-name="actions" data-column-template="#user-table-column-actions" data-sorter="false" data-filter="false" data-priority="1">{{translate("ACTIONS")}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block table_cell_templates %}
|
||||||
|
{# This contains a series of <script> blocks, each of which is a client-side Handlebars template.
|
||||||
|
# Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag,
|
||||||
|
# so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates.
|
||||||
|
#
|
||||||
|
# These templates require handlebars-helpers.js, moment.js
|
||||||
|
#}
|
||||||
|
{% verbatim %}
|
||||||
|
<script id="user-table-column-info" type="text/x-handlebars-template">
|
||||||
|
<td data-text="{{row.last_name}}">
|
||||||
|
<strong>
|
||||||
|
<a href="{{site.uri.public}}/users/u/{{row.user_name}}">{{row.first_name}} {{row.last_name}} ({{row.user_name}})</a>
|
||||||
|
</strong>
|
||||||
|
<div class="js-copy-container">
|
||||||
|
<span class="js-copy-target">{{row.email}}</span>
|
||||||
|
<button class="btn btn-xs uf-copy-trigger js-copy-trigger"><i class="fas fa-copy"></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="user-table-column-organisations" type="text/x-handlebars-template">
|
||||||
|
{{#if row.organisations.length }}
|
||||||
|
<td style="line-height: 2em;">
|
||||||
|
{{#each row.organisations }}
|
||||||
|
<a href="{% endverbatim %}{{site.uri.public}}{% verbatim %}/organisations/o/{{this.slug}}" class="label label-primary {{#if this.pivot.flag_admin }}organisation-admin{{/if}}" title="{{this.description}}" data-text="{{this.name}}" style="font-size: 100%;">{{this.name}}</a><br>
|
||||||
|
{{/each}}
|
||||||
|
</td>
|
||||||
|
{{ else }}
|
||||||
|
<td></td>
|
||||||
|
{{/if }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="user-table-column-last-activity" type="text/x-handlebars-template">
|
||||||
|
{{#if row.last_activity }}
|
||||||
|
<td data-num="{{dateFormat row.last_activity.occurred_at format='x'}}">
|
||||||
|
{{dateFormat row.last_activity.occurred_at format="dddd"}}<br>{{dateFormat row.last_activity.occurred_at format="MMM Do, YYYY h:mm a"}}
|
||||||
|
<br>
|
||||||
|
<i>{{row.last_activity.description}}</i>
|
||||||
|
</td>
|
||||||
|
{{ else }}
|
||||||
|
<td data-num="0">
|
||||||
|
<i>{% endverbatim %}{{translate("UNKNOWN")}}{% verbatim %}</i>
|
||||||
|
</td>
|
||||||
|
{{/if }}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="user-table-column-status" type="text/x-handlebars-template">
|
||||||
|
<td
|
||||||
|
{{#ifx row.flag_enabled '==' 0 }}
|
||||||
|
data-text="disabled"
|
||||||
|
{{ else }}
|
||||||
|
{{#ifx row.flag_verified '==' 0 }}
|
||||||
|
data-text="unactivated"
|
||||||
|
{{ else }}
|
||||||
|
data-text="active"
|
||||||
|
{{/ifx }}
|
||||||
|
{{/ifx }}
|
||||||
|
>
|
||||||
|
{{#ifx row.flag_enabled '==' 0 }}
|
||||||
|
<span class="text-muted">
|
||||||
|
{% endverbatim %}{{translate("DISABLED")}}{% verbatim %}
|
||||||
|
</span>
|
||||||
|
{{ else }}
|
||||||
|
{{#ifx row.flag_verified '==' 0 }}
|
||||||
|
<span class="text-yellow">
|
||||||
|
{% endverbatim %}{{translate("UNACTIVATED")}}{% verbatim %}
|
||||||
|
</span>
|
||||||
|
{{ else }}
|
||||||
|
<span>
|
||||||
|
{% endverbatim %}{{translate("ACTIVE")}}{% verbatim %}
|
||||||
|
</span>
|
||||||
|
{{/ifx }}
|
||||||
|
{{/ifx }}
|
||||||
|
</td>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="user-table-column-actions" type="text/x-handlebars-template">
|
||||||
|
<td class="uf-table-fit-width">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">{% endverbatim %}{{translate("ACTIONS")}}{% verbatim %}<span class="caret"></span></button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-right-responsive" role="menu">
|
||||||
|
{{#ifx row.flag_verified '==' 0 }}
|
||||||
|
<li>
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-activate">
|
||||||
|
<i class="fas fa-bolt"></i> {% endverbatim %}{{translate("USER.ACTIVATE")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{/ifx }}
|
||||||
|
<li>
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-edit">
|
||||||
|
<i class="fas fa-edit"></i> {% endverbatim %}{{translate("USER.EDIT")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-roles">
|
||||||
|
<i class="fas fa-id-card"></i> {% endverbatim %}{{translate("ROLE.MANAGE")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-organisations">
|
||||||
|
<i class="fas fa-sitemap"></i> {% endverbatim %}{{translate("ORGANISATION.MANAGE")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-password">
|
||||||
|
<i class="fas fa-key"></i> {% endverbatim %}{{translate("USER.ADMIN.CHANGE_PASSWORD")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{#ifx row.flag_enabled '==' 1 }}
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-disable">
|
||||||
|
<i class="fas fa-minus-circle"></i> {% endverbatim %}{{translate("USER.DISABLE")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
{{ else }}
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-enable">
|
||||||
|
<i class="fas fa-plus-circle"></i> {% endverbatim %}{{translate("USER.ENABLE")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
{{/ifx }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" data-user_name="{{row.user_name}}" class="js-user-delete">
|
||||||
|
<i class="fas fa-trash-alt"></i> {% endverbatim %}{{translate("USER.DELETE")}}{% verbatim %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="user-table-column-via-roles" type="text/x-handlebars-template">
|
||||||
|
<td>
|
||||||
|
{{#each row.roles_via }}
|
||||||
|
<a href="{% endverbatim %}{# Handlebars can't access variables in the global scope, so we have to use Twig to insert the base url #}{{site.uri.public}}{% verbatim %}/roles/r/{{this.slug}}" class="label label-primary" title="{{this.description}}">{{this.name}}</a>
|
||||||
|
{{/each}}
|
||||||
|
</td>
|
||||||
|
</script>
|
||||||
|
{% endverbatim %}
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user