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 %}
+