Organisation registration process implemented with configurable approval workflow

This commit is contained in:
2022-02-10 13:00:51 +00:00
parent fade1f8441
commit b64b4d72f9
23 changed files with 1453 additions and 7 deletions

View File

@@ -17,6 +17,9 @@ $(document).ready(function() {
// Bind creation button // Bind creation button
bindOrganisationCreationButton($("#widget-organisations")); bindOrganisationCreationButton($("#widget-organisations"));
// Bind registration button
bindOrganisationRegistrationButton($("#widget-organisations"));
// Bind table buttons // Bind table buttons
$("#widget-organisations").on("pagerComplete.ufTable", function () { $("#widget-organisations").on("pagerComplete.ufTable", function () {
bindOrganisationButtons($(this)); bindOrganisationButtons($(this));

View File

@@ -158,6 +158,31 @@ function bindOrganisationButtons(el, options) {
}); });
}); });
// Cancel a registration request
el.find('.js-organisation-cancelRegistration').click(function(e) {
e.preventDefault();
$("body").ufModal({
sourceUrl: site.uri.public + "/modals/organisations/confirm-cancel-registration",
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.delete_redirect) window.location.href = options.delete_redirect;
else window.location.reload();
});
});
});
// Delete organisation button // Delete organisation button
el.find('.js-organisation-delete').click(function(e) { el.find('.js-organisation-delete').click(function(e) {
e.preventDefault(); e.preventDefault();
@@ -197,3 +222,17 @@ function bindOrganisationCreationButton(el) {
attachOrganisationForm(); attachOrganisationForm();
}); });
}; };
function bindOrganisationRegistrationButton(el) {
// Link create button
el.find('.js-organisation-register').click(function(e) {
e.preventDefault();
$("body").ufModal({
sourceUrl: site.uri.public + "/modals/organisations/register",
msgTarget: $("#alerts-page")
});
attachOrganisationForm();
});
};

26
config/default.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
/*
* Configuration file for the UF Organisations
*
* Sensitive credentials should be stored in an environment variable or your .env file.
* Database password: DB_PASSWORD
* SMTP server password: SMTP_PASSWORD
*/
return [
'organisation' => [
'registration' => [
'require_approval' => true,
],
'approval' => [
'timeout' => -1,
],
],
];

View File

