diff --git a/assets/organisations/js/widgets/members.js b/assets/organisations/js/widgets/members.js index 33a666d..f17be50 100644 --- a/assets/organisations/js/widgets/members.js +++ b/assets/organisations/js/widgets/members.js @@ -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) { diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index 2714459..ec404cd 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -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 {{user_name}} from being an administrator of organisation {{name}}', + + 'RESET_PASSWORD' => 'Send password reset link', + 'SEND_PASSWORD_LINK_CONFIRM' => 'Please confirm that you wish to send a password reset link to:

{{user_name}} ({{email}}).', + 'PASSWORD_LINK_SENT' => 'A password reset link has successfully been sent to {{email}}.' ], ], @@ -168,7 +172,7 @@ return [ 'DEMOTE' => 'Demote to member', 'EDIT' => 'Edit member', - 'CHANGE_PASSWORD' => 'Change member password', + 'RESET_PASSWORD' => 'Send password reset link', ], 'ADMIN' => [ diff --git a/routes/organisation-members.php b/routes/organisation-members.php index e22bcf6..79aeaf7 100644 --- a/routes/organisation-members.php +++ b/routes/organisation-members.php @@ -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()); \ No newline at end of file diff --git a/src/Controller/OrganisationMembersController.php b/src/Controller/OrganisationMembersController.php index 0efef7a..5aab58d 100644 --- a/src/Controller/OrganisationMembersController.php +++ b/src/Controller/OrganisationMembersController.php @@ -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. diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index f8905f7..e1d8366 100644 --- a/src/Database/Seeds/OrganisationPermissions.php +++ b/src/Database/Seeds/OrganisationPermissions.php @@ -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.', ]), ]; diff --git a/templates/modals/member-reset-password.html.twig b/templates/modals/member-reset-password.html.twig new file mode 100644 index 0000000..7b4c2ad --- /dev/null +++ b/templates/modals/member-reset-password.html.twig @@ -0,0 +1,32 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("ORGANISATION.MEMBER.RESET_PASSWORD")}}{% endblock %} + +{% block modal_body %} +
+ {% include "forms/csrf.html.twig" %} +
+
+
+
+ {{translate("ORGANISATION.MEMBER.SEND_PASSWORD_LINK_CONFIRM", { + 'user_name': user.user_name, + 'email': user.email + })}} +
+
+
+
+
+ +
+
+ +
+
+
+ + +{% endblock %} diff --git a/templates/tables/columns/organisation_members-actions.html.twig b/templates/tables/columns/organisation_members-actions.html.twig index d59866f..f5d8410 100644 --- a/templates/tables/columns/organisation_members-actions.html.twig +++ b/templates/tables/columns/organisation_members-actions.html.twig @@ -36,8 +36,8 @@
  • - - {% endverbatim %}{{translate("MEMBER.CHANGE_PASSWORD")}}{% verbatim %} + + {% endverbatim %}{{translate("MEMBER.RESET_PASSWORD")}}{% verbatim %}