aboutsummaryrefslogtreecommitdiffhomepage
path: root/main/app/sprinkles/admin/src
diff options
context:
space:
mode:
Diffstat (limited to 'main/app/sprinkles/admin/src')
-rwxr-xr-xmain/app/sprinkles/admin/src/Admin.php20
-rwxr-xr-xmain/app/sprinkles/admin/src/Controller/ActivityController.php85
-rwxr-xr-xmain/app/sprinkles/admin/src/Controller/AdminController.php150
-rwxr-xr-xmain/app/sprinkles/admin/src/Controller/GroupController.php725
-rwxr-xr-xmain/app/sprinkles/admin/src/Controller/PermissionController.php206
-rwxr-xr-xmain/app/sprinkles/admin/src/Controller/RoleController.php930
-rwxr-xr-xmain/app/sprinkles/admin/src/Controller/UserController.php1261
-rwxr-xr-xmain/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php84
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php80
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/GroupSprunje.php42
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php93
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php48
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/RoleSprunje.php67
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php48
-rwxr-xr-xmain/app/sprinkles/admin/src/Sprunje/UserSprunje.php185
15 files changed, 4024 insertions, 0 deletions
diff --git a/main/app/sprinkles/admin/src/Admin.php b/main/app/sprinkles/admin/src/Admin.php
new file mode 100755
index 0000000..8a6dcc1
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Admin.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin;
+
+use UserFrosting\System\Sprinkle\Sprinkle;
+
+/**
+ * Bootstrapper class for the 'admin' sprinkle.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class Admin extends Sprinkle
+{
+
+}
diff --git a/main/app/sprinkles/admin/src/Controller/ActivityController.php b/main/app/sprinkles/admin/src/Controller/ActivityController.php
new file mode 100755
index 0000000..2fbe0d9
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Controller/ActivityController.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Controller;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\NotFoundException;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\ForbiddenException;
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Controller class for activity-related requests.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class ActivityController extends SimpleController
+{
+ /**
+ * Returns a list of Activities
+ *
+ * Generates a list of activities, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_activities')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) {
+ return $query->with('user');
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders the activity listing page.
+ *
+ * This page renders a table of user activities.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_activities')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/activities.html.twig');
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Controller/AdminController.php b/main/app/sprinkles/admin/src/Controller/AdminController.php
new file mode 100755
index 0000000..da4da8a
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Controller/AdminController.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Controller;
+
+use Carbon\Carbon;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Account\Database\Models\Group;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Account\Database\Models\Role;
+use UserFrosting\Sprinkle\Core\Database\Models\Version;
+use UserFrosting\Sprinkle\Core\Util\EnvironmentInfo;
+use UserFrosting\Support\Exception\ForbiddenException;
+
+/**
+ * AdminController Class
+ *
+ * Controller class for /dashboard URL. Handles admin-related activities
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AdminController extends SimpleController
+{
+
+ /**
+ * Renders the admin panel dashboard
+ *
+ */
+ public function pageDashboard($request, $response, $args)
+ {
+ //** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_dashboard')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Probably a better way to do this
+ $users = $classMapper->staticMethod('user', 'orderBy', 'created_at', 'desc')
+ ->take(8)
+ ->get();
+
+ // Transform the `create_at` date in "x days ago" type of string
+ $users->transform(function ($item, $key) {
+ $item->registered = Carbon::parse($item->created_at)->diffForHumans();
+ return $item;
+ });
+
+ /** @var Config $config */
+ $config = $this->ci->config;
+
+ /** @var Config $config */
+ $cache = $this->ci->cache;
+
+ // Get each sprinkle db version
+ $sprinkles = $this->ci->sprinkleManager->getSprinkleNames();
+
+ return $this->ci->view->render($response, 'pages/dashboard.html.twig', [
+ 'counter' => [
+ 'users' => $classMapper->staticMethod('user', 'count'),
+ 'roles' => $classMapper->staticMethod('role', 'count'),
+ 'groups' => $classMapper->staticMethod('group', 'count')
+ ],
+ 'info' => [
+ 'version' => [
+ 'UF' => \UserFrosting\VERSION,
+ 'php' => phpversion(),
+ 'database' => EnvironmentInfo::database()
+ ],
+ 'database' => [
+ 'name' => $config['db.default.database']
+ ],
+ 'environment' => $this->ci->environment,
+ 'path' => [
+ 'project' => \UserFrosting\ROOT_DIR
+ ]
+ ],
+ 'sprinkles' => $sprinkles,
+ 'users' => $users
+ ]);
+ }
+
+ /**
+ * Clear the site cache.
+ *
+ * This route requires authentication.
+ * Request type: POST
+ */
+ public function clearCache($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'clear_cache')) {
+ throw new ForbiddenException();
+ }
+
+ // Flush cache
+ $this->ci->cache->flush();
+
+ /** @var MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'CACHE.CLEARED');
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Renders the modal form to confirm cache deletion.
+ *
+ * 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
+ */
+ public function getModalConfirmClearCache($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'clear_cache')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-clear-cache.html.twig', [
+ 'form' => [
+ 'action' => 'api/dashboard/clear-cache',
+ ]
+ ]);
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Controller/GroupController.php b/main/app/sprinkles/admin/src/Controller/GroupController.php
new file mode 100755
index 0000000..7ca94b1
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Controller/GroupController.php
@@ -0,0 +1,725 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Controller;
+
+use Carbon\Carbon;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\NotFoundException;
+use UserFrosting\Fortress\RequestDataTransformer;
+use UserFrosting\Fortress\RequestSchema;
+use UserFrosting\Fortress\ServerSideValidator;
+use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
+use UserFrosting\Sprinkle\Account\Database\Models\Group;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\ForbiddenException;
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Controller class for group-related requests, including listing groups, CRUD for groups, etc.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class GroupController extends SimpleController
+{
+ /**
+ * Processes the request to create a new group.
+ *
+ * Processes the request from the group creation form, checking that:
+ * 1. The group name and slug are not already in use;
+ * 2. The user has permission to create a new group;
+ * 3. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: POST
+ * @see getModalCreateGroup
+ */
+ public function create($request, $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\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_group')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/group/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->staticMethod('group', 'where', 'name', $data['name'])->first()) {
+ $ms->addMessageTranslated('danger', 'GROUP.NAME.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($classMapper->staticMethod('group', 'where', 'slug', $data['slug'])->first()) {
+ $ms->addMessageTranslated('danger', 'GROUP.SLUG.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // All checks passed! log events/activities and create group
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) {
+ // Create the group
+ $group = $classMapper->createInstance('group', $data);
+
+ // Store new group to database
+ $group->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} created group {$group->name}.", [
+ 'type' => 'group_create',
+ 'user_id' => $currentUser->id
+ ]);
+
+ $ms->addMessageTranslated('success', 'GROUP.CREATION_SUCCESSFUL', $data);
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to delete an existing group.
+ *
+ * Deletes the specified group.
+ * Before doing so, checks that:
+ * 1. The user has permission to delete this group;
+ * 2. The group is not currently set as the default for new users;
+ * 3. The group is empty (does not have any users);
+ * 4. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: DELETE
+ */
+ public function delete($request, $response, $args)
+ {
+ $group = $this->getGroupFromParams($args);
+
+ // If the group doesn't exist, return 404
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_group', [
+ 'group' => $group
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Check that we are not deleting the default group
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if ($group->slug == $config['site.registration.user_defaults.group']) {
+ $e = new BadRequestException();
+ $e->addUserMessage('GROUP.DELETE_DEFAULT', $group->toArray());
+ throw $e;
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if there are any users in this group
+ $countGroupUsers = $classMapper->staticMethod('user', 'where', 'group_id', $group->id)->count();
+ if ($countGroupUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('GROUP.NOT_EMPTY', $group->toArray());
+ throw $e;
+ }
+
+ $groupName = $group->name;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($group, $groupName, $currentUser) {
+ $group->delete();
+ unset($group);
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted group {$groupName}.", [
+ 'type' => 'group_delete',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'GROUP.DELETION_SUCCESSFUL', [
+ 'name' => $groupName
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns info for a single group.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_groups')) {
+ throw new ForbiddenException();
+ }
+
+ $slug = $args['slug'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $group = $classMapper->staticMethod('group', 'where', 'slug', $slug)->first();
+
+ // If the group doesn't exist, return 404
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get group
+ $result = $group->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);
+ }
+
+ /**
+ * Returns a list of Groups
+ *
+ * Generates a list of groups, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_groups')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('group_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ public function getModalConfirmDelete($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $group = $this->getGroupFromParams($params);
+
+ // If the group no longer exists, forward to main group listing page
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_group', [
+ 'group' => $group
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if there are any users in this group
+ $countGroupUsers = $classMapper->staticMethod('user', 'where', 'group_id', $group->id)->count();
+ if ($countGroupUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('GROUP.NOT_EMPTY', $group->toArray());
+ throw $e;
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-delete-group.html.twig', [
+ 'group' => $group,
+ 'form' => [
+ 'action' => "api/groups/g/{$group->slug}",
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for creating a new group.
+ *
+ * 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
+ */
+ public function getModalCreate($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_group')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Create a dummy group to prepopulate fields
+ $group = $classMapper->createInstance('group', []);
+
+ $group->icon = 'fa fa-user';
+
+ $fieldNames = ['name', 'slug', 'icon', 'description'];
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/group/create.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/group.html.twig', [
+ 'group' => $group,
+ 'form' => [
+ 'action' => 'api/groups',
+ 'method' => 'POST',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('CREATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing an existing group.
+ *
+ * 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
+ */
+ public function getModalEdit($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $group = $this->getGroupFromParams($params);
+
+ // If the group doesn't exist, return 404
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @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\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "icon", "description" for this group
+ $fieldNames = ['name', 'slug', 'icon', 'description'];
+ if (!$authorizer->checkAccess($currentUser, 'update_group_field', [
+ 'group' => $group,
+ 'fields' => $fieldNames
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Generate form
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/group/edit-info.yaml');
+ $validator = new JqueryValidationAdapter($schema, $translator);
+
+ return $this->ci->view->render($response, 'modals/group.html.twig', [
+ 'group' => $group,
+ 'form' => [
+ 'action' => "api/groups/g/{$group->slug}",
+ 'method' => 'PUT',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('UPDATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ public function getUsers($request, $response, $args)
+ {
+ $group = $this->getGroupFromParams($args);
+
+ // If the group no longer exists, forward to main group listing page
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_group_field', [
+ 'group' => $group,
+ 'property' => 'users'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($group) {
+ return $query->where('group_id', $group->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders a page displaying a group's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view the requested group's info.
+ * It checks each field individually, showing only those that you have permission to view.
+ * This will also try to show buttons for deleting, and editing the group.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args)
+ {
+ $group = $this->getGroupFromParams($args);
+
+ // If the group no longer exists, forward to main group listing page
+ if (!$group) {
+ $redirectPage = $this->ci->router->pathFor('uri_groups');
+ return $response->withRedirect($redirectPage, 404);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_group', [
+ 'group' => $group
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Determine fields that currentUser is authorized to view
+ $fieldNames = ['name', 'slug', 'icon', 'description'];
+
+ // Generate form
+ $fields = [
+ 'hidden' => []
+ ];
+
+ foreach ($fieldNames as $field) {
+ if (!$authorizer->checkAccess($currentUser, 'view_group_field', [
+ 'group' => $group,
+ 'property' => $field
+ ])) {
+ $fields['hidden'][] = $field;
+ }
+ }
+
+ // Determine buttons to display
+ $editButtons = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'update_group_field', [
+ 'group' => $group,
+ 'fields' => ['name', 'slug', 'icon', 'description']
+ ])) {
+ $editButtons['hidden'][] = 'edit';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'delete_group', [
+ 'group' => $group
+ ])) {
+ $editButtons['hidden'][] = 'delete';
+ }
+
+ return $this->ci->view->render($response, 'pages/group.html.twig', [
+ 'group' => $group,
+ 'fields' => $fields,
+ 'tools' => $editButtons
+ ]);
+ }
+
+ /**
+ * Renders the group listing page.
+ *
+ * This page renders a table of groups, with dropdown menus for admin actions for each group.
+ * Actions typically include: edit group, delete group.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_groups')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/groups.html.twig');
+ }
+
+ /**
+ * Processes the request to update an existing group's details.
+ *
+ * Processes the request from the group update form, checking that:
+ * 1. The group name/slug are not already in use;
+ * 2. The user has the necessary permissions to update the posted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: PUT
+ * @see getModalGroupEdit
+ */
+ public function updateInfo($request, $response, $args)
+ {
+ // Get the group based on slug in URL
+ $group = $this->getGroupFromParams($args);
+
+ if (!$group) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters: (name, slug, icon, description)
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/group/edit-info.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;
+ }
+
+ // Determine targeted fields
+ $fieldNames = [];
+ foreach ($data as $name => $value) {
+ $fieldNames[] = $name;
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit submitted fields for this group
+ if (!$authorizer->checkAccess($currentUser, 'update_group_field', [
+ 'group' => $group,
+ 'fields' => array_values(array_unique($fieldNames))
+ ])) {
+ 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'] != $group->name &&
+ $classMapper->staticMethod('group', 'where', 'name', $data['name'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'GROUP.NAME.IN_USE', $data);
+ $error = true;
+ }
+
+ if (
+ isset($data['slug']) &&
+ $data['slug'] != $group->slug &&
+ $classMapper->staticMethod('group', 'where', 'slug', $data['slug'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'GROUP.SLUG.IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($data, $group, $currentUser) {
+ // Update the group and generate success messages
+ foreach ($data as $name => $value) {
+ if ($value != $group->$name) {
+ $group->$name = $value;
+ }
+ }
+
+ $group->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for group {$group->name}.", [
+ 'type' => 'group_update_info',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ $ms->addMessageTranslated('success', 'GROUP.UPDATE', [
+ 'name' => $group->name
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ protected function getGroupFromParams($params)
+ {
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/group/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 group
+ $group = $classMapper->staticMethod('group', 'where', 'slug', $data['slug'])
+ ->first();
+
+ return $group;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Controller/PermissionController.php b/main/app/sprinkles/admin/src/Controller/PermissionController.php
new file mode 100755
index 0000000..f3e93ce
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Controller/PermissionController.php
@@ -0,0 +1,206 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Controller;
+
+use Carbon\Carbon;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\NotFoundException;
+use UserFrosting\Fortress\RequestDataTransformer;
+use UserFrosting\Fortress\RequestSchema;
+use UserFrosting\Fortress\ServerSideValidator;
+use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
+use UserFrosting\Sprinkle\Account\Database\Models\Permission;
+use UserFrosting\Sprinkle\Account\Database\Models\Role;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\ForbiddenException;
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Controller class for permission-related requests, including listing permissions, CRUD for permissions, etc.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class PermissionController extends SimpleController
+{
+ /**
+ * Returns info for a single permission.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ $permissionId = $args['id'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $permission = $classMapper->staticMethod('permission', 'find', $permissionId);
+
+ // If the permission doesn't exist, return 404
+ if (!$permission) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get permission
+ $result = $permission->load('users')->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);
+ }
+
+ /**
+ * Returns a list of Permissions
+ *
+ * Generates a list of permissions, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('permission_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Returns a list of Users for a specified Permission.
+ *
+ * Generates a list of users, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getUsers($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $params['permission_id'] = $args['id'];
+
+ $sprunje = $classMapper->createInstance('permission_user_sprunje', $classMapper, $params);
+
+ $response = $sprunje->toResponse($response);
+
+ // 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;
+ }
+
+ /**
+ * Renders a page displaying a permission's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view permissions.
+ * Note that permissions cannot be modified through the interface. This is because
+ * permissions are highly coupled to the code and should only be modified by developers.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ $permissionId = $args['id'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $permission = $classMapper->staticMethod('permission', 'find', $permissionId);
+
+ // If the permission doesn't exist, return 404
+ if (!$permission) {
+ throw new NotFoundException($request, $response);
+ }
+
+ return $this->ci->view->render($response, 'pages/permission.html.twig', [
+ 'permission' => $permission
+ ]);
+ }
+
+ /**
+ * Renders the permission listing page.
+ *
+ * This page renders a table of permissions, with dropdown menus for admin actions for each permission.
+ * Actions typically include: edit permission, delete permission.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/permissions.html.twig');
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Controller/RoleController.php b/main/app/sprinkles/admin/src/Controller/RoleController.php
new file mode 100755
index 0000000..ab86c88
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Controller/RoleController.php
@@ -0,0 +1,930 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Controller;
+
+use Carbon\Carbon;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\NotFoundException;
+use UserFrosting\Fortress\RequestDataTransformer;
+use UserFrosting\Fortress\RequestSchema;
+use UserFrosting\Fortress\ServerSideValidator;
+use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
+use UserFrosting\Sprinkle\Account\Database\Models\Role;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\ForbiddenException;
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Controller class for role-related requests, including listing roles, CRUD for roles, etc.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class RoleController extends SimpleController
+{
+ /**
+ * Processes the request to create a new role.
+ *
+ * Processes the request from the role creation form, checking that:
+ * 1. The role name and slug are not already in use;
+ * 2. The user has permission to create a new role;
+ * 3. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: POST
+ * @see getModalCreateRole
+ */
+ public function create($request, $response, $args)
+ {
+ // Get POST parameters: name, slug, description
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_role')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/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->staticMethod('role', 'where', 'name', $data['name'])->first()) {
+ $ms->addMessageTranslated('danger', 'ROLE.NAME_IN_USE', $data);
+ $error = true;
+ }
+
+ if ($classMapper->staticMethod('role', 'where', 'slug', $data['slug'])->first()) {
+ $ms->addMessageTranslated('danger', 'SLUG_IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // All checks passed! log events/activities and create role
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) {
+ // Create the role
+ $role = $classMapper->createInstance('role', $data);
+
+ // Store new role to database
+ $role->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} created role {$role->name}.", [
+ 'type' => 'role_create',
+ 'user_id' => $currentUser->id
+ ]);
+
+ $ms->addMessageTranslated('success', 'ROLE.CREATION_SUCCESSFUL', $data);
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to delete an existing role.
+ *
+ * Deletes the specified role.
+ * Before doing so, checks that:
+ * 1. The user has permission to delete this role;
+ * 2. The role is not a default for new users;
+ * 3. The role does not have any associated users;
+ * 4. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: DELETE
+ */
+ public function delete($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_role', [
+ 'role' => $role
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check that we are not deleting a default role
+ $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs');
+
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if (in_array($role->slug, $defaultRoleSlugs)) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.DELETE_DEFAULT');
+ throw $e;
+ }
+
+ // Check if there are any users associated with this role
+ $countUsers = $role->users()->count();
+ if ($countUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.HAS_USERS');
+ throw $e;
+ }
+
+ $roleName = $role->name;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($role, $roleName, $currentUser) {
+ $role->delete();
+ unset($role);
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted role {$roleName}.", [
+ 'type' => 'role_delete',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'ROLE.DELETION_SUCCESSFUL', [
+ 'name' => $roleName
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns info for a single role, along with associated permissions.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_roles')) {
+ throw new ForbiddenException();
+ }
+
+ $slug = $args['slug'];
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $role = $classMapper->staticMethod('role', 'where', 'slug', $slug)->first();
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get role
+ $result = $role->load('permissions')->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);
+ }
+
+ /**
+ * Returns a list of Roles
+ *
+ * Generates a list of roles, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_roles')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ public function getModalConfirmDelete($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $role = $this->getRoleFromParams($params);
+
+ // If the role no longer exists, forward to main role listing page
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_role', [
+ 'role' => $role
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check that we are not deleting a default role
+ $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs');
+
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if (in_array($role->slug, $defaultRoleSlugs)) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.DELETE_DEFAULT', $role->toArray());
+ throw $e;
+ }
+
+ // Check if there are any users associated with this role
+ $countUsers = $role->users()->count();
+ if ($countUsers > 0) {
+ $e = new BadRequestException();
+ $e->addUserMessage('ROLE.HAS_USERS', $role->toArray());
+ throw $e;
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-delete-role.html.twig', [
+ 'role' => $role,
+ 'form' => [
+ 'action' => "api/roles/r/{$role->slug}",
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for creating a new role.
+ *
+ * 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
+ */
+ public function getModalCreate($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_role')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Create a dummy role to prepopulate fields
+ $role = $classMapper->createInstance('role', []);
+
+ $fieldNames = ['name', 'slug', 'description'];
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/role/create.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/role.html.twig', [
+ 'role' => $role,
+ 'form' => [
+ 'action' => 'api/roles',
+ 'method' => 'POST',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('CREATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing an existing role.
+ *
+ * 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
+ */
+ public function getModalEdit($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $role = $this->getRoleFromParams($params);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @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\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "description" for this role
+ $fieldNames = ['name', 'slug', 'description'];
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => $fieldNames
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Generate form
+ $fields = [
+ 'hidden' => [],
+ 'disabled' => []
+ ];
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/role/edit-info.yaml');
+ $validator = new JqueryValidationAdapter($schema, $translator);
+
+ return $this->ci->view->render($response, 'modals/role.html.twig', [
+ 'role' => $role,
+ 'form' => [
+ 'action' => "api/roles/r/{$role->slug}",
+ 'method' => 'PUT',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('UPDATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', false)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing a role's permissions.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEditPermissions($request, $response, $args)
+ {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $role = $this->getRoleFromParams($params);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "permissions" field for this role
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => ['permissions']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'modals/role-manage-permissions.html.twig', [
+ 'role' => $role
+ ]);
+ }
+
+ /**
+ * Returns a list of Permissions for a specified Role.
+ *
+ * Generates a list of permissions, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getPermissions($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role no longer exists, forward to main role listing page
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_role_field', [
+ 'role' => $role,
+ 'property' => 'permissions'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('permission_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($role) {
+ return $query->forRole($role->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Returns users associated with a single role.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getUsers($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role doesn't exist, return 404
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_role_field', [
+ 'role' => $role,
+ 'property' => 'users'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($role) {
+ return $query->forRole($role->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders a page displaying a role's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view the requested role's info.
+ * It checks each field individually, showing only those that you have permission to view.
+ * This will also try to show buttons for deleting and editing the role.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args)
+ {
+ $role = $this->getRoleFromParams($args);
+
+ // If the role no longer exists, forward to main role listing page
+ if (!$role) {
+ $redirectPage = $this->ci->router->pathFor('uri_roles');
+ return $response->withRedirect($redirectPage, 404);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_role', [
+ 'role' => $role
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Determine fields that currentUser is authorized to view
+ $fieldNames = ['name', 'slug', 'description'];
+
+ // Generate form
+ $fields = [
+ 'hidden' => []
+ ];
+
+ foreach ($fieldNames as $field) {
+ if (!$authorizer->checkAccess($currentUser, 'view_role_field', [
+ 'role' => $role,
+ 'property' => $field
+ ])) {
+ $fields['hidden'][] = $field;
+ }
+ }
+
+ // Determine buttons to display
+ $editButtons = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => ['name', 'slug', 'description']
+ ])) {
+ $editButtons['hidden'][] = 'edit';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'delete_role', [
+ 'role' => $role
+ ])) {
+ $editButtons['hidden'][] = 'delete';
+ }
+
+ return $this->ci->view->render($response, 'pages/role.html.twig', [
+ 'role' => $role,
+ 'fields' => $fields,
+ 'tools' => $editButtons
+ ]);
+ }
+
+ /**
+ * Renders the role listing page.
+ *
+ * This page renders a table of roles, with dropdown menus for admin actions for each role.
+ * Actions typically include: edit role, delete role.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args)
+ {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_roles')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/roles.html.twig');
+ }
+
+ /**
+ * Processes the request to update an existing role's details.
+ *
+ * Processes the request from the role update form, checking that:
+ * 1. The role name/slug are not already in use;
+ * 2. The user has the necessary permissions to update the posted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: PUT
+ * @see getModalRoleEdit
+ */
+ public function updateInfo($request, $response, $args)
+ {
+ // Get the role based on slug in the URL
+ $role = $this->getRoleFromParams($args);
+
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters: (name, slug, description)
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/edit-info.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;
+ }
+
+ // Determine targeted fields
+ $fieldNames = [];
+ foreach ($data as $name => $value) {
+ $fieldNames[] = $name;
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit submitted fields for this role
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => array_values(array_unique($fieldNames))
+ ])) {
+ 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'] != $role->name &&
+ $classMapper->staticMethod('role', 'where', 'name', $data['name'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'ROLE.NAME_IN_USE', $data);
+ $error = true;
+ }
+
+ if (
+ isset($data['slug']) &&
+ $data['slug'] != $role->slug &&
+ $classMapper->staticMethod('role', 'where', 'slug', $data['slug'])->first()
+ ) {
+ $ms->addMessageTranslated('danger', 'SLUG_IN_USE', $data);
+ $error = true;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($data, $role, $currentUser) {
+ // Update the role and generate success messages
+ foreach ($data as $name => $value) {
+ if ($value != $role->$name){
+ $role->$name = $value;
+ }
+ }
+
+ $role->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for role {$role->name}.", [
+ 'type' => 'role_update_info',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ $ms->addMessageTranslated('success', 'ROLE.UPDATED', [
+ 'name' => $role->name
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to update a specific field for an existing role, including permissions.
+ *
+ * Processes the request from the role update form, checking that:
+ * 1. The logged-in user has the necessary permissions to update the putted field(s);
+ * 2. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: PUT
+ */
+ public function updateField($request, $response, $args)
+ {
+ // Get the username from the URL
+ $role = $this->getRoleFromParams($args);
+
+ if (!$role) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get key->value pair from URL and request body
+ $fieldName = $args['field'];
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit the specified field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_role_field', [
+ 'role' => $role,
+ 'fields' => [$fieldName]
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters: value
+ $put = $request->getParsedBody();
+
+ if (!isset($put['value'])) {
+ throw new BadRequestException();
+ }
+
+ $params = [
+ $fieldName => $put['value']
+ ];
+
+ // Validate key -> value pair
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/edit-field.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;
+ }
+
+ // Get validated and transformed value
+ $fieldValue = $data[$fieldName];
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $ms = $this->ci->alerts;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction( function() use ($fieldName, $fieldValue, $role, $currentUser) {
+ if ($fieldName == 'permissions') {
+ $newPermissions = collect($fieldValue)->pluck('permission_id')->all();
+ $role->permissions()->sync($newPermissions);
+ } else {
+ $role->$fieldName = $fieldValue;
+ $role->save();
+ }
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for role {$role->name}.", [
+ 'type' => 'role_update_field',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ // Add success messages
+ if ($fieldName == 'permissions') {
+ $ms->addMessageTranslated('success', 'ROLE.PERMISSIONS_UPDATED', [
+ 'name' => $role->name
+ ]);
+ } else {
+ $ms->addMessageTranslated('success', 'ROLE.UPDATED', [
+ 'name' => $role->name
+ ]);
+ }
+
+ return $response->withStatus(200);
+ }
+
+ protected function getRoleFromParams($params)
+ {
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/role/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 role
+ $role = $classMapper->staticMethod('role', 'where', 'slug', $data['slug'])
+ ->first();
+
+ return $role;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Controller/UserController.php b/main/app/sprinkles/admin/src/Controller/UserController.php
new file mode 100755
index 0000000..5bece6a
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Controller/UserController.php
@@ -0,0 +1,1261 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Admin\Controller;
+
+use Carbon\Carbon;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\NotFoundException;
+use UserFrosting\Fortress\RequestDataTransformer;
+use UserFrosting\Fortress\RequestSchema;
+use UserFrosting\Fortress\ServerSideValidator;
+use UserFrosting\Fortress\Adapter\JqueryValidationAdapter;
+use UserFrosting\Sprinkle\Account\Database\Models\Group;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Account\Facades\Password;
+use UserFrosting\Sprinkle\Core\Controller\SimpleController;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Mail\EmailRecipient;
+use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\ForbiddenException;
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Controller class for user-related requests, including listing users, CRUD for users, etc.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class UserController extends SimpleController
+{
+ /**
+ * Processes the request to create a new user (from the admin controls).
+ *
+ * Processes the request from the user creation form, checking that:
+ * 1. The username and email are not already in use;
+ * 2. The logged-in user has the necessary permissions to update the posted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: POST
+ * @see getModalCreate
+ */
+ public function create($request, $response, $args) {
+ // Get POST parameters: user_name, first_name, last_name, email, locale, (group)
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_user')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/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 username or email already exists
+ if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) {
+ $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data);
+ $error = TRUE;
+ }
+
+ if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) {
+ $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data);
+ $error = TRUE;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // If currentUser does not have permission to set the group, but they try to set it to something other than their own group,
+ // throw an exception.
+ if (!$authorizer->checkAccess($currentUser, 'create_user_field', [
+ 'fields' => ['group']
+ ])) {
+ if (isset($data['group_id']) && $data['group_id'] != $currentUser->group_id) {
+ throw new ForbiddenException();
+ }
+ }
+
+ // In any case, set the group id if not otherwise set
+ if (!isset($data['group_id'])) {
+ $data['group_id'] = $currentUser->group_id;
+ }
+
+ $data['flag_verified'] = 1;
+ // Set password as empty on initial creation. We will then send email so new user can set it themselves via a verification token
+ $data['password'] = '';
+
+ // All checks passed! log events/activities, create user, and send verification email (if required)
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction(function () use ($classMapper, $data, $ms, $config, $currentUser) {
+ // Create the user
+ $user = $classMapper->createInstance('user', $data);
+
+ // Store new user to database
+ $user->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} created a new account for {$user->user_name}.", [
+ 'type' => 'account_create',
+ 'user_id' => $currentUser->id
+ ]);
+
+ // Load default roles
+ $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs');
+ $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get();
+ $defaultRoleIds = $defaultRoles->pluck('id')->all();
+
+ // Attach default roles
+ $user->roles()->attach($defaultRoleIds);
+
+ // Try to generate a new password request
+ $passwordRequest = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.create']);
+
+ // Create and send welcome email with password set link
+ $message = new TwigMailMessage($this->ci->view, 'mail/password-create.html.twig');
+
+ $message->from($config['address_book.admin'])
+ ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
+ ->addParams([
+ 'user' => $user,
+ 'create_password_expiration' => $config['password_reset.timeouts.create'] / 3600 . ' hours',
+ 'token' => $passwordRequest->getToken()
+ ]);
+
+ $this->ci->mailer->send($message);
+
+ $ms->addMessageTranslated('success', 'USER.CREATED', $data);
+ });
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to send a user a password reset email.
+ *
+ * Processes the request from the user update form, checking that:
+ * 1. The target user's new email address, if specified, is not already in use;
+ * 2. The logged-in user has the necessary permissions to update the posted field(s);
+ * 3. We're not trying to disable the master account;
+ * 4. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: POST
+ */
+ public function createPasswordReset($request, $response, $args) {
+ // Get the username from the URL
+ $user = $this->getUserFromParams($args);
+
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "password" for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['password']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction(function () use ($user, $config) {
+
+ // Create a password reset and shoot off an email
+ $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']);
+
+ // Create and send welcome email with password set link
+ $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig');
+
+ $message->from($config['address_book.admin'])
+ ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
+ ->addParams([
+ 'user' => $user,
+ 'token' => $passwordReset->getToken(),
+ 'request_date' => Carbon::now()->format('Y-m-d H:i:s')
+ ]);
+
+ $this->ci->mailer->send($message);
+ });
+
+ $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', [
+ 'email' => $user->email
+ ]);
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to delete an existing user.
+ *
+ * Deletes the specified user, removing any existing associations.
+ * Before doing so, checks that:
+ * 1. You are not trying to delete the master account;
+ * 2. You have permission to delete the target user's account.
+ * This route requires authentication (and should generally be limited to admins or the root user).
+ * Request type: DELETE
+ */
+ public function delete($request, $response, $args) {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Check that we are not deleting the master account
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if ($user->id == $config['reserved_user_ids.master']) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DELETE_MASTER');
+ throw $e;
+ }
+
+ $userName = $user->user_name;
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction(function () use ($user, $userName, $currentUser) {
+ $user->delete();
+ unset($user);
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted the account for {$userName}.", [
+ 'type' => 'account_delete',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ $ms->addMessageTranslated('success', 'DELETION_SUCCESSFUL', [
+ 'user_name' => $userName
+ ]);
+
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Returns activity history for a single user.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getActivities($request, $response, $args) {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'activities'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params);
+
+ $sprunje->extendQuery(function ($query) use ($user) {
+ return $query->where('user_id', $user->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Returns info for a single user.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getInfo($request, $response, $args) {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Join user's most recent activity
+ $user = $classMapper->createInstance('user')
+ ->where('user_name', $user->user_name)
+ ->joinLastActivity()
+ ->with('lastActivity', 'group')
+ ->first();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $result = $user->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);
+ }
+
+ /**
+ * Returns a list of Users
+ *
+ * Generates a list of users, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getList($request, $response, $args) {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_users')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params);
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders the modal form to confirm user deletion.
+ *
+ * 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
+ */
+ public function getModalConfirmDelete($request, $response, $args) {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'delete_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Check that we are not deleting the master account
+ // Need to use loose comparison for now, because some DBs return `id` as a string
+ if ($user->id == $config['reserved_user_ids.master']) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DELETE_MASTER');
+ throw $e;
+ }
+
+ return $this->ci->view->render($response, 'modals/confirm-delete-user.html.twig', [
+ 'user' => $user,
+ 'form' => [
+ 'action' => "api/users/u/{$user->user_name}",
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for creating a new user.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages.
+ * If the currently logged-in user has permission to modify user group membership, then the group toggle will be displayed.
+ * Otherwise, the user will be added to the default group and receive the default roles automatically.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalCreate($request, $response, $args) {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ /** @var UserFrosting\I18n\MessageTranslator $translator */
+ $translator = $this->ci->translator;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'create_user')) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Determine form fields to hide/disable
+ // TODO: come back to this when we finish implementing theming
+ $fields = [
+ 'hidden' => ['theme'],
+ 'disabled' => []
+ ];
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ // Determine if currentUser has permission to modify the group. If so, show the 'group' dropdown.
+ // Otherwise, set to the currentUser's group and disable the dropdown.
+ if ($authorizer->checkAccess($currentUser, 'create_user_field', [
+ 'fields' => ['group']
+ ])) {
+ // Get a list of all groups
+ $groups = $classMapper->staticMethod('group', 'all');
+ } else {
+ // Get the current user's group
+ $groups = $currentUser->group()->get();
+ $fields['disabled'][] = 'group';
+ }
+
+ // Create a dummy user to prepopulate fields
+ $data = [
+ 'group_id' => $currentUser->group_id,
+ 'locale' => $config['site.registration.user_defaults.locale'],
+ 'theme' => ''
+ ];
+
+ $user = $classMapper->createInstance('user', $data);
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/user/create.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/user.html.twig', [
+ 'user' => $user,
+ 'groups' => $groups,
+ 'locales' => $locales,
+ 'form' => [
+ 'action' => 'api/users',
+ 'method' => 'POST',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('CREATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', FALSE)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing an existing user.
+ *
+ * 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
+ */
+ public function getModalEdit($request, $response, $args) {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Get the user to edit
+ $user = $classMapper->staticMethod('user', 'where', 'user_name', $user->user_name)
+ ->with('group')
+ ->first();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "email", "locale" for this user
+ $fieldNames = ['name', 'email', 'locale'];
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => $fieldNames
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Get a list of all groups
+ $groups = $classMapper->staticMethod('group', 'all');
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ // Generate form
+ $fields = [
+ 'hidden' => ['theme'],
+ 'disabled' => ['user_name']
+ ];
+
+ // Disable group field if currentUser doesn't have permission to modify group
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['group']
+ ])) {
+ $fields['disabled'][] = 'group';
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/user/edit-info.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ $translator = $this->ci->translator;
+
+ return $this->ci->view->render($response, 'modals/user.html.twig', [
+ 'user' => $user,
+ 'groups' => $groups,
+ 'locales' => $locales,
+ 'form' => [
+ 'action' => "api/users/u/{$user->user_name}",
+ 'method' => 'PUT',
+ 'fields' => $fields,
+ 'submit_text' => $translator->translate('UPDATE')
+ ],
+ 'page' => [
+ 'validators' => $validator->rules('json', FALSE)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing a user's password.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEditPassword($request, $response, $args) {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "password" field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['password']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Load validation rules
+ $schema = new RequestSchema('schema://requests/user/edit-password.yaml');
+ $validator = new JqueryValidationAdapter($schema, $this->ci->translator);
+
+ return $this->ci->view->render($response, 'modals/user-set-password.html.twig', [
+ 'user' => $user,
+ 'page' => [
+ 'validators' => $validator->rules('json', FALSE)
+ ]
+ ]);
+ }
+
+ /**
+ * Renders the modal form for editing a user's roles.
+ *
+ * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getModalEditRoles($request, $response, $args) {
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ $user = $this->getUserFromParams($params);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit "roles" field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['roles']
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'modals/user-manage-roles.html.twig', [
+ 'user' => $user
+ ]);
+ }
+
+ /**
+ * Returns a list of effective Permissions for a specified User.
+ *
+ * Generates a list of permissions, optionally paginated, sorted and/or filtered.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getPermissions($request, $response, $args) {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'permissions'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ $params['user_id'] = $user->id;
+ $sprunje = $classMapper->createInstance('user_permission_sprunje', $classMapper, $params);
+
+ $response = $sprunje->toResponse($response);
+
+ // 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;
+ }
+
+ /**
+ * Returns roles associated with a single user.
+ *
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function getRoles($request, $response, $args) {
+ $user = $this->getUserFromParams($args);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // GET parameters
+ $params = $request->getQueryParams();
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'roles'
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params);
+ $sprunje->extendQuery(function ($query) use ($user) {
+ return $query->forUser($user->id);
+ });
+
+ // 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 $sprunje->toResponse($response);
+ }
+
+ /**
+ * Renders a page displaying a user's information, in read-only mode.
+ *
+ * This checks that the currently logged-in user has permission to view the requested user's info.
+ * It checks each field individually, showing only those that you have permission to view.
+ * This will also try to show buttons for activating, disabling/enabling, deleting, and editing the user.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageInfo($request, $response, $args) {
+ $user = $this->getUserFromParams($args);
+
+ // If the user no longer exists, forward to main user listing page
+ if (!$user) {
+ $usersPage = $this->ci->router->pathFor('uri_users');
+ return $response->withRedirect($usersPage, 404);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_user', [
+ 'user' => $user
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get a list of all locales
+ $locales = $config->getDefined('site.locales.available');
+
+ // Determine fields that currentUser is authorized to view
+ $fieldNames = ['user_name', 'name', 'email', 'locale', 'group', 'roles'];
+
+ // Generate form
+ $fields = [
+ // Always hide these
+ 'hidden' => ['theme']
+ ];
+
+ // Determine which fields should be hidden
+ foreach ($fieldNames as $field) {
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => $field
+ ])) {
+ $fields['hidden'][] = $field;
+ }
+ }
+
+ // Determine buttons to display
+ $editButtons = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['name', 'email', 'locale']
+ ])) {
+ $editButtons['hidden'][] = 'edit';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['flag_enabled']
+ ])) {
+ $editButtons['hidden'][] = 'enable';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['flag_verified']
+ ])) {
+ $editButtons['hidden'][] = 'activate';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['password']
+ ])) {
+ $editButtons['hidden'][] = 'password';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => ['roles']
+ ])) {
+ $editButtons['hidden'][] = 'roles';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'delete_user', [
+ 'user' => $user
+ ])) {
+ $editButtons['hidden'][] = 'delete';
+ }
+
+ // Determine widgets to display
+ $widgets = [
+ 'hidden' => []
+ ];
+
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'permissions'
+ ])) {
+ $widgets['hidden'][] = 'permissions';
+ }
+
+ if (!$authorizer->checkAccess($currentUser, 'view_user_field', [
+ 'user' => $user,
+ 'property' => 'activities'
+ ])) {
+ $widgets['hidden'][] = 'activities';
+ }
+
+ return $this->ci->view->render($response, 'pages/user.html.twig', [
+ 'user' => $user,
+ 'locales' => $locales,
+ 'fields' => $fields,
+ 'tools' => $editButtons,
+ 'widgets' => $widgets
+ ]);
+ }
+
+ /**
+ * Renders the user listing page.
+ *
+ * This page renders a table of users, with dropdown menus for admin actions for each user.
+ * Actions typically include: edit user details, activate user, enable/disable user, delete user.
+ * This page requires authentication.
+ * Request type: GET
+ */
+ public function pageList($request, $response, $args) {
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled page
+ if (!$authorizer->checkAccess($currentUser, 'uri_users')) {
+ throw new ForbiddenException();
+ }
+
+ return $this->ci->view->render($response, 'pages/users.html.twig');
+ }
+
+ /**
+ * Processes the request to update an existing user's basic details (first_name, last_name, email, locale, group_id)
+ *
+ * Processes the request from the user update form, checking that:
+ * 1. The target user's new email address, if specified, is not already in use;
+ * 2. The logged-in user has the necessary permissions to update the putted field(s);
+ * 3. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: PUT
+ */
+ public function updateInfo($request, $response, $args) {
+ // Get the username from the URL
+ $user = $this->getUserFromParams($args);
+
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Get PUT parameters
+ $params = $request->getParsedBody();
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/edit-info.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;
+ }
+
+ // Determine targeted fields
+ $fieldNames = [];
+ foreach ($data as $name => $value) {
+ if ($name == 'first_name' || $name == 'last_name') {
+ $fieldNames[] = 'name';
+ } elseif ($name == 'group_id') {
+ $fieldNames[] = 'group';
+ } else {
+ $fieldNames[] = $name;
+ }
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit submitted fields for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => array_values(array_unique($fieldNames))
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ // Only the master account can edit the master account!
+ if (
+ ($user->id == $config['reserved_user_ids.master']) &&
+ ($currentUser->id != $config['reserved_user_ids.master'])
+ ) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+ $classMapper = $this->ci->classMapper;
+
+ // Check if email already exists
+ if (
+ isset($data['email']) &&
+ $data['email'] != $user->email &&
+ $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')
+ ) {
+ $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data);
+ $error = TRUE;
+ }
+
+ if ($error) {
+ return $response->withStatus(400);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction(function () use ($data, $user, $currentUser) {
+ // Update the user and generate success messages
+ foreach ($data as $name => $value) {
+ if ($value != $user->$name) {
+ $user->$name = $value;
+ }
+ }
+
+ $user->save();
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated basic account info for user {$user->user_name}.", [
+ 'type' => 'account_update_info',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [
+ 'user_name' => $user->user_name
+ ]);
+ return $response->withStatus(200);
+ }
+
+ /**
+ * Processes the request to update a specific field for an existing user.
+ *
+ * Supports editing all user fields, including password, enabled/disabled status and verification status.
+ * Processes the request from the user update form, checking that:
+ * 1. The logged-in user has the necessary permissions to update the putted field(s);
+ * 2. We're not trying to disable the master account;
+ * 3. The submitted data is valid.
+ * This route requires authentication.
+ * Request type: PUT
+ */
+ public function updateField($request, $response, $args) {
+ // Get the username from the URL
+ $user = $this->getUserFromParams($args);
+
+ if (!$user) {
+ throw new NotFoundException($request, $response);
+ }
+
+ // Get key->value pair from URL and request body
+ $fieldName = $args['field'];
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
+ $authorizer = $this->ci->authorizer;
+
+ /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */
+ $currentUser = $this->ci->currentUser;
+
+ // Access-controlled resource - check that currentUser has permission to edit the specified field for this user
+ if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
+ 'user' => $user,
+ 'fields' => [$fieldName]
+ ])) {
+ throw new ForbiddenException();
+ }
+
+ /** @var UserFrosting\Config\Config $config */
+ $config = $this->ci->config;
+
+ // Only the master account can edit the master account!
+ if (
+ ($user->id == $config['reserved_user_ids.master']) &&
+ ($currentUser->id != $config['reserved_user_ids.master'])
+ ) {
+ throw new ForbiddenException();
+ }
+
+ // Get PUT parameters: value
+ $put = $request->getParsedBody();
+
+ if (!isset($put['value'])) {
+ throw new BadRequestException();
+ }
+
+ // Create and validate key -> value pair
+ $params = [
+ $fieldName => $put['value']
+ ];
+
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/edit-field.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;
+ }
+
+ // Get validated and transformed value
+ $fieldValue = $data[$fieldName];
+
+ /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */
+ $ms = $this->ci->alerts;
+
+ // Special checks and transformations for certain fields
+ if ($fieldName == 'flag_enabled') {
+ // Check that we are not disabling the master account
+ if (
+ ($user->id == $config['reserved_user_ids.master']) &&
+ ($fieldValue == '0')
+ ) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DISABLE_MASTER');
+ throw $e;
+ } elseif (
+ ($user->id == $currentUser->id) &&
+ ($fieldValue == '0')
+ ) {
+ $e = new BadRequestException();
+ $e->addUserMessage('DISABLE_SELF');
+ throw $e;
+ }
+ } elseif ($fieldName == 'password') {
+ $fieldValue = Password::hash($fieldValue);
+ }
+
+ // Begin transaction - DB will be rolled back if an exception occurs
+ Capsule::transaction(function () use ($fieldName, $fieldValue, $user, $currentUser) {
+ if ($fieldName == 'roles') {
+ $newRoles = collect($fieldValue)->pluck('role_id')->all();
+ $user->roles()->sync($newRoles);
+ } else {
+ $user->$fieldName = $fieldValue;
+ $user->save();
+ }
+
+ // Create activity record
+ $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for user {$user->user_name}.", [
+ 'type' => 'account_update_field',
+ 'user_id' => $currentUser->id
+ ]);
+ });
+
+ // Add success messages
+ if ($fieldName == 'flag_enabled') {
+ if ($fieldValue == '1') {
+ $ms->addMessageTranslated('success', 'ENABLE_SUCCESSFUL', [
+ 'user_name' => $user->user_name
+ ]);
+ } else {
+ $ms->addMessageTranslated('success', 'DISABLE_SUCCESSFUL', [
+ 'user_name' => $user->user_name
+ ]);
+ }
+ } elseif ($fieldName == 'flag_verified') {
+ $ms->addMessageTranslated('success', 'MANUALLY_ACTIVATED', [
+ 'user_name' => $user->user_name
+ ]);
+ } else {
+ $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [
+ 'user_name' => $user->user_name
+ ]);
+ }
+
+ return $response->withStatus(200);
+ }
+
+ protected function getUserFromParams($params) {
+ // Load the request schema
+ $schema = new RequestSchema('schema://requests/user/get-by-username.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 user to delete
+ $user = $classMapper->staticMethod('user', 'where', 'user_name', $data['user_name'])
+ ->first();
+
+ return $user;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php b/main/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php
new file mode 100755
index 0000000..061d90c
--- /dev/null
+++ b/main/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\ServicesProvider;
+
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use UserFrosting\Sprinkle\Account\Authenticate\Authenticator;
+use UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+
+/**
+ * Registers services for the admin sprinkle.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class ServicesProvider
+{
+ /**
+ * Register UserFrosting's admin services.
+ *
+ * @param Container $container A DI container implementing ArrayAccess and container-interop.
+ */
+ public function register($container)
+ {
+ /**
+ * Extend the 'classMapper' service to register sprunje classes.
+ *
+ * Mappings added: 'activity_sprunje', 'group_sprunje', 'permission_sprunje', 'role_sprunje', 'user_sprunje'
+ */
+ $container->extend('classMapper', function ($classMapper, $c) {
+ $classMapper->setClassMapping('activity_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\ActivitySprunje');
+ $classMapper->setClassMapping('group_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\GroupSprunje');
+ $classMapper->setClassMapping('permission_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\PermissionSprunje');
+ $classMapper->setClassMapping('permission_user_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\PermissionUserSprunje');
+ $classMapper->setClassMapping('role_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\RoleSprunje');
+ $classMapper->setClassMapping('user_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\UserSprunje');
+ $classMapper->setClassMapping('user_permission_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\UserPermissionSprunje');
+ return $classMapper;
+ });
+
+ /**
+ * Returns a callback that handles setting the `UF-Redirect` header after a successful login.
+ *
+ * Overrides the service definition in the account Sprinkle.
+ */
+ $container['redirect.onLogin'] = function ($c) {
+ /**
+ * This method is invoked when a user completes the login process.
+ *
+ * Returns a callback that handles setting the `UF-Redirect` header after a successful login.
+ * @param \Psr\Http\Message\ServerRequestInterface $request
+ * @param \Psr\Http\Message\ResponseInterface $response
+ * @param array $args
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ return function (Request $request, Response $response, array $args) use ($c) {
+ // Backwards compatibility for the deprecated determineRedirectOnLogin service
+ if ($c->has('determineRedirectOnLogin')) {
+ $determineRedirectOnLogin = $c->determineRedirectOnLogin;
+
+ return $determineRedirectOnLogin($response)->withStatus(200);
+ }
+
+ /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */
+ $authorizer = $c->authorizer;
+
+ $currentUser = $c->authenticator->user();
+
+ if ($authorizer->checkAccess($currentUser, 'uri_dashboard')) {
+ return $response->withHeader('UF-Redirect', $c->router->pathFor('dashboard'));
+ } elseif ($authorizer->checkAccess($currentUser, 'uri_account_settings')) {
+ return $response->withHeader('UF-Redirect', $c->router->pathFor('settings'));
+ } else {
+ return $response->withHeader('UF-Redirect', $c->router->pathFor('index'));
+ }
+ };
+ };
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php b/main/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php
new file mode 100755
index 0000000..da4f0e3
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
+
+/**
+ * ActivitySprunje
+ *
+ * Implements Sprunje for the activities API.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class ActivitySprunje extends Sprunje
+{
+ protected $sortable = [
+ 'occurred_at',
+ 'user',
+ 'description'
+ ];
+
+ protected $filterable = [
+ 'occurred_at',
+ 'user',
+ 'description'
+ ];
+
+ protected $name = 'activities';
+
+ /**
+ * Set the initial query used by your Sprunje.
+ */
+ protected function baseQuery()
+ {
+ $query = $this->classMapper->createInstance('activity');
+
+ return $query->joinUser();
+ }
+
+ /**
+ * Filter LIKE the user info.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterUser($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('users.first_name', $value)
+ ->orLike('users.last_name', $value)
+ ->orLike('users.email', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Sort based on user last name.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortUser($query, $direction)
+ {
+ $query->orderBy('users.last_name', $direction);
+ return $this;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/GroupSprunje.php b/main/app/sprinkles/admin/src/Sprunje/GroupSprunje.php
new file mode 100755
index 0000000..7c75691
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/GroupSprunje.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
+
+/**
+ * GroupSprunje
+ *
+ * Implements Sprunje for the groups API.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class GroupSprunje extends Sprunje
+{
+ protected $name = 'groups';
+
+ protected $sortable = [
+ 'name',
+ 'description'
+ ];
+
+ protected $filterable = [
+ 'name',
+ 'description'
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function baseQuery()
+ {
+ return $this->classMapper->createInstance('group')->newQuery();
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php b/main/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php
new file mode 100755
index 0000000..c1803f1
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
+
+/**
+ * PermissionSprunje
+ *
+ * Implements Sprunje for the permissions API.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class PermissionSprunje extends Sprunje
+{
+ protected $name = 'permissions';
+
+ protected $sortable = [
+ 'name',
+ 'properties'
+ ];
+
+ protected $filterable = [
+ 'name',
+ 'properties',
+ 'info'
+ ];
+
+ protected $excludeForAll = [
+ 'info'
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function baseQuery()
+ {
+ return $this->classMapper->createInstance('permission')->newQuery();
+ }
+
+ /**
+ * Filter LIKE the slug, conditions, or description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterInfo($query, $value)
+ {
+ return $this->filterProperties($query, $value);
+ }
+
+ /**
+ * Filter LIKE the slug, conditions, or description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterProperties($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('slug', $value)
+ ->orLike('conditions', $value)
+ ->orLike('description', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Sort based on slug.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortProperties($query, $direction)
+ {
+ $query->orderBy('slug', $direction);
+ return $this;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php b/main/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php
new file mode 100755
index 0000000..242681d
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\NotFoundException;
+
+/**
+ * PermissionUserSprunje
+ *
+ * Implements Sprunje for retrieving a list of users for a specified permission.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class PermissionUserSprunje extends UserSprunje
+{
+ protected $name = 'permission_users';
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function baseQuery()
+ {
+ // Requires a permission id
+ if (!isset($this->options['permission_id'])) {
+ throw new BadRequestException();
+ }
+
+ $permission = $this->classMapper->staticMethod('permission', 'find', $this->options['permission_id']);
+
+ // If the permission doesn't exist, return 404
+ if (!$permission) {
+ throw new NotFoundException;
+ }
+
+ // Get permission users
+ $query = $permission->users()->withVia('roles_via');
+
+ return $query;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/RoleSprunje.php b/main/app/sprinkles/admin/src/Sprunje/RoleSprunje.php
new file mode 100755
index 0000000..c5e0f8b
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/RoleSprunje.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
+
+/**
+ * RoleSprunje
+ *
+ * Implements Sprunje for the roles API.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class RoleSprunje extends Sprunje
+{
+ protected $name = 'roles';
+
+ protected $sortable = [
+ 'name',
+ 'description'
+ ];
+
+ protected $filterable = [
+ 'name',
+ 'description',
+ 'info'
+ ];
+
+ protected $excludeForAll = [
+ 'info'
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function baseQuery()
+ {
+ return $this->classMapper->createInstance('role')->newQuery();
+ }
+
+ /**
+ * Filter LIKE name OR description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterInfo($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('name', $value)
+ ->orLike('description', $value);
+ }
+ });
+ return $this;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php b/main/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php
new file mode 100755
index 0000000..6142e74
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Support\Exception\BadRequestException;
+use UserFrosting\Support\Exception\NotFoundException;
+
+/**
+ * UserPermissionSprunje
+ *
+ * Implements Sprunje for retrieving a list of permissions for a specified user.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class UserPermissionSprunje extends PermissionSprunje
+{
+ protected $name = 'user_permissions';
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function baseQuery()
+ {
+ // Requires a user id
+ if (!isset($this->options['user_id'])) {
+ throw new BadRequestException();
+ }
+
+ $user = $this->classMapper->staticMethod('user', 'find', $this->options['user_id']);
+
+ // If the user doesn't exist, return 404
+ if (!$user) {
+ throw new NotFoundException;
+ }
+
+ // Get user permissions
+ $query = $user->permissions()->withVia('roles_via');
+
+ return $query;
+ }
+}
diff --git a/main/app/sprinkles/admin/src/Sprunje/UserSprunje.php b/main/app/sprinkles/admin/src/Sprunje/UserSprunje.php
new file mode 100755
index 0000000..12378f9
--- /dev/null
+++ b/main/app/sprinkles/admin/src/Sprunje/UserSprunje.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+namespace UserFrosting\Sprinkle\Admin\Sprunje;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Sprinkle\Core\Facades\Debug;
+use UserFrosting\Sprinkle\Core\Facades\Translator;
+use UserFrosting\Sprinkle\Core\Sprunje\Sprunje;
+
+/**
+ * UserSprunje
+ *
+ * Implements Sprunje for the users API.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class UserSprunje extends Sprunje
+{
+ protected $name = 'users';
+
+ protected $listable = [
+ 'status'
+ ];
+
+ protected $sortable = [
+ 'name',
+ 'last_activity',
+ 'status'
+ ];
+
+ protected $filterable = [
+ 'name',
+ 'last_activity',
+ 'status'
+ ];
+
+ protected $excludeForAll = [
+ 'last_activity'
+ ];
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function baseQuery()
+ {
+ $query = $this->classMapper->createInstance('user');
+
+ // Join user's most recent activity
+ return $query->joinLastActivity()->with('lastActivity');
+ }
+
+ /**
+ * Filter LIKE the last activity description.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterLastActivity($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('activities.description', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Filter LIKE the first name, last name, or email.
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterName($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ $query->orLike('first_name', $value)
+ ->orLike('last_name', $value)
+ ->orLike('email', $value);
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Filter by status (active, disabled, unactivated)
+ *
+ * @param Builder $query
+ * @param mixed $value
+ * @return $this
+ */
+ protected function filterStatus($query, $value)
+ {
+ // Split value on separator for OR queries
+ $values = explode($this->orSeparator, $value);
+ $query->where(function ($query) use ($values) {
+ foreach ($values as $value) {
+ if ($value == 'disabled') {
+ $query->orWhere('flag_enabled', 0);
+ } elseif ($value == 'unactivated') {
+ $query->orWhere('flag_verified', 0);
+ } elseif ($value == 'active') {
+ $query->orWhere(function ($query) {
+ $query->where('flag_enabled', 1)->where('flag_verified', 1);
+ });
+ }
+ }
+ });
+ return $this;
+ }
+
+ /**
+ * Return a list of possible user statuses.
+ *
+ * @return array
+ */
+ protected function listStatus()
+ {
+ return [
+ [
+ 'value' => 'active',
+ 'text' => Translator::translate('ACTIVE')
+ ],
+ [
+ 'value' => 'unactivated',
+ 'text' => Translator::translate('UNACTIVATED')
+ ],
+ [
+ 'value' => 'disabled',
+ 'text' => Translator::translate('DISABLED')
+ ]
+ ];
+ }
+
+ /**
+ * Sort based on last activity time.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortLastActivity($query, $direction)
+ {
+ $query->orderBy('activities.occurred_at', $direction);
+ return $this;
+ }
+
+ /**
+ * Sort based on last name.
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortName($query, $direction)
+ {
+ $query->orderBy('last_name', $direction);
+ return $this;
+ }
+
+ /**
+ * Sort active, unactivated, disabled
+ *
+ * @param Builder $query
+ * @param string $direction
+ * @return $this
+ */
+ protected function sortStatus($query, $direction)
+ {
+ $query->orderBy('flag_enabled', $direction)->orderBy('flag_verified', $direction);
+ return $this;
+ }
+}