@@ -43,6 +43,14 @@ return [
'MEMBER_COUNT' => '# Members <sub>(excl admins)</sub>', 'MEMBER_COUNT' => '# Members <sub>(excl admins)</sub>',
'ADMIN_COUNT' => '# Admins', 'ADMIN_COUNT' => '# Admins',
'REGISTER' => 'Register organisation',
'REGISTRATION_SUCCESSFUL' => 'Successfully registered organisation <strong>{{name}}</strong>',
'CANCEL_REGISTRATION' => 'Cancel organisation registration',
'CANCEL_REGISTRATION_CONFIRM' => 'Are you sure you want to cancel the registration request for organisation <strong>{{name}}</strong>?',
'CANCEL_REGISTRATION_YES' => 'Yes, cancel organisation registration',
'CANCEL_REGISTRATION_SUCCESSFUL' => 'Successfully cancelled registration of organisation <strong>{{name}}</strong>',
'SELF' => 'My Organisations', 'SELF' => 'My Organisations',
'MANAGE' => 'Manage Organisations', 'MANAGE' => 'Manage Organisations',
@@ -57,6 +65,12 @@ return [
'SLUG' => [ 'SLUG' => [
'IN_USE' => 'Organisation slug <strong>{{slug}}</strong> is already in use.', 'IN_USE' => 'Organisation slug <strong>{{slug}}</strong> is already in use.',
], ],
'APPROVAL' => [
'PENDING' => 'This organisation is pending approval!',
'TOKEN_NOT_FOUND' => 'Approval token does not exist / Organisation is already approved/rejected.',
'APPROVED' => 'You have successfully approved the organisation <strong>{{name}}</strong>.',
'REJECTED' => 'You have successfully rejected the organisation <strong>{{name}}</strong>.',
],
], ],
'MEMBER' => [ 'MEMBER' => [
@@ -78,7 +92,9 @@ return [
'SLUG_NOT_IN_USE' => 'A <strong>{{slug}}</strong> slug does not exist', 'SLUG_NOT_IN_USE' => 'A <strong>{{slug}}</strong> slug does not exist',
'LEAVE_CANNOT_UNDONE' => 'This action cannot be undone.', 'LEAVE_CANNOT_UNDONE' => 'This action cannot be undone.',
'CANCEL_REGISTRATION' => 'Cancel registration request',
'APPROVED' => 'Approved', 'APPROVED' => 'Approved',
'PENDING' => 'Pending', 'PENDING' => 'Pending',
'APPROVE' => 'Approve',
'REJECT' => 'Reject',
]; ];

View File

@@ -0,0 +1,31 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
use UserFrosting\Sprinkle\Core\Util\NoCache;
/*
* Routes for organisation registration workflows.
*/
$app->group('/organisations', function () {
$this->get('/registration/approve', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:approveToken');
$this->get('/registration/reject', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:rejectToken');
$this->post('/o/{slug}/registration/approve', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:approve');
$this->post('/o/{slug}/registration/reject', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:reject');
})->add('authGuard')->add(new NoCache());
$app->group('/api/organisations', function () {
$this->post('/register', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:register');
$this->delete('/o/{slug}/registration', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:cancel');
})->add('authGuard')->add(new NoCache());
$app->group('/modals/organisations', function () {
$this->get('/register', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:getModalRegister');
$this->get('/registration/confirm-cancel', 'UserFrosting\Sprinkle\Organisations\Controller\OrganisationRegistrationController:getModalConfirmCancel');
})->add('authGuard')->add(new NoCache());

View File

@@ -47,4 +47,3 @@ $app->group('/modals/organisations', function () {
})->add('authGuard')->add(new NoCache()); })->add('authGuard')->add(new NoCache());
// TODO: add route for accepting members // TODO: add route for accepting members
// TODO: add route for verifying organisations

View File

@@ -0,0 +1,6 @@
---
token:
validators:
required:
label: validation token
message: VALIDATION.REQUIRED

View File

@@ -830,11 +830,13 @@ class OrganisationController extends SimpleController
$editButtons['hidden'][] = 'delete'; $editButtons['hidden'][] = 'delete';
} }
$canAccessOrganisations = $authorizer->checkAccess($currentUser, 'uri_organisations');
return $this->ci->view->render($response, 'pages/organisation.html.twig', [ return $this->ci->view->render($response, 'pages/organisation.html.twig', [
'organisation' => $organisation, 'organisation' => $organisation,
'fields' => $fields, 'fields' => $fields,
'tools' => $editButtons, 'tools' => $editButtons,
'delete_redirect' => $this->ci->router->pathFor('uri_organisations'), 'delete_redirect' => ($canAccessOrganisations ? $this->ci->router->pathFor('uri_organisations') : $this->ci->router->pathFor('dashboard')),
'leave_redirect' => $this->ci->router->pathFor('dashboard'), 'leave_redirect' => $this->ci->router->pathFor('dashboard'),
]); ]);
} }

View File

@@ -0,0 +1,738 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Organisations\Controller;
use Illuminate\Database\Capsule\Manager as Capsule;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
use UserFrosting\Fortress\RequestDataTransformer;
use UserFrosting\Fortress\RequestSchema;
use UserFrosting\Fortress\ServerSideValidator;
use UserFrosting\Sprinkle\Organisations\Database\Models\Organisation;
use UserFrosting\Sprinkle\Core\Controller\SimpleController;
use UserFrosting\Support\Exception\BadRequestException;
use UserFrosting\Support\Exception\ForbiddenException;
use UserFrosting\Support\Exception\NotFoundException;
use UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface;
use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationInterface;
use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage;
use UserFrosting\Sprinkle\Core\Mail\EmailRecipient;
/**
* Controller class for organisation registration-related requests, including registering, approving, rejecting, etc.
*
* @author Craig Williams (https://avsdev.uk)
*/
class OrganisationRegistrationController extends SimpleController
{
/**
* Processes the request to register a new organisation.
*
* Processes the request from the organisation registration form, checking that:
* 1. The organisation name and slug are not already in use;
* 2. The user has permission to register a new organisation;
* 3. The submitted data is valid.
* This route requires authentication.
*
* Request type: POST
*
* @see getModalCreateOrganisation
*
* @param Request $request
* @param Response $response
* @param array $args
*
* @throws ForbiddenException If user is not authorized to access page
*/
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;
// 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;
// Load the request schema
$schema = new RequestSchema('schema://requests/organisation/create.yaml');
// Whitelist and set parameter defaults
$transformer = new RequestDataTransformer($schema);
$data = $transformer->transform($params);
$error = false;
// Validate request data
$validator = new ServerSideValidator($schema, $this->ci->translator);
if (!$validator->validate($data)) {
$ms->addValidationErrors($validator);
$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);
$error = true;
}
if ($error) {
return $response->withJson([], 400);
}
$slugIncrement = 0;
$slugIncrementString = '';
while ($classMapper->getClassMapping('organisation')::findUnique($data['slug'] . $slugIncrementString, 'slug')) {
$slugIncrement++;
$slugIncrementString = '_' . $slugIncrement;
}
$data['slug'] .= $slugIncrementString;
$data['flag_approved'] = !$config['organisation.registration.require_approval'];
// 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) {
// 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]);
// Save members
$organisation->save();
// 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.approval.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);
});
return $response->withJson([], 200);
}
/**
* Processes the request to cancel an organisation registration request.
*
* Deletes the specified organisation (HARD!).
* 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).
*
* 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 cancel(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, 'register_organisation', [
'organisation' => $organisation,
])) {
throw new ForbiddenException();
}
if (!$authorizer->runCallback($currentUser, 'is_organisation_admin', $currentUser->id, $organisation->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) {
$organisation->delete(true);
unset($organisation);
// Create activity record
$this->ci->userActivityLogger->info("User {$currentUser->user_name} cancelled the request to register organisation {$organisationName}.", [
'type' => 'organisation_delete',
'user_id' => $currentUser->id,
]);
});
/** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
$ms = $this->ci->alerts;
$ms->addMessageTranslated('success', 'ORGANISATION.CANCEL_REGISTRATION_SUCCESSFUL', [
'name' => $organisationName,
]);
return $response->withJson([], 200);
}
/**
* Approves an organisation registration request.
*
* Processes the request from the organisation page, checking that:
* 1. The organisation exists;
* 2. The organisation is not already approved;
* This route requires authorization.
*
* AuthGuard: true
* Route: /organisations/o/{slug}/registration/approve
* Route Name: {none}
* Request type: GET
*
* @param Request $request
* @param Response $response
* @param array $args
*/
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;
$organisation = $this->getOrganisationFromParams($args);
// If the organisation doesn't exist, return 404
if (!$organisation) {
throw new NotFoundException();
}
// Access-controlled page
if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) {
throw new ForbiddenException();
}
$verification = $this->ci->repoOrganisationApproval->completeWithoutToken($organisation, $currentUser, ['approved' => true]);
$requester = $classMapper->getClassMapping('user')::find($verification->requester_id);
$this->sendApprovedEmail($organisation, $requester);
$ms->addMessageTranslated('success', 'ORGANISATION.APPROVAL.APPROVED', [
'name' => $organisation->name
]);
return $response->withRedirect($this->ci->router->pathFor('uri_organisation', ['slug' => $organisation->slug]));
}
/**
* Approves 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;
* 2. The organisation is not already approved;
* This route requires authorization.
*
* AuthGuard: true
* Route: /organisations/o/{slug}/approve
* Route Name: {none}
* Request type: GET
*
* @param Request $request
* @param Response $response
* @param array $args
*/
public function approveToken(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;
// Access-controlled page
if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) {
throw new ForbiddenException();
}
// GET parameters
$params = $request->getQueryParams();
// Load request schema
$schema = new RequestSchema('schema://requests/organisation/verify.yaml');
// Whitelist and set parameter defaults
$transformer = new RequestDataTransformer($schema);
$data = $transformer->transform($params);
// Validate, and halt on validation errors. This is a GET request, so we redirect on validation error.
$validator = new ServerSideValidator($schema, $this->ci->translator);
if (!$validator->validate($data)) {
$ms->addValidationErrors($validator);
return $response->withRedirect($this->ci->router->pathFor('dashboard'));
}
$verification = $this->ci->repoOrganisationApproval->complete($data['token'], $currentUser, ['approved' => true]);
if (!$verification) {
$ms->addMessageTranslated('danger', 'ORGANISATION.APPROVAL.TOKEN_NOT_FOUND');
return $response->withRedirect($this->ci->router->pathFor('dashboard'));
}
$organisation = $classMapper->getClassMapping('organisation')::find($verification->organisation_id);
$requester = $classMapper->getClassMapping('user')::find($verification->requester_id);
$this->sendApprovedEmail($organisation, $requester);
$ms->addMessageTranslated('success', 'ORGANISATION.APPROVAL.APPROVED', [
'name' => $organisation->name
]);
// Forward to login page
return $response->withRedirect($this->ci->router->pathFor('dashboard'));
}
/**
* Rejects 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;
* 2. The organisation is not already approved;
* This route requires authorization.
*
* AuthGuard: true
* Route: /organisations/o/{slug}/reject
* Route Name: {none}
* Request type: GET
*
* @param Request $request
* @param Response $response
* @param array $args
*/
public function reject(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;
$organisation = $this->getOrganisationFromParams($args);
// If the organisation doesn't exist, return 404
if (!$organisation) {
throw new NotFoundException();
}
// Access-controlled page
if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) {
throw new ForbiddenException();
}
$verification = $this->ci->repoOrganisationApproval->completeWithoutToken($organisation, $currentUser, ['approved' => false]);
$requester = $classMapper->getClassMapping('user')::find($verification->requester_id);
$this->sendRejectedEmail($organisation, $requester);
$organisation->delete();
$ms->addMessageTranslated('success', 'ORGANISATION.APPROVAL.REJECTED', [
'name' => $organisation->name
]);
return $response->withRedirect($this->ci->router->pathFor('uri_organisations'));
}
/**
* Rejects 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;
* 2. The organisation is not already approved;
* This route requires authorization.
*
* AuthGuard: true
* Route: /organisations/o/{slug}/reject
* Route Name: {none}
* Request type: GET
*
* @param Request $request
* @param Response $response
* @param array $args
*/
public function rejectToken(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;
$dashboardPage = $this->ci->router->pathFor('dashboard');
// Access-controlled page
if (!$authorizer->checkAccess($currentUser, 'approve_organisation')) {
throw new ForbiddenException();
}
// GET parameters
$params = $request->getQueryParams();
// Load request schema
$schema = new RequestSchema('schema://requests/organisation/verify.yaml');
// Whitelist and set parameter defaults
$transformer = new RequestDataTransformer($schema);
$data = $transformer->transform($params);
// Validate, and halt on validation errors. This is a GET request, so we redirect on validation error.
$validator = new ServerSideValidator($schema, $this->ci->translator);
if (!$validator->validate($data)) {
$ms->addValidationErrors($validator);
return $response->withRedirect($dashboardPage);
}
$verification = $this->ci->repoOrganisationApproval->complete($data['token'], $currentUser, ['approved' => false]);
if ($verification === false) {
$ms->addMessageTranslated('danger', 'ORGANISATION.APPROVAL.TOKEN_NOT_FOUND');
return $response->withRedirect($dashboardPage);
}
$organisation = $classMapper->getClassMapping('organisation')::find($verification->organisation_id);
$requester = $classMapper->getClassMapping('user')::find($verification->requester_id);
$this->sendRejectedEmail($organisation, $requester);
$ms->addMessageTranslated('success', 'ORGANISATION.APPROVAL.REJECTED', [
'name' => $organisation->name
]);
// Forward to login page
return $response->withRedirect($dashboardPage);
}
/**
* Renders the modal form for registering a new organisation.
*
* This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
* This page requires authentication.
*
* Request type: GET
*
* @param Request $request
* @param Response $response
* @param array $args
*
* @throws ForbiddenException If user is not authorized to access page
*/
public function getModalRegister(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;
// 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', []);
$fieldNames = ['name', 'slug', 'description'];
$fields = [
'hidden' => ['slug'],
'disabled' => [],
];
// Load validation rules
$schema = new RequestSchema('schema://requests/organisation/create.yaml');
$validator = new JqueryValidationAdapter($schema, $this->ci->translator);
return $this->ci->view->render($response, 'modals/organisation.html.twig', [
'organisation' => $organisation,
'form' => [
'action' => 'api/organisations/register',
'method' => 'POST',
'fields' => $fields,
'submit_text' => $translator->translate('REGISTER'),
],
'page' => [
'validators' => $validator->rules('json', false),
],
]);
}
/**
* Get cancel registration 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 getModalConfirmCancel(Request $request, Response $response, $args)
{
// GET parameters
$params = $request->getQueryParams();
$organisation = $this->getOrganisationFromParams($params);
// 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, 'register_organisation', [
'organisation' => $organisation,
])) {
throw new ForbiddenException();
}
// Access-controlled page
if (!$authorizer->runCallback($currentUser, 'is_organisation_admin', $currentUser->id, $organisation->id)) {
throw new ForbiddenException();
}
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = $this->ci->classMapper;
return $this->ci->view->render($response, 'modals/confirm-cancel-organisation-registration.html.twig', [
'organisation' => $organisation,
'form' => [
'action' => "api/organisations/o/{$organisation->slug}/registration",
],
]);
}
/**
* Send approval email for specified organisation and confirmation to user.
*
* @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)
{
$timeout = $this->ci->config['organisation.approval.timeout'];
// Create and send approval email
$message = new TwigMailMessage($this->ci->view, 'mail/organisation-approval-request.html.twig');
$message->from($this->ci->config['address_book.admin'])
->addParams([
'requester' => $requester,
'organisation' => $organisation,
'token' => $token,
'approval_expiration' => ($timeout > 0 ? floor($timeout / 86400) . ' days' : false),
]);
$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();
}
$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();
}
}
/**
* Send approved email for specified organisation.
*
* @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)
{
$message = new TwigMailMessage($this->ci->view, 'mail/organisation-approval-approved.html.twig');
$message->from($this->ci->config['address_book.admin'])
->addParams([
'recipient' => $requester,
'organisation' => $organisation
]);
$message->addEmailRecipient(new EmailRecipient($requester->email, $requester->full_name));
$this->ci->mailer->send($message);
}
/**
* Send rejected email for specified organisation.
*
* @param UserInterface $requester The user to send the rejection notice to
* @param UserInterface $organisation The organisation to send the email for
*/
protected function sendRejectedEmail(OrganisationInterface $organisation, UserInterface $requester)
{
$message = new TwigMailMessage($this->ci->view, 'mail/organisation-approval-rejected.html.twig');
$message->from($this->ci->config['address_book.admin'])
->addParams([
'recipient' => $requester,
'organisation' => $organisation
]);
$message->addEmailRecipient(new EmailRecipient($requester->email, $requester->full_name));
$this->ci->mailer->send($message);
}
/**
* Get organisation from params.
*
* @param array $params
*
* @throws BadRequestException
*
* @return Organisation
*/
protected function getOrganisationFromParams($params)
{
// Load the request schema
$schema = new RequestSchema('schema://requests/organisation/get-by-slug.yaml');
// Whitelist and set parameter defaults
$transformer = new RequestDataTransformer($schema);
$data = $transformer->transform($params);
// Validate, and throw exception on validation errors.
$validator = new ServerSideValidator($schema, $this->ci->translator);
if (!$validator->validate($data)) {
// TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException
$e = new BadRequestException();
foreach ($validator->errors() as $idx => $field) {
foreach ($field as $eidx => $error) {
$e->addUserMessage($error);
}
}
throw $e;
}
/** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
$classMapper = $this->ci->classMapper;
// Get the organisation
$organisation = $classMapper->getClassMapping('organisation')::where('slug', $data['slug'])
->first();
return $organisation;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Organisations\Database\Migrations\v001;
use Illuminate\Database\Schema\Blueprint;
use UserFrosting\Sprinkle\Core\Database\Migration;
/**
* Organisation Approvals table migration
* Manages requests for organisation approvals.
* Version 1.0.0.
*
* @author Craig Williams (https://avsdev.uk)
*/
class OrganisationApprovalsTable extends Migration
{
/**
* {@inheritdoc}
*/
public function up()
{
if (!$this->schema->hasTable('organisation_approvals')) {
$this->schema->create('organisation_approvals', function (Blueprint $table) {
$table->increments('id');
$table->integer('requester_id')->unsigned();
$table->integer('organisation_id')->unsigned();
$table->string('hash');
$table->boolean('completed')->default(0);
$table->timestamp('expires_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->integer('approver_id')->unsigned();
$table->timestamps();
$table->engine = 'InnoDB';
$table->collation = 'utf8_unicode_ci';
$table->charset = 'utf8';
$table->foreign('requester_id')->references('id')->on('users');
$table->foreign('approver_id')->references('id')->on('users');
$table->foreign('organisation_id')->references('id')->on('organisations');
$table->index('requester_id');
$table->index('approver_id');
$table->index('hash');
});
}
}
/**
* {@inheritdoc}
*/
public function down()
{
$this->schema->drop('organisation_approvals');
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Organisations\Database\Migrations\v002;
use Illuminate\Database\Schema\Blueprint;
use UserFrosting\Sprinkle\Core\Database\Migration;
/**
* Organisations approvals table migration
* Make the approver_id nullable
* Version 1.0.0.
*
* @author Craig Williams (https://avsdev.uk)
*/
class UpdateOrganisationApprovalsTable extends Migration
{
/**
* {@inheritdoc}
*/
public static $dependencies = [
'\UserFrosting\Sprinkle\Organisations\Database\Migrations\v001\OrganisationApprovalsTable',
];
/**
* {@inheritdoc}
*/
public function up()
{
if ($this->schema->hasTable('organisation_approvals')) {
$this->schema->table('organisation_approvals', function (Blueprint $table) {
$table->integer('approver_id')->unsigned()->nullable()->change();
});
}
}
/**
* {@inheritdoc}
*/
public function down()
{
$this->schema->table('organisations', function (Blueprint $table) {
$table->integer('approver_id')->unsigned()->change();
});
}
}

View File

@@ -151,6 +151,9 @@ class Organisation extends Model implements OrganisationInterface
if ($hardDelete) { if ($hardDelete) {
static::$ci->get('organisation.beforeDelete')($this); static::$ci->get('organisation.beforeDelete')($this);
// Remove all organisation tokens
$classMapper->getClassMapping('organisation_approval')::where('organisation_id', $this->id)->delete();
// Remove all member associations // Remove all member associations
$this->members()->detach(); $this->members()->detach();

View File

@@ -0,0 +1,116 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Organisations\Database\Models;
use Illuminate\Database\Capsule\Manager as DB;
use UserFrosting\Sprinkle\Core\Database\Models\Model;
/**
* Organisation Approval Class.
*
* Represents a pending organisation approval request.
*
* @author Craig Williams (https://avsdev.uk)
*
* @property int $requester_id
* @property int $organisation_id
* @property hash $token
* @property bool $completed
* @property datetime $expires_at
* @property datetime $completed_at
* @property int $approver_id
*/
class OrganisationApproval extends Model
{
/**
* @var string The name of the table for the current model.
*/
protected $table = 'organisation_approvals';
protected $fillable = [
'requester_id',
'organisation_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 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 rejection 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');
}
}

View File

@@ -51,6 +51,12 @@ class OrganisationPermissions extends BaseSeed
'conditions' => 'always()', 'conditions' => 'always()',
'description' => 'Create a new organisation.', 'description' => 'Create a new organisation.',
]), ]),
'register_organisation' => new Permission([
'slug' => 'register_organisation',
'name' => 'Register organisation',
'conditions' => 'always()',
'description' => 'Register a new organisation. May optionally require approval.',
]),
'view_organisation_field' => new Permission([ 'view_organisation_field' => new Permission([
'slug' => 'view_organisation_field', 'slug' => 'view_organisation_field',
'name' => 'View organisation', 'name' => 'View organisation',
@@ -69,6 +75,12 @@ class OrganisationPermissions extends BaseSeed
'conditions' => 'always()', 'conditions' => 'always()',
'description' => 'Edit basic properties of any organisation.', 'description' => 'Edit basic properties of any organisation.',
]), ]),
'approve_organisation' => new Permission([
'slug' => 'approve_organisation',
'name' => 'Approve/Reject organisation',
'conditions' => 'always()',
'description' => 'Approve/Reject organisation registation request.',
]),
'merge_organisations' => new Permission([ 'merge_organisations' => new Permission([
'slug' => 'merge_organisations', 'slug' => 'merge_organisations',
'name' => 'Merge two organisations', 'name' => 'Merge two organisations',
@@ -143,6 +155,7 @@ class OrganisationPermissions extends BaseSeed
$permissions['create_organisation']->id, $permissions['create_organisation']->id,
$permissions['view_organisation_field']->id, $permissions['view_organisation_field']->id,
$permissions['update_organisation_field']->id, $permissions['update_organisation_field']->id,
$permissions['approve_organisation']->id,
$permissions['merge_organisations']->id, $permissions['merge_organisations']->id,
$permissions['delete_organisation']->id, $permissions['delete_organisation']->id,
$permissions['uri_organisations']->id, $permissions['uri_organisations']->id,
@@ -157,6 +170,7 @@ class OrganisationPermissions extends BaseSeed
$permissions['create_organisation']->id, $permissions['create_organisation']->id,
$permissions['view_organisation_field']->id, $permissions['view_organisation_field']->id,
$permissions['update_organisation_field']->id, $permissions['update_organisation_field']->id,
$permissions['approve_organisation']->id,
$permissions['merge_organisations']->id, $permissions['merge_organisations']->id,
$permissions['delete_organisation']->id, $permissions['delete_organisation']->id,
$permissions['uri_organisations']->id, $permissions['uri_organisations']->id,
@@ -170,6 +184,7 @@ class OrganisationPermissions extends BaseSeed
$permissions['uri_organisation_own']->id, $permissions['uri_organisation_own']->id,
$permissions['view_organisation_field_own']->id, $permissions['view_organisation_field_own']->id,
$permissions['leave_organisation']->id, $permissions['leave_organisation']->id,
$permissions['register_organisation']->id,
]); ]);
} }
} }

