Tighten organisation admin permissions & password reset workflow
This commit is contained in:
@@ -137,6 +137,33 @@ function bindMemberButtons(el, options) {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Reset member password button
|
||||
el.find('.js-member-password').click(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var userName = $(this).data('user_name');
|
||||
$("body").ufModal({
|
||||
sourceUrl: site.uri.public + '/modals/organisations/o/' + $(this).data('slug') + '/members/reset-password',
|
||||
ajaxParams: {
|
||||
slug: $(this).data('slug'),
|
||||
user_name: $(this).data('user_name')
|
||||
},
|
||||
msgTarget: $("#alerts-page")
|
||||
});
|
||||
|
||||
$("body").on('renderSuccess.ufModal', function() {
|
||||
var modal = $(this).ufModal('getModal');
|
||||
var form = modal.find('.js-form');
|
||||
|
||||
form.ufForm()
|
||||
.on("submitSuccess.ufForm", function() {
|
||||
// Navigate or reload page on success
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// function bindMemberCreationButton(el) {
|
||||
|
||||
@@ -155,6 +155,10 @@ return [
|
||||
'DEMOTE_CONFIRM_EXTRA' => 'Once demoted they will no longer be able to manage members and agents on the organisation\'s behalf.',
|
||||
'DEMOTE_YES' => 'Yes, demote administrator',
|
||||
'DEMOTE_SUCCESSFUL' => 'Successfully demoted administrator <strong>{{user_name}}</strong> from being an administrator of organisation <strong>{{name}}</strong>',
|
||||
|
||||
'RESET_PASSWORD' => 'Send password reset link',
|
||||
'SEND_PASSWORD_LINK_CONFIRM' => 'Please confirm that you wish to send a password reset link to:<br><br> <strong>{{user_name}}</strong> ({{email}}).',
|
||||
'PASSWORD_LINK_SENT' => 'A password reset link has successfully been sent to <strong>{{email}}</strong>.'
|
||||
],
|
||||
],
|
||||
|
||||
@@ -168,7 +172,7 @@ return [
|
||||
'DEMOTE' => 'Demote to member',
|
||||
|
||||
'EDIT' => 'Edit member',
|
||||
'CHANGE_PASSWORD' => 'Change member password',
|
||||
'RESET_PASSWORD' => 'Send password reset link',
|
||||
],
|
||||
|
||||
'ADMIN' => [
|
||||
|
||||
@@ -33,6 +33,8 @@ $app->group('/api/organisations/o/{slug}/members', function () {
|
||||
|
||||
$this->put('/m/{user_name}/promote', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:promote');
|
||||
$this->put('/m/{user_name}/demote', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:demote');
|
||||
|
||||
$this->post('/m/{user_name}/password-reset', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:createPasswordReset');
|
||||
})->add('authGuard')->add(new NoCache());
|
||||
|
||||
|
||||
@@ -45,4 +47,5 @@ $app->group('/modals/organisations/o/{slug}/members', function () {
|
||||
$this->get('/confirm-reject', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:getModalConfirmReject');
|
||||
$this->get('/confirm-promote', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:getModalConfirmPromote');
|
||||
$this->get('/confirm-demote', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:getModalConfirmDemote');
|
||||
$this->get('/reset-password', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:getModalResetPassword');
|
||||
})->add('authGuard')->add(new NoCache());
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
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;
|
||||
@@ -918,6 +919,81 @@ class OrganisationMembersController extends SimpleController
|
||||
return $response->withRedirect($this->ci->router->pathFor('uri_organisation', ['slug' => $organisation->slug]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the request to send a user a password reset email.
|
||||
*
|
||||
* Processes the request from the user update form, checking that:
|
||||
* 1. The target user's new email address, if specified, is not already in use;
|
||||
* 2. The logged-in user has the necessary permissions to update the posted field(s);
|
||||
* 3. We're not trying to disable the master account;
|
||||
* 4. The submitted data is valid.
|
||||
* This route requires authentication.
|
||||
*
|
||||
* Request type: POST
|
||||
*
|
||||
* @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 createPasswordReset(Request $request, Response $response, array $args)
|
||||
{
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Support\Repository\Repository $config */
|
||||
$config = $this->ci->config;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
|
||||
$ms = $this->ci->alerts;
|
||||
|
||||
|
||||
// Get the username from the URL
|
||||
$user = $this->getUserFromParams($args);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Access-controlled resource - check that currentUser has permission to edit "password" for this user
|
||||
if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
|
||||
'user' => $user,
|
||||
'fields' => ['password'],
|
||||
])) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
// Begin transaction - DB will be rolled back if an exception occurs
|
||||
Capsule::transaction(function () use ($user, $config) {
|
||||
// Create a password reset and shoot off an email
|
||||
$passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']);
|
||||
|
||||
// Create and send welcome email with password set link
|
||||
$message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig');
|
||||
|
||||
$message->from($config['address_book.admin'])
|
||||
->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
|
||||
->addParams([
|
||||
'user' => $user,
|
||||
'token' => $passwordReset->getToken(),
|
||||
'request_date' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
$this->ci->mailer->send($message);
|
||||
});
|
||||
|
||||
$ms->addMessageTranslated('success', 'ORGANISATION.MEMBER.PASSWORD_LINK_SENT', [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
return $response->withJson([], 200);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of organisation members.
|
||||
@@ -1359,6 +1435,60 @@ class OrganisationMembersController extends SimpleController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the modal form for sending a password reset to a member.
|
||||
*
|
||||
* 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 getModalResetPassword(Request $request, Response $response, array $args)
|
||||
{
|
||||
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
|
||||
$authorizer = $this->ci->authorizer;
|
||||
|
||||
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
|
||||
$currentUser = $this->ci->currentUser;
|
||||
|
||||
/** @var \UserFrosting\Support\Repository\Repository $config */
|
||||
$config = $this->ci->config;
|
||||
|
||||
// GET parameters
|
||||
$params = $request->getQueryParams();
|
||||
|
||||
$user = $this->getUserFromParams($params);
|
||||
|
||||
$organisation = $this->getOrganisationFromParams($params);
|
||||
|
||||
// Check organisation & user exists
|
||||
if (!$organisation || !$user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Access-controlled resource - check that currentUser has permission to edit "password" field for this user
|
||||
if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
|
||||
'organisation' => $organisation,
|
||||
'user' => $user,
|
||||
'fields' => ['password'],
|
||||
])) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return $this->ci->view->render($response, 'modals/member-reset-password.html.twig', [
|
||||
'user' => $user,
|
||||
'organisation' => $organisation,
|
||||
'page' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send approval email for specified organisation and confirmation to user.
|
||||
|
||||
@@ -241,7 +241,7 @@ class OrganisationPermissions extends BaseSeed
|
||||
'update_user_field_organisation' => new Permission([
|
||||
'slug' => 'update_user_field',
|
||||
'name' => 'Edit organisation user',
|
||||
'conditions' => "has_matching_organisation(self.id,user.id,true) && !is_master(user.id) && !has_role(user.id,{$roleIds['site-admin']}) && (!has_role(user.id,{$roleIds['organisations-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])",
|
||||
'conditions' => "has_matching_organisation(self.id,user.id,1) && !is_master(user.id) && !has_role(user.id,{$roleIds['site-admin']}) && (!has_role(user.id,{$roleIds['organisations-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])",
|
||||
'description' => 'Edit users in your own organisation who are not Site or (global) Organisation Administrators, except yourself.',
|
||||
]),
|
||||
];
|
||||
|
||||
32
templates/modals/member-reset-password.html.twig
Normal file
32
templates/modals/member-reset-password.html.twig
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "modals/modal.html.twig" %}
|
||||
|
||||
{% block modal_title %}{{translate("ORGANISATION.MEMBER.RESET_PASSWORD")}}{% endblock %}
|
||||
|
||||
{% block modal_body %}
|
||||
<form class="js-form" method="POST" action="{{site.uri.public}}/api/organisations/o/{{organisation.slug}}/members/m/{{user.user_name}}/password-reset">
|
||||
{% include "forms/csrf.html.twig" %}
|
||||
<div class="js-form-alerts">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{{translate("ORGANISATION.MEMBER.SEND_PASSWORD_LINK_CONFIRM", {
|
||||
'user_name': user.user_name,
|
||||
'email': user.email
|
||||
})}}
|
||||
</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('CONFIRM')}}</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>
|
||||
<!-- Include validation rules -->
|
||||
<script>
|
||||
{% include "pages/partials/page.js.twig" %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -36,8 +36,8 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" data-slug="{% endverbatim %}{{organisation.slug}}{% verbatim %}" data-user_name="{{row.user_name}}" class="js-user-password">
|
||||
<i class="fas fa-key"></i> {% endverbatim %}{{translate("MEMBER.CHANGE_PASSWORD")}}{% verbatim %}
|
||||
<a href="#" data-slug="{% endverbatim %}{{organisation.slug}}{% verbatim %}" data-user_name="{{row.user_name}}" class="js-member-password">
|
||||
<i class="fas fa-key"></i> {% endverbatim %}{{translate("MEMBER.RESET_PASSWORD")}}{% verbatim %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
Reference in New Issue
Block a user