diff --git a/src/Controller/OrganisationController.php b/src/Controller/OrganisationController.php index 298f699..ffe05e2 100644 --- a/src/Controller/OrganisationController.php +++ b/src/Controller/OrganisationController.php @@ -50,22 +50,26 @@ class OrganisationController extends SimpleController */ public function create(Request $request, Response $response, $args) { - // Get POST parameters: name, slug, icon, description - $params = $request->getParsedBody(); - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ $authorizer = $this->ci->authorizer; + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ $currentUser = $this->ci->currentUser; + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'create_organisation')) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; + // Get POST parameters: name, slug, icon, description + $params = $request->getParsedBody(); // Load the request schema $schema = new RequestSchema('schema://requests/organisation/create.yaml'); @@ -83,9 +87,6 @@ class OrganisationController extends SimpleController $error = true; } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - // Check if name or slug already exists if ($classMapper->getClassMapping('organisation')::findUnique($data['name'], 'name')) { $ms->addMessageTranslated('danger', 'ORGANISATION.NAME.IN_USE', $data); @@ -102,6 +103,7 @@ class OrganisationController extends SimpleController } $data['flag_approved'] = 1; + $data['registrant_id'] = $currentUser->id; // All checks passed! log events/activities and create organisation // Begin transaction - DB will be rolled back if an exception occurs @@ -139,6 +141,13 @@ class OrganisationController extends SimpleController */ public function getInfo(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; + + $organisation = $this->getOrganisationFromParams($args); // If the organisation doesn't exist, return 404 @@ -146,21 +155,6 @@ class OrganisationController extends SimpleController throw new NotFoundException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - // Join organisation's most recent activity - $organisation = $classMapper->createInstance('organisation') - ->where('slug', $organisation->slug) - ->joinMemberCounts() - ->first(); - - /** @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, 'uri_organisation', [ 'organisation' => $organisation, @@ -168,10 +162,11 @@ class OrganisationController extends SimpleController throw new ForbiddenException(); } + // Join organisation's member counts + $organisation = $organisation->joinMemberCounts()->first(); + $result = $organisation->toArray(); - // 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 $response->withJson($result, 200, JSON_PRETTY_PRINT); } @@ -197,6 +192,19 @@ class OrganisationController extends SimpleController */ public function update(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @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 organisation based on slug in URL $organisation = $this->getOrganisationFromParams($args); @@ -207,9 +215,6 @@ class OrganisationController extends SimpleController // Get PUT parameters: (name, slug, icon, description) $params = $request->getParsedBody(); - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - // Load the request schema $schema = new RequestSchema('schema://requests/organisation/edit-info.yaml'); @@ -232,12 +237,6 @@ class OrganisationController extends SimpleController $fieldNames[] = $name; } - /** @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 submitted fields for this organisation if (!$authorizer->checkAccess($currentUser, 'update_organisation_field', [ 'organisation' => $organisation, @@ -246,14 +245,11 @@ class OrganisationController extends SimpleController throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - // Check if name or slug already exists if ( isset($data['name']) && $data['name'] != $organisation->name && - $classMapper->getClassMapping('organisation')::where('name', $data['name'])->first() + $classMapper->getClassMapping('organisation')::findUnique($data['name'], 'name') ) { $ms->addMessageTranslated('danger', 'ORGANISATION.NAME.IN_USE', $data); $error = true; @@ -262,7 +258,7 @@ class OrganisationController extends SimpleController if ( isset($data['slug']) && $data['slug'] != $organisation->slug && - $classMapper->getClassMapping('organisation')::where('slug', $data['slug'])->first() + $classMapper->getClassMapping('organisation')::findUnique($data['slug'], 'slug') ) { $ms->addMessageTranslated('danger', 'ORGANISATION.SLUG.IN_USE', $data); $error = true; @@ -319,6 +315,24 @@ class OrganisationController extends SimpleController */ public function merge(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'merge_organisations')) { + throw new ForbiddenException(); + } + // Get POST parameters $params = $request->getParsedBody(); @@ -329,53 +343,26 @@ class OrganisationController extends SimpleController $transformer = new RequestDataTransformer($schema); $data = $transformer->transform($params); - // Validate, and throw exception on validation errors. + // Validate, and return bad request on validation errors. $validator = new ServerSideValidator($schema, $this->ci->translator); if (!$validator->validate($data)) { - $e = new BadRequestException(); - - foreach ($validator->errors() as $idx => $field) { - foreach ($field as $eidx => $error) { - $e->addUserMessage($error); - } - } - - throw $e; + $ms->addValidationErrors($validator); + return $response->withJson([], 400); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - // Get the organisations - $source = $classMapper->getClassMapping('organisation')::where('slug', $data['source_slug'])->first(); - $target = $classMapper->getClassMapping('organisation')::where('slug', $data['target_slug'])->first(); + $source = $classMapper->getClassMapping('organisation')::findUnique($data['source_slug'], 'slug', false); + $target = $classMapper->getClassMapping('organisation')::findUnique($data['target_slug'], 'slug', false); // If a organisation doesn't exist, return 404 if (!$source || !$target) { - throw new BadRequestException(); + 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, 'merge_organisations')) { - throw new ForbiddenException(); - } - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $sourceName = $source->name; - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($source, $sourceName, $target, $currentUser) { + Capsule::transaction(function () use ($source, $target, $currentUser) { + $sourceName = $source->name; + $this->ci->get('organisation.beforeMerge')($source, $target); $source->beforeMerge($target, ['currentUser' => $currentUser]); @@ -390,9 +377,6 @@ class OrganisationController extends SimpleController ]); }); - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - $ms->addMessageTranslated('success', 'ORGANISATION.MERGE_SUCCESSFUL', [ 'source' => $sourceName, 'target' => $target->name, @@ -423,6 +407,16 @@ class OrganisationController extends SimpleController */ public function delete(Request $request, Response $response, $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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + $organisation = $this->getOrganisationFromParams($args); // If the organisation doesn't exist, return 404 @@ -430,12 +424,6 @@ class OrganisationController extends SimpleController 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, 'delete_organisation', [ 'organisation' => $organisation, @@ -443,16 +431,11 @@ class OrganisationController extends SimpleController throw new ForbiddenException(); } - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $organisationName = $organisation->name; - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($organisation, $organisationName, $currentUser) { + Capsule::transaction(function () use ($organisation, $currentUser) { + $organisationName = $organisation->name; + + // Delete the organisation (soft) $organisation->delete(); unset($organisation); @@ -463,9 +446,6 @@ class OrganisationController extends SimpleController ]); }); - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - $ms->addMessageTranslated('success', 'ORGANISATION.DELETION_SUCCESSFUL', [ 'name' => $organisationName, ]); @@ -495,6 +475,16 @@ class OrganisationController extends SimpleController */ public function deletePermenent(Request $request, Response $response, $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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + $organisation = $this->getOrganisationFromParams($args, true); // If the organisation doesn't exist, return 404 @@ -502,12 +492,6 @@ class OrganisationController extends SimpleController 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, 'permenent_delete_organisation', [ 'organisation' => $organisation, @@ -515,16 +499,11 @@ class OrganisationController extends SimpleController throw new ForbiddenException(); } - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - $organisationName = $organisation->name; // Begin transaction - DB will be rolled back if an exception occurs Capsule::transaction(function () use ($organisation, $organisationName, $currentUser) { + // Delete the organisation (HARD) $organisation->delete(true); unset($organisation); @@ -535,9 +514,6 @@ class OrganisationController extends SimpleController ]); }); - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - $ms->addMessageTranslated('success', 'ORGANISATION.PERMENENT_DELETION_SUCCESSFUL', [ 'name' => $organisationName, ]); @@ -565,6 +541,16 @@ class OrganisationController extends SimpleController */ public function restore(Request $request, Response $response, $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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + $organisation = $this->getOrganisationFromParams($args, true); // If the organisation doesn't exist, return 404 @@ -572,12 +558,6 @@ class OrganisationController extends SimpleController 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, 'restore_organisation', [ 'organisation' => $organisation, @@ -585,16 +565,10 @@ class OrganisationController extends SimpleController 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) { if (!$organisation->flag_approved) { - $verification = $this->ci->repoOrganisationApproval->revert($organisation); + $this->ci->repoOrganisationApproval->revert($organisation); } $organisation->restore(); @@ -606,9 +580,6 @@ class OrganisationController extends SimpleController ]); }); - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - $ms->addMessageTranslated('success', 'ORGANISATION.RESTORE_SUCCESSFUL', [ 'name' => $organisation->name, ]); @@ -633,22 +604,23 @@ class OrganisationController extends SimpleController */ public function getList(Request $request, Response $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ $authorizer = $this->ci->authorizer; + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ $currentUser = $this->ci->currentUser; + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'uri_organisations')) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; + // GET parameters + $params = $request->getQueryParams(); $params['ci'] = $this->ci; @@ -682,22 +654,23 @@ class OrganisationController extends SimpleController */ public function getListDeleted(Request $request, Response $response, $args) { - // GET parameters - $params = $request->getQueryParams(); - /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ $authorizer = $this->ci->authorizer; + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ $currentUser = $this->ci->currentUser; + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'uri_deleted_organisations')) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; + // GET parameters + $params = $request->getQueryParams(); $sprunje = $classMapper->createInstance('organisation_sprunje', $classMapper, $params); $sprunje->extendQuery(function ($query) use ($user) { @@ -722,6 +695,16 @@ class OrganisationController extends SimpleController */ public function getModalConfirmDelete(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // GET parameters $params = $request->getQueryParams(); @@ -732,12 +715,6 @@ class OrganisationController extends SimpleController 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, 'delete_organisation', [ 'organisation' => $organisation, @@ -745,9 +722,6 @@ class OrganisationController extends SimpleController throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - return $this->ci->view->render($response, 'modals/confirm-delete-organisation.html.twig', [ 'organisation' => $organisation, 'form' => [ @@ -769,6 +743,16 @@ class OrganisationController extends SimpleController */ public function getModalConfirmPermenentDelete(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // GET parameters $params = $request->getQueryParams(); @@ -779,12 +763,6 @@ class OrganisationController extends SimpleController 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, 'permenent_delete_organisation', [ 'organisation' => $organisation, @@ -792,9 +770,6 @@ class OrganisationController extends SimpleController throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - return $this->ci->view->render($response, 'modals/confirm-permenent-delete-organisation.html.twig', [ 'organisation' => $organisation, 'form' => [ @@ -822,20 +797,21 @@ class OrganisationController extends SimpleController /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ $authorizer = $this->ci->authorizer; + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ $currentUser = $this->ci->currentUser; /** @var \UserFrosting\I18n\Translator $translator */ $translator = $this->ci->translator; + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'create_organisation')) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - // Create a dummy organisation to prepopulate fields $organisation = $classMapper->createInstance('organisation', []); @@ -880,6 +856,16 @@ class OrganisationController extends SimpleController */ public function getModalEdit(Request $request, Response $response, $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\I18n\Translator $translator */ + $translator = $this->ci->translator; + + // GET parameters $params = $request->getQueryParams(); @@ -890,18 +876,6 @@ class OrganisationController extends SimpleController throw new NotFoundException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @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\I18n\Translator $translator */ - $translator = $this->ci->translator; - // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "description" for this organisation $fieldNames = ['name', 'slug', 'description']; if (!$authorizer->checkAccess($currentUser, 'update_organisation_field', [ @@ -955,6 +929,19 @@ class OrganisationController extends SimpleController */ public function getModalMerge(Request $request, Response $response, $args) { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\I18n\Translator $translator */ + $translator = $this->ci->translator; + + // GET parameters $params = $request->getQueryParams(); @@ -965,18 +952,6 @@ class OrganisationController extends SimpleController throw new NotFoundException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @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\I18n\Translator $translator */ - $translator = $this->ci->translator; - // Access-controlled resource - check that currentUser has permission to merge organisations. if (!$authorizer->checkAccess($currentUser, 'merge_organisations')) { throw new ForbiddenException(); @@ -1020,7 +995,13 @@ class OrganisationController extends SimpleController * @throws ForbiddenException If user is not authorized to access page */ public function pageInfo(Request $request, Response $response, $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; + + $organisation = $this->getOrganisationFromParams($args); // If the organisation no longer exists, forward to main organisation listing page @@ -1028,12 +1009,6 @@ class OrganisationController extends SimpleController 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, 'uri_organisation', [ 'organisation' => $organisation, diff --git a/src/Controller/OrganisationRegistrationController.php b/src/Controller/OrganisationRegistrationController.php index 8a6ec3f..276b516 100644 --- a/src/Controller/OrganisationRegistrationController.php +++ b/src/Controller/OrganisationRegistrationController.php @@ -54,25 +54,29 @@ class OrganisationRegistrationController extends SimpleController */ public function register(Request $request, Response $response, $args) { - // Get POST parameters: name, slug, icon, description - $params = $request->getParsedBody(); - /** @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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'register_organisation')) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; + // Get POST parameters: name, slug, icon, description + $params = $request->getParsedBody(); // Load the request schema $schema = new RequestSchema('schema://requests/organisation/create.yaml'); @@ -90,9 +94,6 @@ class OrganisationRegistrationController extends SimpleController $error = true; } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - // Check if name or slug already exists if ($classMapper->getClassMapping('organisation')::findUnique($data['name'], 'name')) { $ms->addMessageTranslated('danger', 'ORGANISATION.NAME.IN_USE_REGISTER', $data); @@ -103,6 +104,7 @@ class OrganisationRegistrationController extends SimpleController return $response->withJson([], 400); } + // Generate a unique slug $slugIncrement = 0; $slugIncrementString = ''; while ($classMapper->getClassMapping('organisation')::findUnique($data['slug'] . $slugIncrementString, 'slug')) { @@ -111,41 +113,45 @@ class OrganisationRegistrationController extends SimpleController } $data['slug'] .= $slugIncrementString; + // Prefill some default values $data['flag_approved'] = !$config['organisation.registration.require_approval']; + $data['registrant_id'] = $currentUser->id; // All checks passed! log events/activities and create organisation // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($classMapper, $data, $ms, $currentUser, $config) { + Capsule::transaction(function () use ($classMapper, $data, $currentUser, $config) { // Create the organisation $organisation = $classMapper->createInstance('organisation', $data); // Store new organisation to database $organisation->save(); - // Attach the members - $organisation->members()->attach($currentUser, ['flag_admin' => true]); + // Attach the creating member + $organisation->members()->attach($currentUser->id, [ + 'flag_admin' => 1, + 'flag_approved' => 1, + ]); - // Save members - $organisation->save(); + // If approval is required, generate a token & send an email + if ($config['organisation.registration.require_approval']) { + $timeout = $config['organisation.registration.timeout']; + + // Try to generate a new approval request + $approval = $this->ci->repoOrganisationApproval->create($organisation, $timeout); + + $this->sendApprovalEmail($currentUser, $organisation, $approval->getToken(), $timeout); + } // Create activity record $this->ci->userActivityLogger->info("User {$currentUser->user_name} registered organisation {$organisation->name}.", [ 'type' => 'organisation_register', 'user_id' => $currentUser->id, ]); - - if ($config['organisation.registration.require_approval']) { - $timeout = $this->ci->config['organisation.registration.timeout']; - - // Try to generate a new approval request - $approval = $this->ci->repoOrganisationApproval->create($organisation, $currentUser, $timeout); - - $this->sendApprovalEmail($currentUser, $organisation, $approval->getToken()); - } - - $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.SUCCESSFUL', $data); }); + // Generate a success message + $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.SUCCESSFUL', $data); + return $response->withJson([], 200); } @@ -156,7 +162,7 @@ class OrganisationRegistrationController extends SimpleController * Before doing so, checks that: * 1. The user has permission to cancel this organisation registration request; * 2. The submitted data is valid. - * This route requires authentication (and should generally be limited to admins or the root user). + * This route requires authentication. (NOTE: This route is only available to the organisation registrant!) * * Request type: DELETE * @@ -170,6 +176,17 @@ class OrganisationRegistrationController extends SimpleController */ public function cancel(Request $request, Response $response, $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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + + // Get the organisation from the uri $organisation = $this->getOrganisationFromParams($args); // If the organisation doesn't exist, return 404 @@ -177,12 +194,6 @@ class OrganisationRegistrationController extends SimpleController 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, 'register_organisation', [ 'organisation' => $organisation, @@ -190,20 +201,16 @@ class OrganisationRegistrationController extends SimpleController throw new ForbiddenException(); } - if (!$authorizer->runCallback($currentUser, 'is_organisation_admin', $currentUser->id, $organisation->id)) { + // Check the user is registrant of this organisation + if ($organisation->registrant_id == $currentUser->id) { throw new ForbiddenException(); } - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - $organisationName = $organisation->name; - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($organisation, $organisationName, $currentUser) { + Capsule::transaction(function () use ($organisation, $currentUser) { + $organisationName = $organisation->name; + + // Delete the organisation $organisation->delete(true); unset($organisation); @@ -214,9 +221,7 @@ class OrganisationRegistrationController extends SimpleController ]); }); - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - + // Generate a response $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.CANCEL_SUCCESSFUL', [ 'name' => $organisationName, ]); @@ -243,21 +248,22 @@ class OrganisationRegistrationController extends SimpleController */ public function approve(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - /** @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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) { + throw new ForbiddenException(); + } + + // Get the organisation from the uri $organisation = $this->getOrganisationFromParams($args); // If the organisation doesn't exist, return 404 @@ -265,22 +271,30 @@ class OrganisationRegistrationController extends SimpleController throw new NotFoundException(); } - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) { - throw new ForbiddenException(); + $approval = $this->ci->repoOrganisationApproval->exists($organisation); + if (!$approval) { + $ms->addMessageTranslated('danger', 'ORGANISATION.REGISTRATION.TOKEN_NOT_FOUND'); + return $response->withJson([], 400); } - $verification = $this->ci->repoOrganisationApproval->completeWithoutToken($organisation, $currentUser, ['approved' => true]); + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($organisation, $currentUser) { + $this->ci->repoOrganisationApproval->completeForOwner($organisation, [ + 'approver_id' => $currentUser->id, + 'approved' => true + ]); - $requester = $classMapper->getClassMapping('user')::find($verification->requester_id); + // Send a notification email to the registrant + $this->sendApprovedEmail($organisation); - $this->sendApprovedEmail($organisation, $requester); + // Record the event in the activity log + $this->ci->userActivityLogger->info("User {$currentUser->user_name} approved the registration request for organisation {$organisation->name}.", [ + 'type' => 'organisation_approved', + 'user_id' => $currentUser->id, + ]); + }); - $this->ci->userActivityLogger->info("User {$currentUser->user_name} approved the registration request for organisation {$organisation->name}.", [ - 'type' => 'organisation_approved', - 'user_id' => $currentUser->id, - ]); - + // Generate a response $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.APPROVED', [ 'name' => $organisation->name ]); @@ -307,21 +321,19 @@ class OrganisationRegistrationController extends SimpleController */ public function approveToken(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ $classMapper = $this->ci->classMapper; - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) { throw new ForbiddenException(); @@ -342,39 +354,50 @@ class OrganisationRegistrationController extends SimpleController if (!$validator->validate($data)) { $ms->addValidationErrors($validator); - return $response->withRedirect($this->ci->router->pathFor('dashboard')); + return $response->withRedirect($this->ci->router->pathFor('uri_organisations')); } - $verification = $this->ci->repoOrganisationApproval->complete($data['token'], $currentUser, ['approved' => true]); + $token = $data['token']; - if (!$verification) { + $organisation = $classMapper->getClassMapping('organisation')::query() + ->where('id', $this->ci->repoOrganisationApproval->findOwner($token)) + ->first(); + if (!$organisation) { $ms->addMessageTranslated('danger', 'ORGANISATION.REGISTRATION.TOKEN_NOT_FOUND'); - return $response->withRedirect($this->ci->router->pathFor('dashboard')); + return $response->withRedirect($this->ci->router->pathFor('uri_organisations')); } - - $organisation = $classMapper->getClassMapping('organisation')::find($verification->organisation_id); - $requester = $classMapper->getClassMapping('user')::find($verification->requester_id); - $this->sendApprovedEmail($organisation, $requester); + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($currentUser, $token, $organisation) { + $this->ci->repoOrganisationApproval->complete($token, [ + 'approver_id' => $currentUser->id, + 'approved' => true + ]); - $this->ci->userActivityLogger->info("User {$currentUser->user_name} approved the registration request for organisation {$organisation->name}.", [ - 'type' => 'organisation_approved', - 'user_id' => $currentUser->id, - ]); - + // Send a notification email to the registrant + $this->sendApprovedEmail($organisation); + + // Record the event in the activity log + $this->ci->userActivityLogger->info("User {$currentUser->user_name} approved the registration request for organisation {$organisation->name}.", [ + 'type' => 'organisation_approved', + 'user_id' => $currentUser->id, + ]); + }); + + // Generate the response $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.APPROVED', [ 'name' => $organisation->name ]); - // Forward to login page - return $response->withRedirect($this->ci->router->pathFor('dashboard')); + // Forward to organisations page + return $response->withRedirect($this->ci->router->pathFor('uri_organisations')); } /** * Denies an organisation registration request. * - * Processes the request from the email verification link that was emailed to the organisation administrators, checking that: - * 1. The token provided matches an organisation in the database; + * Processes the request from the organisation page, checking that: + * 1. The organisation exists; * 2. The organisation is not already approved; * This route requires authorization. * @@ -389,21 +412,22 @@ class OrganisationRegistrationController extends SimpleController */ public function deny(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; - - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - /** @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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) { + throw new ForbiddenException(); + } + + // Get the organisation from the uri $organisation = $this->getOrganisationFromParams($args); // If the organisation doesn't exist, return 404 @@ -411,24 +435,30 @@ class OrganisationRegistrationController extends SimpleController throw new NotFoundException(); } - // Access-controlled page - if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) { - throw new ForbiddenException(); + $approval = $this->ci->repoOrganisationApproval->exists($organisation); + if (!$approval) { + $ms->addMessageTranslated('danger', 'ORGANISATION.REGISTRATION.TOKEN_NOT_FOUND'); + return $response->withJson([], 400); } - $verification = $this->ci->repoOrganisationApproval->completeWithoutToken($organisation, $currentUser, ['approved' => false]); + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($organisation, $currentUser) { + $this->ci->repoOrganisationApproval->completeForOwner($organisation, [ + 'approver_id' => $currentUser->id, + 'approved' => false + ]); - $requester = $classMapper->getClassMapping('user')::find($verification->requester_id); + // Send a notification email to the registrant + $this->sendDeniedEmail($organisation); - $this->sendDeniedEmail($organisation, $requester); + // Record the event in the activity log + $this->ci->userActivityLogger->info("User {$currentUser->user_name} denied the registration request for organisation {$organisation->name}.", [ + 'type' => 'organisation_approved', + 'user_id' => $currentUser->id, + ]); + }); - $organisation->delete(); - - $this->ci->userActivityLogger->info("User {$currentUser->user_name} denied the registration request for organisation {$organisation->name}.", [ - 'type' => 'organisation_approval', - 'user_id' => $currentUser->id, - ]); - + // Generate a response $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.DENIED', [ 'name' => $organisation->name ]); @@ -455,23 +485,18 @@ class OrganisationRegistrationController extends SimpleController */ public function denyToken(Request $request, Response $response, $args) { - /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ - $ms = $this->ci->alerts; + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ $classMapper = $this->ci->classMapper; - /** @var \UserFrosting\Support\Repository\Repository $config */ - $config = $this->ci->config; - - /** @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\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; - $dashboardPage = $this->ci->router->pathFor('dashboard'); // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) { @@ -493,33 +518,43 @@ class OrganisationRegistrationController extends SimpleController if (!$validator->validate($data)) { $ms->addValidationErrors($validator); - return $response->withRedirect($dashboardPage); + return $response->withRedirect($this->ci->router->pathFor('uri_organisations')); } - $verification = $this->ci->repoOrganisationApproval->complete($data['token'], $currentUser, ['approved' => false]); + $token = $data['token']; - if ($verification === false) { + $organisation = $classMapper->getClassMapping('organisation')::query() + ->where('id', $this->ci->repoOrganisationApproval->findOwner($token)) + ->first(); + if (!$organisation) { $ms->addMessageTranslated('danger', 'ORGANISATION.REGISTRATION.TOKEN_NOT_FOUND'); - - return $response->withRedirect($dashboardPage); + return $response->withRedirect($this->ci->router->pathFor('uri_organisations')); } - - $organisation = $classMapper->getClassMapping('organisation')::find($verification->organisation_id); - $requester = $classMapper->getClassMapping('user')::find($verification->requester_id); - $this->sendDeniedEmail($organisation, $requester); + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction(function () use ($currentUser, $token, $organisation) { + $this->ci->repoOrganisationApproval->complete($token, [ + 'approver_id' => $currentUser->id, + 'approved' => false + ]); - $this->ci->userActivityLogger->info("User {$currentUser->user_name} denied the registration request for organisation {$organisation->name}.", [ - 'type' => 'organisation_approval', - 'user_id' => $currentUser->id, - ]); + // Send a notification email to the registrant + $this->sendDeniedEmail($organisation); + // Record the event in the activity log + $this->ci->userActivityLogger->info("User {$currentUser->user_name} denied the registration request for organisation {$organisation->name}.", [ + 'type' => 'organisation_approved', + 'user_id' => $currentUser->id, + ]); + }); + + // Generate the response $ms->addMessageTranslated('success', 'ORGANISATION.REGISTRATION.DENIED', [ 'name' => $organisation->name ]); - // Forward to login page - return $response->withRedirect($dashboardPage); + // Forward to organisations page + return $response->withRedirect($this->ci->router->pathFor('uri_organisations')); } @@ -542,20 +577,21 @@ class OrganisationRegistrationController extends SimpleController /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ $authorizer = $this->ci->authorizer; + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ $currentUser = $this->ci->currentUser; /** @var \UserFrosting\I18n\Translator $translator */ $translator = $this->ci->translator; + // Access-controlled page if (!$authorizer->checkAccess($currentUser, 'register_organisation')) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - // Create a dummy organisation to prepopulate fields $organisation = $classMapper->createInstance('organisation', []); @@ -569,6 +605,7 @@ class OrganisationRegistrationController extends SimpleController $schema = new RequestSchema('schema://requests/organisation/create.yaml'); $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + // Generate the response return $this->ci->view->render($response, 'modals/organisation.html.twig', [ 'organisation' => $organisation, 'form' => [ @@ -596,6 +633,12 @@ class OrganisationRegistrationController extends SimpleController */ public function getModalConfirmCancel(Request $request, Response $response, $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; + // GET parameters $params = $request->getQueryParams(); @@ -606,12 +649,6 @@ class OrganisationRegistrationController extends SimpleController 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, 'register_organisation', [ 'organisation' => $organisation, @@ -619,14 +656,12 @@ class OrganisationRegistrationController extends SimpleController throw new ForbiddenException(); } - // Access-controlled page - if (!$authorizer->runCallback($currentUser, 'is_organisation_admin', $currentUser->id, $organisation->id)) { + // Check the user is registrant of this organisation + if ($organisation->registrant_id == $currentUser->id) { throw new ForbiddenException(); } - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - + // Generate the response return $this->ci->view->render($response, 'modals/confirm-cancel-organisation-registration.html.twig', [ 'organisation' => $organisation, 'form' => [ @@ -642,10 +677,8 @@ class OrganisationRegistrationController extends SimpleController * @param UserInterface $requester The user to send the confirmation of registration to * @param UserInterface $organisation The organisation to send the email for */ - protected function sendApprovalEmail(UserInterface $requester, OrganisationInterface $organisation, $token) + protected function sendApprovalEmail(UserInterface $requester, OrganisationInterface $organisation, $token, $timeout) { - $timeout = $this->ci->config['organisation.registration.timeout']; - // Create and send approval email $message = new TwigMailMessage($this->ci->view, 'mail/organisation-approval-request.html.twig'); @@ -657,18 +690,19 @@ class OrganisationRegistrationController extends SimpleController 'approval_expiration' => ($timeout > 0 ? floor($timeout / 86400) . ' days' : false), ]); + // Find the organisations admins or, if there are none, the site admin. $role = $this->ci->classMapper->getClassMapping('role')::where('slug', 'organisations-admin')->with('users')->first(); if ($role->users()->count() == 0) { $role = $this->ci->classMapper->getClassMapping('role')::where('slug', 'site-admin')->with('users')->first(); } + // Generate a list of recipients and send the email addressed to each. $recipients = $role->users()->get(); foreach($recipients as $recipient) { $message->addEmailRecipient(new EmailRecipient($recipient->email, $recipient->full_name)); $message->addParams([ 'recipient' => $recipient ]); $this->ci->mailer->send($message); - $message->addParams([ 'recipient' => null ]); $message->clearRecipients(); } } @@ -679,8 +713,10 @@ class OrganisationRegistrationController extends SimpleController * @param UserInterface $requester The user to send the approved notice to * @param UserInterface $organisation The organisation to send the email for */ - protected function sendApprovedEmail(OrganisationInterface $organisation, UserInterface $requester) + protected function sendApprovedEmail(OrganisationInterface $organisation) { + $requester = $organisation->registrant()->first(); + $message = new TwigMailMessage($this->ci->view, 'mail/organisation-approval-approved.html.twig'); $message->from($this->ci->config['address_book.admin']) @@ -699,8 +735,13 @@ class OrganisationRegistrationController extends SimpleController * @param UserInterface $requester The user to send the denial notice to * @param UserInterface $organisation The organisation to send the email for */ - protected function sendDeniedEmail(OrganisationInterface $organisation, UserInterface $requester) + protected function sendDeniedEmail(OrganisationInterface $organisation) { + $requester = $organisation->registrant()->first(); + + $this->ci->debugLogger->debug('organisation', [$organisation]); + $this->ci->debugLogger->debug('requester', [$requester]); + $message = new TwigMailMessage($this->ci->view, 'mail/organisation-approval-denied.html.twig'); $message->from($this->ci->config['address_book.admin']) diff --git a/src/Database/Migrations/v003/UpdateOrganisationApprovalsTable.php b/src/Database/Migrations/v003/UpdateOrganisationApprovalsTable.php new file mode 100644 index 0000000..d8d0158 --- /dev/null +++ b/src/Database/Migrations/v003/UpdateOrganisationApprovalsTable.php @@ -0,0 +1,68 @@ +schema->hasTable('organisation_approvals')) { + DB::table('organisation_approvals')->delete(); + $this->schema->table('organisation_approvals', function (Blueprint $table) { + $table->dropForeign(['requester_id']); + $table->dropForeign(['organisation_id']); + $table->dropIndex(['requester_id']); + $table->dropColumn(['requester_id']); + $table->dropColumn(['organisation_id']); + + $table->integer('owner_id')->unsigned(); + $table->index('owner_id'); + }); + } + } + + /** + * {@inheritdoc} + */ + public function down() + { + DB::table('organisation_approvals')->delete(); + $this->schema->table('organisation_approvals', function (Blueprint $table) { + $table->dropIndex(['owner_id']); + $table->dropColumn('owner_id')->unsigned(); + + $table->integer('requester_id')->unsigned(); + $table->integer('organisation_id')->unsigned(); + $table->index('requester_id'); + $table->foreign('requester_id')->references('id')->on('users'); + $table->foreign('organisation_id')->references('id')->on('organisations'); + }); + } +} diff --git a/src/Database/Migrations/v003/UpdateOrganisationsTable.php b/src/Database/Migrations/v003/UpdateOrganisationsTable.php new file mode 100644 index 0000000..d39b46a --- /dev/null +++ b/src/Database/Migrations/v003/UpdateOrganisationsTable.php @@ -0,0 +1,55 @@ +schema->hasTable('organisations')) { + $this->schema->table('organisations', function (Blueprint $table) { + $table->integer('registrant_id')->unsigned()->nullable(); + }); + } + } + + /** + * {@inheritdoc} + */ + public function down() + { + $this->schema->table('organisations', function (Blueprint $table) { + $table->dropColumn('registrant_id'); + }); + } +} diff --git a/src/Database/Models/BaseOrganisationApproval.php b/src/Database/Models/BaseOrganisationApproval.php deleted file mode 100644 index e80caaa..0000000 --- a/src/Database/Models/BaseOrganisationApproval.php +++ /dev/null @@ -1,111 +0,0 @@ -token; - } - - /** - * @param string $value - * - * @return self - */ - public function setToken($value) - { - $this->token = $value; - - return $this; - } - - /** - * Get the user associated with this request of this approval. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function requester() - { - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('user'), 'requester_id'); - } - - /** - * Get the organisation associated with this approval request. - * - * @return \Illuminate\Database\Eloquent\Relations\belongsTo - */ - public function organisation() - { - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('organisation'), 'organisation_id'); - } - - /** - * Get the user associated with this approval or denial of this request. - * - * @return \Illuminate\Database\Eloquent\Relations\belongsTo - */ - public function approver() - { - /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = static::$ci->classMapper; - - return $this->belongsTo($classMapper->getClassMapping('user'), 'approver_id'); - } -} diff --git a/src/Database/Models/Organisation.php b/src/Database/Models/Organisation.php index 718dc21..4cbcea6 100644 --- a/src/Database/Models/Organisation.php +++ b/src/Database/Models/Organisation.php @@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Capsule\Manager as DB; use UserFrosting\Sprinkle\Core\Database\Models\Model; use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationInterface; +use UserFrosting\Sprinkle\Organisations\Repository\Interfaces\TokenOwnerInterface; /** * Organisation Class. @@ -28,7 +29,7 @@ use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationI * @property timestamp $updated_at * @property timestamp $deleted_at */ -class Organisation extends Model implements OrganisationInterface +class Organisation extends Model implements OrganisationInterface, TokenOwnerInterface { use SoftDeletes; @@ -46,6 +47,7 @@ class Organisation extends Model implements OrganisationInterface 'slug', 'name', 'description', + 'registrant_id', 'flag_approved', 'deleted_at', ]; @@ -82,6 +84,25 @@ class Organisation extends Model implements OrganisationInterface */ public $timestamps = true; + /** + * Get the mapping id, as per TokenOwnerInterface + */ + public function getId() + { + return $this->id; + } + + + /** + * Get the user who registered this organisation + */ + public function registrant() + { + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->hasOne($classMapper->getClassMapping('user'), 'id', 'registrant_id'); + } /** * Get a list of members within this organisation. @@ -93,8 +114,7 @@ class Organisation extends Model implements OrganisationInterface return $this ->belongsToMany($classMapper->getClassMapping('user'), 'organisation_members', 'organisation_id', 'user_id') - ->where('flag_admin', false) - ->withTimestamps(); + ->where('flag_admin', false); } /** @@ -107,8 +127,7 @@ class Organisation extends Model implements OrganisationInterface return $this ->belongsToMany($classMapper->getClassMapping('user'), 'organisation_members', 'organisation_id', 'user_id') - ->where('flag_admin', true) - ->withTimestamps(); + ->where('flag_admin', true); } /** @@ -127,7 +146,7 @@ class Organisation extends Model implements OrganisationInterface static::$ci->get('organisation.beforeDelete')($this); // Remove all organisation tokens - $classMapper->getClassMapping('organisation_approval')::where('organisation_id', $this->id)->delete(); + $classMapper->getClassMapping('organisation_approval')::where('owner_id', $this->id)->delete(); // Remove all member associations $this->members()->detach(); diff --git a/src/Database/Models/OrganisationApproval.php b/src/Database/Models/OrganisationApproval.php index 349abeb..b182390 100644 --- a/src/Database/Models/OrganisationApproval.php +++ b/src/Database/Models/OrganisationApproval.php @@ -9,8 +9,7 @@ namespace UserFrosting\Sprinkle\Organisations\Database\Models; -use Illuminate\Database\Capsule\Manager as DB; -use UserFrosting\Sprinkle\Organisations\Database\Models\BaseOrganisationApproval; +use UserFrosting\Sprinkle\Core\Database\Models\Model; /** * Organisation Approval Class. @@ -18,12 +17,111 @@ use UserFrosting\Sprinkle\Organisations\Database\Models\BaseOrganisationApproval * Represents a pending organisation approval request. * * @author Craig Williams (https://avsdev.uk) + * + * @property int $owner_id + * @property hash $token + * @property bool $completed + * @property datetime $expires_at + * @property datetime $completed_at + * @property int $approver_id */ - -class OrganisationApproval extends BaseOrganisationApproval +class OrganisationApproval extends Model { /** * @var string The name of the table for the current model. */ protected $table = 'organisation_approvals'; + + /** + * Fields that should be mass-assignable when creating a new Organisation. + * + * @var string[] + */ + protected $fillable = [ + 'owner_id', + 'hash', + 'completed', + 'expires_at', + 'completed_at', + 'approver_id', + ]; + + /** + * @var bool Enable timestamps for Verifications. + */ + public $timestamps = true; + + + /** + * @var string Stores the raw (unhashed) token when created, so that it can be emailed out to the user. NOT persisted. + */ + protected $token; + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * @param string $value + * + * @return self + */ + public function setToken($value) + { + $this->token = $value; + + return $this; + } + + + + /** + * Get the owner. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function owner() + { + return $this->organisation(); + } + + /** + * Get the organisation of this approval. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function organisation() + { + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('organisation'), 'owner_id'); + } + + /** + * Get the requester of this approval. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function requester() + { + return $this->organisation->registrant(); + } + + /** + * Get the user associated with this approval or denial of this request. + * + * @return \Illuminate\Database\Eloquent\Relations\belongsTo + */ + public function approver() + { + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'approver_id'); + } } diff --git a/src/Database/Models/User.php b/src/Database/Models/User.php index 57f8c05..f1e45b8 100644 --- a/src/Database/Models/User.php +++ b/src/Database/Models/User.php @@ -37,7 +37,11 @@ class User extends UFUser /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ $classMapper = static::$ci->classMapper; - return $this->belongsToMany($classMapper->getClassMapping('organisation'), 'organisation_members', 'user_id', 'organisation_id')->orderBy('organisations.name', 'asc')->withPivot('flag_admin'); + return $this->belongsToMany( + $classMapper->getClassMapping('organisation'), 'organisation_members', 'user_id', 'organisation_id' + ) + ->orderBy('organisations.name', 'asc') + ->withPivot('flag_admin'); } /** @@ -53,7 +57,16 @@ class User extends UFUser $classMapper = static::$ci->classMapper; if ($hardDelete) { - // TODO: remove user from organisations? + $classMapper->getClassMapping('organisation')::query() + ->where('registrant_id', $this->id) + ->update(['registrant_id' => null]); + + $classMapper->getClassMapping('organisation_approval')::query() + ->where('approver_id', $this->id) + ->update(['approver_id' => null]); + + $this->organisations()->detach(); + $this->refresh(); } return parent::delete(); diff --git a/src/Repository/OrganisationApprovalRepository.php b/src/Repository/OrganisationApprovalRepository.php index 366ab78..2e7e88a 100644 --- a/src/Repository/OrganisationApprovalRepository.php +++ b/src/Repository/OrganisationApprovalRepository.php @@ -9,12 +9,6 @@ namespace UserFrosting\Sprinkle\Organisations\Repository; -use Carbon\Carbon; -use Illuminate\Database\Capsule\Manager as Capsule; -use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationInterface; -use UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface; -use UserFrosting\Sprinkle\Account\Repository\TokenRepository; -use UserFrosting\Sprinkle\Core\Database\Models\Model; use UserFrosting\Sprinkle\Core\Util\ClassMapper; /** @@ -22,215 +16,38 @@ use UserFrosting\Sprinkle\Core\Util\ClassMapper; * * @author Craig Williams (https://avsdev.uk) */ -class OrganisationApprovalRepository extends TokenRepository +class OrganisationApprovalRepository extends BasicTokenRepository { /** * {@inheritdoc} */ protected $modelIdentifier = 'organisation_approval'; - /** * {@inheritdoc} */ - public function complete($token, UserInterface $approver, $params = []) + protected function updateTokenOwner($owner_id, $model, $args) { - // Hash the token for the stored version - $hash = hash($this->algorithm, $token); + $organisation = $this->classMapper->getClassMapping('organisation')::findUnique($owner_id, 'id', false); - // Find an unexpired, incomplete token for the specified hash - $model = $this->classMapper->getClassMapping($this->modelIdentifier)::query() - ->where('hash', $hash) - ->where('completed', false) - ->where(function($query) { - return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at'); - }) - ->first(); - - if ($model === null) { + if (!$organisation) { return false; } - // Fetch user for this token - $organisation = $this->classMapper->getClassMapping('organisation')::find($model->organisation_id); - $requester = $this->classMapper->getClassMapping('user')::find($model->requester_id); - - if (!$organisation || !$requester) { - return false; + // If specified, recored the approver. This assumes the model has an approver_id field which it may not... + if ($args['approver_id']) { + $model->approver_id = $args['approver_id']; } - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($model, $organisation, $requester, $approver, $params) { - $this->updateOrganisation($organisation, $requester, $approver, $params); - - $model->fill([ - 'completed' => true, - 'completed_at' => Carbon::now(), - ]); - - $model->approver_id = $approver->id; - - $model->save(); - }); - - return $model; - } - - /** - * Completes a token request without requiring the token (admin overrride) - */ - public function completeWithoutToken(OrganisationInterface $organisation, UserInterface $approver, $params = []) - { - $model = $this->classMapper->getClassMapping($this->modelIdentifier)::query() - ->where('organisation_id', $organisation->id) - ->where('completed', false) - ->first(); - - if ($model === null) { - return false; - } - - // Fetch user for this token - $requester = $this->classMapper->getClassMapping('user')::find($model->requester_id); - - if (!$requester) { - return false; - } - - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($model, $organisation, $requester, $approver, $params) { - $this->updateOrganisation($organisation, $requester, $approver, $params); - - $model->fill([ - 'completed' => true, - 'completed_at' => Carbon::now(), - ]); - - $model->approver_id = $approver->id; - - $model->save(); - }); - - return $model; - } - - /** - * Reverts a token request without requiring the token (admin overrride) - */ - public function revert(OrganisationInterface $organisation) - { - $model = $this->classMapper->getClassMapping($this->modelIdentifier)::query() - ->where('organisation_id', $organisation->id) - ->where('completed', true) - ->first(); - - if ($model === null) { - return false; - } - - // Begin transaction - DB will be rolled back if an exception occurs - Capsule::transaction(function () use ($model) { - $model->fill([ - 'completed' => false, - 'completed_at' => null, - 'approver_id' => null, - ]); - - $model->save(); - }); - - return $model; - } - - /** - * {@inheritdoc} - */ - public function create(OrganisationInterface $organisation, UserInterface $requester, $timeout) - { - // Remove any previous tokens for this organisation - $this->removeExisting($organisation); - - // Compute expiration time - $expiresAt = Carbon::now()->addSeconds($timeout); - - $model = $this->classMapper->createInstance($this->modelIdentifier); - - // Generate a random token - $model->setToken($this->generateRandomToken()); - - // Hash the password reset token for the stored version - $hash = hash($this->algorithm, $model->getToken()); - - $model->fill([ - 'organisation_id' => $organisation->id, - 'requester_id' => $requester->id, - 'hash' => $hash, - 'completed' => false, - 'expires_at' => ($timeout >= 0 ? $expiresAt : null), - ]); - - $model->save(); - - return $model; - } - - /** - * {@inheritdoc} - */ - public function exists(OrganisationInterface $organisation, UserInterface $requester = null, $token = null) - { - $model = $this->classMapper->getClassMapping($this->modelIdentifier)::query() - ->where('organisation_id', $organisation->id) - ->where('completed', false) - ->where(function($query) { - return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at'); - }); - - if ($token) { - // get token hash - $hash = hash($this->algorithm, $token); - $model->where('hash', $hash); - } - - if ($requester) { - $model->where('requester_id', $requester->id); - } - - return $model->first() ?: false; - } - - /** - * {@inheritdoc} - */ - protected function removeExisting(OrganisationInterface $organisation, UserInterface $requester = null) - { - $model = $this->classMapper->getClassMapping($this->modelIdentifier)::query() - ->where('organisation_id', $organisation->id); - - if ($requester) { - $model->where('requester_id', $requester->id); - } - - return $model->delete(); - } - - - /** - * {@inheritdoc} - */ - protected function updateOrganisation(OrganisationInterface $organisation, UserInterface $requester, UserInterface $approver, $args) - { if ($args['approved']) { + // Mark the organisation as approved $organisation->flag_approved = 1; $organisation->save(); + } else { + // Soft delete the organisation + $organisation->delete(false); } - } - /** - * Overridden - */ - protected function updateUser(UserInterface $user, $args) - { - return false; + return true; } } diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php index 4117729..f672e16 100644 --- a/src/ServicesProvider/ServicesProvider.php +++ b/src/ServicesProvider/ServicesProvider.php @@ -156,7 +156,7 @@ class ServicesProvider */ $container['organisation.beforeDelete'] = function ($c) { /* - * This method is invoked when an organisation is about to be merged + * This method is invoked when an organisation is about to be deleted * * Returns a callback that handles re-owning any organisation objects. * Throwing exceptions is allowed but not recommended. This method is triggered within a Capsule context. @@ -168,7 +168,7 @@ class ServicesProvider }; /* - * Repository for approval requests. + * Repository for organisation registration approval requests. * * @return \UserFrosting\Sprinkle\Organisations\Repository\OrganisationApprovalRepository */ @@ -176,7 +176,7 @@ class ServicesProvider $classMapper = $c->classMapper; $config = $c->config; - $repo = new OrganisationApprovalRepository($classMapper, $config['verification.algorithm']); + $repo = new OrganisationApprovalRepository($classMapper, $config['verification.algorithm'], $c['tokenLogger'], $config['debug.tokens']); return $repo; };