View File

@@ -0,0 +1,205 @@
<?php
/*
* AVSDev UF Organisations (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
*/
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;
/**
* Token repository class for new organisation approval.
*
* @author Craig Williams (https://avsdev.uk)
*/
class OrganisationApprovalRepository extends TokenRepository
{
/**
* {@inheritdoc}
*/
protected $modelIdentifier = 'organisation_approval';
/**
* {@inheritdoc}
*/
public function complete($token, UserInterface $approver, $params = [])
{
// Hash the token for the stored version
$hash = hash($this->algorithm, $token);
// Find an unexpired, incomplete token for the specified hash
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::where('hash', $hash)
->where('completed', false)
->where(function($query) {
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
})
->first();
if ($model === null) {
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;
}
// 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)::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;
}
/**
* {@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([
'hash' => $hash,
'completed' => false,
'expires_at' => ($timeout >= 0 ? $expiresAt : null),
]);
$model->organisation_id = $organisation->id;
$model->requester_id = $requester->id;
$model->save();
return $model;
}
/**
* {@inheritdoc}
*/
public function exists(OrganisationInterface $organisation, UserInterface $requester = null, $token = null)
{
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::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)::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']) {
$organisation->flag_approved = 1;
$organisation->save();
}
}
/**
* Overridden
*/
protected function updateUser(UserInterface $user, $args)
{
return false;
}
}

