From 711968df08b53cfe5f782caabbd5a1535d306e1a Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Wed, 9 Feb 2022 12:34:30 +0000 Subject: [PATCH] Allow members to leave organisations --- assets/avsdev/js/pages/organisation.js | 2 +- assets/avsdev/js/widgets/organisations.js | 25 ++++ locale/en_US/messages.php | 13 +- routes/organisation-members.php | 7 ++ src/Controller/OrganisationController.php | 1 + .../OrganisationMembersController.php | 113 ++++++++++++++++++ .../Seeds/OrganisationPermissions.php | 7 ++ .../confirm-leave-organisation.html.twig | 17 +++ templates/pages/organisation.html.twig | 9 +- 9 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 templates/modals/confirm-leave-organisation.html.twig diff --git a/assets/avsdev/js/pages/organisation.js b/assets/avsdev/js/pages/organisation.js index 02b3693..f365d09 100644 --- a/assets/avsdev/js/pages/organisation.js +++ b/assets/avsdev/js/pages/organisation.js @@ -9,7 +9,7 @@ $(document).ready(function() { // Control buttons - bindOrganisationButtons($("#view-organisation"), { delete_redirect: page.delete_redirect }); + bindOrganisationButtons($("#view-organisation"), { delete_redirect: page.delete_redirect, leave_redirect: page.leave_redirect }); // Table of users in this organisation $("#widget-organisation-members").ufTable({ diff --git a/assets/avsdev/js/widgets/organisations.js b/assets/avsdev/js/widgets/organisations.js index 77847d0..c189251 100644 --- a/assets/avsdev/js/widgets/organisations.js +++ b/assets/avsdev/js/widgets/organisations.js @@ -133,6 +133,31 @@ function bindOrganisationButtons(el, options) { }); }); + // Leave organisation button + el.find('.js-organisation-leave').click(function(e) { + e.preventDefault(); + + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/organisations/o/" + page.organisation_slug + "/members/confirm-leave", + ajaxParams: { + slug: $(this).data('slug') + }, + 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 + if (options.leave_redirect) window.location.href = options.leave_redirect; + else window.location.reload(); + }); + }); + }); + // Delete organisation button el.find('.js-organisation-delete').click(function(e) { e.preventDefault(); diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php index b9b1300..a2198ac 100644 --- a/locale/en_US/messages.php +++ b/locale/en_US/messages.php @@ -27,6 +27,11 @@ return [ 'EDIT' => 'Edit organistion', 'UPDATE' => 'Details updated for organistion {{name}}', + 'LEAVE' => 'Leave organisation', + 'LEAVE_CONFIRM' => 'Are you sure you want to leave the organisation {{name}}?', + 'LEAVE_YES' => 'Yes, leave organisation.', + 'LEAVE_SUCCESSFUL' => 'Successfully left organisation {{name}}', + 'MERGE' => 'Merge organisation', 'MERGE_INFORM' => 'Select an organisation from the list below to merge organisation {{name}} into. All users of organisation {{name}} will be moved into the chosen organisation.', 'MERGE_SUCCESSFUL' => 'Successfully merged organisation {{source}} into organisation {{target}}.', @@ -68,7 +73,9 @@ return [ 'MERGE_INTO' => 'Merge into', 'MERGE_CANNOT_UNDONE' => 'This action cannot be undone.', - 'SOURCE_SLUG' => 'Source Slug', - 'TARGET_SLUG' => 'Target Slug', - 'SLUG_NOT_IN_USE' => 'A {{slug}} slug does not exist', + 'SOURCE_SLUG' => 'Source Slug', + 'TARGET_SLUG' => 'Target Slug', + 'SLUG_NOT_IN_USE' => 'A {{slug}} slug does not exist', + + 'LEAVE_CANNOT_UNDONE' => 'This action cannot be undone.', ]; diff --git a/routes/organisation-members.php b/routes/organisation-members.php index b8b4a24..6d742e9 100644 --- a/routes/organisation-members.php +++ b/routes/organisation-members.php @@ -14,4 +14,11 @@ use UserFrosting\Sprinkle\Core\Util\NoCache; */ $app->group('/api/organisations/o/{slug}/members', function () { $this->get('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:getList'); + + $this->delete('', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:leave'); +})->add('authGuard')->add(new NoCache()); + + +$app->group('/modals/organisations/o/{slug}/members', function () { + $this->get('/confirm-leave', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationMembersController:getModalConfirmLeave'); })->add('authGuard')->add(new NoCache()); \ No newline at end of file diff --git a/src/Controller/OrganisationController.php b/src/Controller/OrganisationController.php index 525c9de..b11058c 100644 --- a/src/Controller/OrganisationController.php +++ b/src/Controller/OrganisationController.php @@ -836,6 +836,7 @@ class OrganisationController extends SimpleController 'fields' => $fields, 'tools' => $editButtons, 'delete_redirect' => $this->ci->router->pathFor('uri_organisations'), + 'leave_redirect' => $this->ci->router->pathFor('dashboard'), ]); } diff --git a/src/Controller/OrganisationMembersController.php b/src/Controller/OrganisationMembersController.php index b91e492..b84e7da 100644 --- a/src/Controller/OrganisationMembersController.php +++ b/src/Controller/OrganisationMembersController.php @@ -29,6 +29,75 @@ use UserFrosting\Support\Exception\NotFoundException; */ class OrganisationMembersController extends SimpleController { + + /** + * Processes the request to leave an organisation. + * + * Removes a member from the specified organisation. + * Before doing so, checks that: + * 1. The user has permission to leave this organisation; + * 2. The submitted data is valid. + * This route requires authentication. + * + * Request type: DELETE + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws NotFoundException If organisation is not found + * @throws ForbiddenException If user is not authorized to access page + * @throws BadRequestException + */ + public function leave(Request $request, Response $response, $args) + { + $organisation = $this->getOrganisationFromParams($args); + + // If the organisation doesn't exist, return 404 + if (!$organisation) { + 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 page + if (!$authorizer->checkAccess($currentUser, 'leave_organisation', [ + 'organisation' => $organisation, + ])) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($organisation, $currentUser) { + $currentUser->organisations()->detach($organisation->id); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} left organisation {$organisation->name}.", [ + 'type' => 'leave_organisation', + 'user_id' => $currentUser->id, + ]); + }); + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'ORGANISATION.LEAVE_SUCCESSFUL', [ + 'name' => $organisationName, + ]); + + return $response->withJson([], 200); + } + /** * Returns a list of organisation members. * @@ -82,6 +151,50 @@ class OrganisationMembersController extends SimpleController return $sprunje->toResponse($response); } + /** + * Get leave confirmation modal. + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws NotFoundException If organisation is not found + * @throws ForbiddenException If user is not authorized to access page + * @throws BadRequestException + */ + public function getModalConfirmLeave(Request $request, Response $response, $args) + { + $organisation = $this->getOrganisationFromParams($args); + + // If the organisation no longer exists, forward to main organisation listing page + if (!$organisation) { + 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 page + if (!$authorizer->checkAccess($currentUser, 'leave_organisation', [ + 'organisation' => $organisation, + ])) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + return $this->ci->view->render($response, 'modals/confirm-leave-organisation.html.twig', [ + 'organisation' => $organisation, + 'form' => [ + 'action' => "api/organisations/o/{$organisation->slug}/members", + ], + ]); + } + /** * Get organisation from params. diff --git a/src/Database/Seeds/OrganisationPermissions.php b/src/Database/Seeds/OrganisationPermissions.php index f5ad470..66c8e0f 100644 --- a/src/Database/Seeds/OrganisationPermissions.php +++ b/src/Database/Seeds/OrganisationPermissions.php @@ -75,6 +75,12 @@ class OrganisationPermissions extends BaseSeed 'conditions' => 'always()', 'description' => 'Merge two organisations together, including all the members.', ]), + 'leave_organisation' => new Permission([ + 'slug' => 'leave_organisation', + 'name' => 'Leave organisation', + 'conditions' => 'always()', + 'description' => 'Allows members to leave organisations.', + ]), 'delete_organisation' => new Permission([ 'slug' => 'delete_organisation', 'name' => 'Delete organisation', @@ -163,6 +169,7 @@ class OrganisationPermissions extends BaseSeed $roleUser->permissions()->syncWithoutDetaching([ $permissions['uri_organisation_own']->id, $permissions['view_organisation_field_own']->id, + $permissions['leave_organisation']->id, ]); } } diff --git a/templates/modals/confirm-leave-organisation.html.twig b/templates/modals/confirm-leave-organisation.html.twig new file mode 100644 index 0000000..a7091b5 --- /dev/null +++ b/templates/modals/confirm-leave-organisation.html.twig @@ -0,0 +1,17 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("ORGANISATION.LEAVE")}}{% endblock %} + +{% block modal_body %} +
+ {% include "forms/csrf.html.twig" %} +
+
+

{{translate("ORGANISATION.LEAVE_CONFIRM", {name: organisation.name})}}
{{translate("LEAVE_CANNOT_UNDONE")}}

+
+
+ + +
+
+{% endblock %} diff --git a/templates/pages/organisation.html.twig b/templates/pages/organisation.html.twig index 19121cc..3eb5b28 100644 --- a/templates/pages/organisation.html.twig +++ b/templates/pages/organisation.html.twig @@ -64,6 +64,12 @@

{% endif %} {% block organisation_profile %}{% endblock %} + {% if checkAccess('leave_organisation') %} +
+
+ +
+ {% endif %} @@ -95,7 +101,8 @@ true, // deep extend { organisation_slug: "{{organisation.slug}}", - delete_redirect: "{{delete_redirect}}" + delete_redirect: "{{delete_redirect}}", + leave_redirect: "{{leave_redirect}}" }, page );