View File

@@ -15,6 +15,7 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationInterface; use UserFrosting\Sprinkle\Organisations\Database\Models\Interfaces\OrganisationInterface;
use UserFrosting\Sprinkle\Organisations\Twig\OrganisationsExtension; use UserFrosting\Sprinkle\Organisations\Twig\OrganisationsExtension;
use UserFrosting\Sprinkle\Organisations\Repository\OrganisationApprovalRepository;
use UserFrosting\Sprinkle\Organisations\Authorize\AuthorizationManager; use UserFrosting\Sprinkle\Organisations\Authorize\AuthorizationManager;
@@ -41,6 +42,7 @@ class ServicesProvider
*/ */
$container->extend('classMapper', function ($classMapper, $c) { $container->extend('classMapper', function ($classMapper, $c) {
$classMapper->setClassMapping('organisation', 'UserFrosting\Sprinkle\Organisations\Database\Models\Organisation'); $classMapper->setClassMapping('organisation', 'UserFrosting\Sprinkle\Organisations\Database\Models\Organisation');
$classMapper->setClassMapping('organisation_approval', 'UserFrosting\Sprinkle\Organisations\Database\Models\OrganisationApproval');
$classMapper->setClassMapping('organisation_sprunje', 'UserFrosting\Sprinkle\Organisations\Sprunje\OrganisationSprunje'); $classMapper->setClassMapping('organisation_sprunje', 'UserFrosting\Sprinkle\Organisations\Sprunje\OrganisationSprunje');
$classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Organisations\Database\Models\User'); $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Organisations\Database\Models\User');
$classMapper->setClassMapping('user_sprunje', 'UserFrosting\Sprinkle\Organisations\Sprunje\UserSprunje'); $classMapper->setClassMapping('user_sprunje', 'UserFrosting\Sprinkle\Organisations\Sprunje\UserSprunje');
@@ -138,5 +140,19 @@ class ServicesProvider
}; };
}; };
/*
* Repository for approval requests.
*
* @return \UserFrosting\Sprinkle\Organisations\Repository\OrganisationApprovalRepository
*/
$container['repoOrganisationApproval'] = function ($c) {
$classMapper = $c->classMapper;
$config = $c->config;
$repo = new OrganisationApprovalRepository($classMapper, $config['verification.algorithm']);
return $repo;
};
} }
} }

View File

@@ -32,6 +32,8 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
<input type="hidden" name="slug" autocomplete="off" value="{{organisation.slug}}">
{% endif %} {% endif %}
{% if 'description' not in form.fields.hidden %} {% if 'description' not in form.fields.hidden %}
<div class="col-sm-12"> <div class="col-sm-12">

View File

@@ -0,0 +1,16 @@
{% block subject %}
{{site.title}} - organisation registration approved
{% endblock %}
{% block body %}
<p>
Dear {{recipient.first_name}},
</p>
<p>
The organisation registration request you submitted at {{site.title}} ({{site.uri.public}}) has been approved.
</p>
<p>
With regards,<br>
The {{site.title}} Team
</p>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% block subject %}
{{site.title}} - organisation registration rejected
{% endblock %}
{% block body %}
<p>
Dear {{recipient.first_name}},
</p>
<p>
The organisation registration request you submitted at {{site.title}} ({{site.uri.public}}) has been rejected.
</p>
<p>
With regards,<br>
The {{site.title}} Team
</p>
{% endblock %}

View File

@@ -0,0 +1,46 @@
{% block subject %}
{{site.title}} - new organisation requires approval
{% endblock %}
{% block body %}
<p>
Dear {{recipient.first_name}},
</p>
<p>
Someone has created a new organisation which requires approving at {{site.title}} ({{site.uri.public}}).
</p>
<p>
The organisation details are:
<dl>
<dt>Requester name:</dt>
<dd>{{requester.full_name}}</dd>
<dt>Requester email:</dt>
<dd>{{requester.email}}</dd>
<dt>Organisation name:</dt>
<dd>{{organisation.name}}</dd>
<dt>Organisation description:</dt>
<dd>{{organisation.description}}</dd>
</p>
<p>
You may verify or reject this organisation via the control dashboard (<a href="{{site.uri.public}}/organisations">{{site.uri.public}}/organisations</a>).
</p>
<p>
To verify this organisation immediately you may do so by visiting: <a href="{{site.uri.public}}/organisations/approve?token={{token}}">{{site.uri.public}}/organisations/approve?token={{token}}</a>.
</p>
<p>
To reject this organisation immediately you may do so by visiting: <a href="{{site.uri.public}}/organisations/reject?token={{token}}">{{site.uri.public}}/organisations/reject?token={{token}}</a>.
</p>
{% if approval_expiration %}
<p>
The approcal period for this organisation will expire in {{approval_expiration}} at which point the organisation will be automatically rejected.
</p>
{% endif %}
<p>
With regards,<br>
The {{site.title}} Team
</p>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends "modals/modal.html.twig" %}
{% block modal_title %}{{translate("ORGANISATION.CANCEL_REGISTRATION")}}{% endblock %}
{% block modal_body %}
<form class="js-form" method="delete" action="{{site.uri.public}}/{{form.action}}">
{% include "forms/csrf.html.twig" %}
<div class="js-form-alerts">
</div>
<h4>{{translate("ORGANISATION.CANCEL_REGISTRATION_CONFIRM", {name: organisation.name})}}{% if delete_hard %}<br><small>{{translate("DELETE_CANNOT_UNDONE")}}</small>{% endif %}</h4>
<br>
<div class="btn-group-action">
<button type="submit" class="btn btn-danger btn-lg btn-block">{{translate("ORGANISATION.CANCEL_REGISTRATION_YES")}}</button>
<button type="button" class="btn btn-default btn-lg btn-block" data-dismiss="modal">{{translate("CANCEL")}}</button>
</div>
</form>
{% endblock %}

View File

@@ -50,6 +50,23 @@
{{organisation.description}} {{organisation.description}}
</p> </p>
{% endif %} {% endif %}
{% if organisation.flag_approved != 1 %}
<hr>
<h4 class="text-danger text-center">
{{ translate('ORGANISATION.APPROVAL.PENDING') }}
</h4>
<div class="text-center">
{% if checkAccess('approve_organisation') %}
<form method="POST">
{% include "forms/csrf.html.twig" %}
<button type="submit" class="btn btn-success" formaction="{{site.uri.public}}/organisations/o/{{organisation.slug}}/registration/approve">{{translate('APPROVE')}}</button>
<button type="submit" class="btn btn-danger" formaction="{{site.uri.public}}/organisations/o/{{organisation.slug}}/registration/reject">{{translate('REJECT')}}</button>
</form>
{% elseif isOrganisationAdmin(organisation) %}
<button type="button" class="btn btn-danger js-organisation-cancelRegistration" data-slug="{{organisation.slug}}">{{translate('CANCEL_REGISTRATION')}}</button>
{% endif %}
</div>
{% endif %}
{% if 'members' not in fields.hidden %} {% if 'members' not in fields.hidden %}
<hr> <hr>
<strong><i class="fas fa-wrench margin-r-5"></i> {{ translate('ADMIN', 2)}}</strong> <strong><i class="fas fa-wrench margin-r-5"></i> {{ translate('ADMIN', 2)}}</strong>

View File

@@ -26,13 +26,18 @@
} }
%} %}
</div> </div>
{% if checkAccess('create_organisation') %}
<div class="box-footer"> <div class="box-footer">
{% if checkAccess('create_organisation') %}
<button type="button" class="btn btn-success js-organisation-create"> <button type="button" class="btn btn-success js-organisation-create">
<i class="fas fa-plus-square"></i> {{translate("ORGANISATION.CREATE")}} <i class="fas fa-plus-square"></i> {{translate("ORGANISATION.CREATE")}}
</button> </button>
</div>
{% endif %} {% endif %}
{% if checkAccess('register_organisation') %}
<button type="button" class="btn btn-success js-organisation-register">
<i class="fas fa-plus-square"></i> {{translate("ORGANISATION.REGISTER")}}
</button>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>