From 92b7dd3335a6572debeacfb5faa82c63a5e67888 Mon Sep 17 00:00:00 2001 From: Marvin Borner Date: Fri, 8 Jun 2018 20:03:25 +0200 Subject: Some minor fixes --- .../admin/src/Controller/ActivityController.php | 168 +- .../admin/src/Controller/AdminController.php | 296 +- .../admin/src/Controller/GroupController.php | 1426 ++++----- .../admin/src/Controller/PermissionController.php | 404 +-- .../admin/src/Controller/PostController.php | 382 +-- .../admin/src/Controller/RoleController.php | 1830 ++++++------ .../admin/src/Controller/SearchController.php | 112 +- .../admin/src/Controller/UserController.php | 3032 ++++++++++---------- .../admin/src/Controller/WormholeController.php | 292 +- 9 files changed, 3971 insertions(+), 3971 deletions(-) (limited to 'main/app/sprinkles/admin/src/Controller') diff --git a/main/app/sprinkles/admin/src/Controller/ActivityController.php b/main/app/sprinkles/admin/src/Controller/ActivityController.php index 09675d2..0d43fd8 100644 --- a/main/app/sprinkles/admin/src/Controller/ActivityController.php +++ b/main/app/sprinkles/admin/src/Controller/ActivityController.php @@ -1,84 +1,84 @@ -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'); - } -} +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 index 91342de..7e4e2f0 100644 --- a/main/app/sprinkles/admin/src/Controller/AdminController.php +++ b/main/app/sprinkles/admin/src/Controller/AdminController.php @@ -1,148 +1,148 @@ -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', - ] - ]); - } -} +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 index 720f12d..17659ec 100644 --- a/main/app/sprinkles/admin/src/Controller/GroupController.php +++ b/main/app/sprinkles/admin/src/Controller/GroupController.php @@ -1,713 +1,713 @@ -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)) { - $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; - } -} +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)) { + $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 index e8e542f..133cfe3 100644 --- a/main/app/sprinkles/admin/src/Controller/PermissionController.php +++ b/main/app/sprinkles/admin/src/Controller/PermissionController.php @@ -1,202 +1,202 @@ -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'); - } -} +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/PostController.php b/main/app/sprinkles/admin/src/Controller/PostController.php index a310237..cab87cd 100644 --- a/main/app/sprinkles/admin/src/Controller/PostController.php +++ b/main/app/sprinkles/admin/src/Controller/PostController.php @@ -1,191 +1,191 @@ -getUserFromParams($args); - - // If the user doesn't exist, return 404 - if (!$user) { - throw new NotFoundException(); - } - - // Get friends first // TODO: Make friend select query more efficient - $UsersFriends = DB::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1"); - - /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ - $classMapper = $this->ci->classMapper; - $ImagesFromFriends = ""; - $config = $this->ci->config; - foreach ($UsersFriends as $UsersFriendId) { // NOT THAT EFFICIENT... (get images from all friends in an array) - $UsersFriendInformation = $classMapper->createInstance('user')// raw select doesnt work with instance - ->where('id', $UsersFriendId->id) - ->get(); - - $ImagesFromFriends = DB::table('image_posts') - ->where('UserID', '=', $UsersFriendInformation[0]->id) // IMAGES FROM FRIENDS - ->orWhere('UserId', '=', $user->id) // IMAGES FROM THE USER HIMSELF - ->select('PostID as image_id', 'UserID as user_id') - ->get(); - - foreach ($ImagesFromFriends as $ImageFromFriend) { - $ImageFromFriend->image_url = $config["site.uri.public"] . "/image/" . $ImageFromFriend->image_id; - if ($ImageFromFriend->user_id == $user->id) { // UPLOADED FROM HIMSELF - $ImageFromFriend->full_name = $user->full_name; // ADD USERNAME TO IMAGE ID - $ImageFromFriend->avatar = $user->avatar; - } else { // UPLOADED FROM ANOTHER USER - $ImageFromFriend->full_name = $UsersFriendInformation[0]->full_name; // ADD USERNAME TO IMAGE ID - $ImageFromFriend->avatar = $UsersFriendInformation[0]->avatar; - } - } - } - - return $response->withJson($ImagesFromFriends, 200, JSON_PRETTY_PRINT); - } - - /** - * Shows the requested image - * - * @param Request $request - * @param Response $response - * @param $args - * @return Response - * @throws ForbiddenException - * @throws NotFoundException - */ - public function showImage(Request $request, Response $response, $args) { - // check if user is authorized - $authorizer = $this->ci->authorizer; - $currentUser = $this->ci->currentUser; - if (!$authorizer->checkAccess($currentUser, 'view_image')) { - throw new ForbiddenException(); - } - $postID = $args['post_id']; - - // get filename from database - $FileRequestedImage = DB::table('image_posts') - ->where('PostID', '=', $postID) - ->value('File'); - - if ($FileRequestedImage) { - $FileType = pathinfo($FileRequestedImage, PATHINFO_EXTENSION); - - // echo image - $response->write(file_get_contents(__DIR__ . '/../../../../../uploads/' . $FileRequestedImage)); - return $response->withHeader('Content-type', 'image/' . $FileType); - } else { - throw new NotFoundException(); - } - } - - /** - * posts a image - * - * @param Request $request - * @param Response $response - * @return Response - * @throws ForbiddenException - */ - public function postImage(Request $request, Response $response) { - // check if user is authorized - $authorizer = $this->ci->authorizer; - $currentUser = $this->ci->currentUser; - if (!$authorizer->checkAccess($currentUser, 'post_image')) { - throw new ForbiddenException(); - } - - $uploadedFiles = $request->getUploadedFiles(); - $uploadedFile = $uploadedFiles['image']; - - if (!strpos($uploadedFile->getClientMediaType(), "mage")) { - return $response->withStatus(415); - } else if ($uploadedFile->getError() === 1) { - return $response->withStatus(406); - } else if ($uploadedFile->getSize() > 10485760) { - return $response->withStatus(413); - } else { // Upload is accepted - // Move file to upload directory - $extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION); - $basename = bin2hex(random_bytes(8)); - $filename = sprintf('%s.%0.8s', $basename, $extension); - $uploadedFile->moveTo(__DIR__ . '/../../../../../uploads' . DIRECTORY_SEPARATOR . $filename); - - // Store in Database - DB::table('image_posts') - ->insert(['UserID' => $currentUser->id, 'File' => $filename]); - - return $response->write('Uploaded successfully!
'); - } - } - - /** - * @param $params - * @return mixed - * @throws BadRequestException - */ - 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)) { - $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; - } -} +getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException(); + } + + // Get friends first // TODO: Make friend select query more efficient + $UsersFriends = DB::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1"); + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + $ImagesFromFriends = ""; + $config = $this->ci->config; + foreach ($UsersFriends as $UsersFriendId) { // NOT THAT EFFICIENT... (get images from all friends in an array) + $UsersFriendInformation = $classMapper->createInstance('user')// raw select doesnt work with instance + ->where('id', $UsersFriendId->id) + ->get(); + + $ImagesFromFriends = DB::table('image_posts') + ->where('UserID', '=', $UsersFriendInformation[0]->id) // IMAGES FROM FRIENDS + ->orWhere('UserId', '=', $user->id) // IMAGES FROM THE USER HIMSELF + ->select('PostID as image_id', 'UserID as user_id') + ->get(); + + foreach ($ImagesFromFriends as $ImageFromFriend) { + $ImageFromFriend->image_url = $config["site.uri.public"] . "/image/" . $ImageFromFriend->image_id; + if ($ImageFromFriend->user_id == $user->id) { // UPLOADED FROM HIMSELF + $ImageFromFriend->full_name = $user->full_name; // ADD USERNAME TO IMAGE ID + $ImageFromFriend->avatar = $user->avatar; + } else { // UPLOADED FROM ANOTHER USER + $ImageFromFriend->full_name = $UsersFriendInformation[0]->full_name; // ADD USERNAME TO IMAGE ID + $ImageFromFriend->avatar = $UsersFriendInformation[0]->avatar; + } + } + } + + return $response->withJson($ImagesFromFriends, 200, JSON_PRETTY_PRINT); + } + + /** + * Shows the requested image + * + * @param Request $request + * @param Response $response + * @param $args + * @return Response + * @throws ForbiddenException + * @throws NotFoundException + */ + public function showImage(Request $request, Response $response, $args) { + // check if user is authorized + $authorizer = $this->ci->authorizer; + $currentUser = $this->ci->currentUser; + if (!$authorizer->checkAccess($currentUser, 'view_image')) { + throw new ForbiddenException(); + } + $postID = $args['post_id']; + + // get filename from database + $FileRequestedImage = DB::table('image_posts') + ->where('PostID', '=', $postID) + ->value('File'); + + if ($FileRequestedImage) { + $FileType = pathinfo($FileRequestedImage, PATHINFO_EXTENSION); + + // echo image + $response->write(file_get_contents(__DIR__ . '/../../../../../uploads/' . $FileRequestedImage)); + return $response->withHeader('Content-type', 'image/' . $FileType); + } else { + throw new NotFoundException(); + } + } + + /** + * posts a image + * + * @param Request $request + * @param Response $response + * @return Response + * @throws ForbiddenException + */ + public function postImage(Request $request, Response $response) { + // check if user is authorized + $authorizer = $this->ci->authorizer; + $currentUser = $this->ci->currentUser; + if (!$authorizer->checkAccess($currentUser, 'post_image')) { + throw new ForbiddenException(); + } + + $uploadedFiles = $request->getUploadedFiles(); + $uploadedFile = $uploadedFiles['image']; + + if (!strpos($uploadedFile->getClientMediaType(), "mage")) { + return $response->withStatus(415); + } else if ($uploadedFile->getError() === 1) { + return $response->withStatus(406); + } else if ($uploadedFile->getSize() > 10485760) { + return $response->withStatus(413); + } else { // Upload is accepted + // Move file to upload directory + $extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION); + $basename = bin2hex(random_bytes(8)); + $filename = sprintf('%s.%0.8s', $basename, $extension); + $uploadedFile->moveTo(__DIR__ . '/../../../../../uploads' . DIRECTORY_SEPARATOR . $filename); + + // Store in Database + DB::table('image_posts') + ->insert(['UserID' => $currentUser->id, 'File' => $filename]); + + return $response->write('Uploaded successfully!
'); + } + } + + /** + * @param $params + * @return mixed + * @throws BadRequestException + */ + 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)) { + $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/Controller/RoleController.php b/main/app/sprinkles/admin/src/Controller/RoleController.php index 80ac6a0..af1d53c 100644 --- a/main/app/sprinkles/admin/src/Controller/RoleController.php +++ b/main/app/sprinkles/admin/src/Controller/RoleController.php @@ -1,915 +1,915 @@ -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)) { - $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)) { - // 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; - } -} +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)) { + $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)) { + // 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/SearchController.php b/main/app/sprinkles/admin/src/Controller/SearchController.php index 2398a56..612b886 100644 --- a/main/app/sprinkles/admin/src/Controller/SearchController.php +++ b/main/app/sprinkles/admin/src/Controller/SearchController.php @@ -1,56 +1,56 @@ -ci->classMapper; - $users = $classMapper->createInstance('user') - ->where("first_name", "like", "%" . $args["search_term"] . "%") - ->orWhere("last_name", "like", "%" . $args["search_term"] . "%") - ->orWhere(DB::raw("CONCAT(`first_name`, ' ', `last_name`)"), 'LIKE', "%" . $args["search_term"] . "%") - ->orWhere("user_name", "like", "%" . $args["search_term"] . "%") - ->get(); - - foreach ($users as $number => $user) { - $users[$number]["avatar"] = $user->avatar; - } - - if (count($users) === 0) throw new NotFoundException(); - return $response->withJson($users, 200, JSON_PRETTY_PRINT); - } -} +ci->classMapper; + $users = $classMapper->createInstance('user') + ->where("first_name", "like", "%" . $args["search_term"] . "%") + ->orWhere("last_name", "like", "%" . $args["search_term"] . "%") + ->orWhere(DB::raw("CONCAT(`first_name`, ' ', `last_name`)"), 'LIKE', "%" . $args["search_term"] . "%") + ->orWhere("user_name", "like", "%" . $args["search_term"] . "%") + ->get(); + + foreach ($users as $number => $user) { + $users[$number]["avatar"] = $user->avatar; + } + + if (count($users) === 0) throw new NotFoundException(); + return $response->withJson($users, 200, JSON_PRETTY_PRINT); + } +} diff --git a/main/app/sprinkles/admin/src/Controller/UserController.php b/main/app/sprinkles/admin/src/Controller/UserController.php index 45f9919..d66f2f1 100644 --- a/main/app/sprinkles/admin/src/Controller/UserController.php +++ b/main/app/sprinkles/admin/src/Controller/UserController.php @@ -1,1516 +1,1516 @@ -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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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); - } - - - /** - * Sets the users public key - * Request type: POST - * @throws NotFoundException - * @throws BadRequestException - * @throws ForbiddenException - */ - public function setPublicKey($request, $response, $args) { - $requestedUser = $this->getUserFromParams($args); - - if (!$requestedUser) { - throw new NotFoundException($request, $response); - } - - $PublicKey = $request->getParsedBody()["PublicKey"]; - - if ($this->ci->currentUser->id === $requestedUser->id && (Capsule::table('public_keys') - ->where('user_id', "=", $requestedUser->id) - ->exists()) === FALSE) { - Capsule::table('public_keys') - ->insert(['user_id' => $requestedUser->id, 'key' => substr(substr($PublicKey, 100), 0, -40)]); - return $response->withStatus(200); - } else if ($this->ci->currentUser->id === $requestedUser->id) { - Capsule::table('public_keys') - ->where('user_id', $requestedUser->id) - ->update(['key' => substr(substr($PublicKey, 100), 0, -40)]); - return $response->withStatus(200); - } else { - throw new ForbiddenException(); - } - } - - /** - * 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 - * @throws BadRequestException - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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(); - $result["avatar"] = $user->avatar; - - // 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 - * @throws ForbiddenException - */ - 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 - * @throws BadRequestException - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - */ - 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 - $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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws NotFoundException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - * @throws BadRequestException - */ - 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 - * @throws ForbiddenException - */ - 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'); - } - - /** - * Gets the users public key - * Request type: GET - * @throws NotFoundException - * @throws BadRequestException - */ - public function getPublicKey($request, $response, $args) { - $requestedUser = $this->getUserFromParams($args); - - if (!$requestedUser) { - throw new NotFoundException($request, $response); - } - - if ((Capsule::table('public_keys') - ->where('user_id', "=", $requestedUser->id) - ->exists())) { - - $RawPublicKey = Capsule::table('public_keys') - ->where('user_id', "=", $requestedUser->id) - ->value('key'); - $PublicKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: OpenPGP.js v3.0.9\nComment: https://openpgpjs.org\n\n" . $RawPublicKey . "\n-----END PGP PUBLIC KEY BLOCK-----"; - - $ContentType = explode(',', $request->getHeaderLine('Accept'))[0]; - switch ($ContentType) { - case 'application/json': - $response->write(json_encode(array('user_id' => $requestedUser->id, 'PublicKey' => $PublicKey))); - break; - case 'text/html': - $response->write("
" . $PublicKey);
-                    break;
-                default:
-                    $response->write($PublicKey);
-            }
-            return $response->withStatus(200);
-        } else {
-            throw new NotFoundException($request, $response);
-        }
-    }
-
-    /**
-     * Gets the users which are following the requested user
-     * Request type: GET
-     * @throws ForbiddenException
-     * @throws NotFoundException
-     * @throws BadRequestException
-     */
-    public function getFollowers($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, 'uri_user', [
-            'user' => $user
-        ])) {
-            throw new ForbiddenException();
-        }
-
-        $UsersFollowers = Capsule::table('user_follow')
-            ->where('user_id', "=", $user->id)
-            ->join("users", "users.id", "=", "user_follow.followed_by_id")
-            ->select("user_follow.followed_by_id as id", "users.user_name as username")
-            ->get();
-
-        $result = $UsersFollowers->toArray();
-
-        return $response->withJson($result, 200, JSON_PRETTY_PRINT);
-    }
-
-    /**
-     * Get users which the user follows
-     * Request type: GET
-     * @throws ForbiddenException
-     * @throws NotFoundException
-     * @throws BadRequestException
-     */
-    public function getFollows($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 (($user->id == $currentUser->id) || (!$authorizer->checkAccess($currentUser, 'uri_user', [
-                'user' => $user
-            ]))) {
-            throw new ForbiddenException();
-        }
-
-        $UsersFollows = Capsule::table('user_follow')
-            ->where('followed_by_id', "=", $user->id)
-            ->join("users", "users.id", "=", "user_follow.user_id")
-            ->select("user_follow.user_id as id", "users.user_name as username")
-            ->get();
-
-        $result = $UsersFollows->toArray();
-
-        return $response->withJson($result, 200, JSON_PRETTY_PRINT);
-    }
-
-    /**
-     * Get users which the user follows and which are following the user
-     * Request type: GET
-     * @throws NotFoundException
-     * @throws ForbiddenException
-     * @throws BadRequestException
-     */
-    public function getFriends($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, 'uri_user', [
-            'user' => $user
-        ])) {
-            throw new ForbiddenException();
-        }
-
-        $UsersFriends = Capsule::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1");
-
-        /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
-        $classMapper = $this->ci->classMapper;
-
-        foreach ($UsersFriends as $Key => $UsersFriendId) { // NOT THAT EFFICIENT...
-            $UsersFriendInformation = $classMapper->createInstance('user')// raw select doesnt work with instance
-            ->where('id', $UsersFriendId->id)
-                ->get();
-
-            $UsersFriends[$Key]->id = $UsersFriendInformation[0]->id;
-            $UsersFriends[$Key]->username = $UsersFriendInformation[0]->user_name;
-            $UsersFriends[$Key]->avatar = $UsersFriendInformation[0]->avatar;
-            $UsersFriends[$Key]->full_name = $UsersFriendInformation[0]->full_name;
-        }
-
-        $result = $UsersFriends;
-
-        if (sizeof($result) > 0) { // USER HAS FRIENDS
-            return $response->withJson($result, 200, JSON_PRETTY_PRINT);
-        } else {
-            throw new NotFoundException($request, $response);
-        }
-    }
-
-
-    /**
-     * 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
-     * @throws NotFoundException
-     * @throws ForbiddenException
-     * @throws BadRequestException
-     * @throws BadRequestException
-     */
-    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';
-            } else if ($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
-     * @throws ForbiddenException
-     * @throws BadRequestException
-     * @throws BadRequestException
-     * @throws BadRequestException
-     * @throws BadRequestException
-     * @throws BadRequestException
-     * @throws NotFoundException
-     * @throws BadRequestException
-     */
-    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)) {
-            // 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;
-            } else if (
-                ($user->id == $currentUser->id) &&
-                ($fieldValue == '0')
-            ) {
-                $e = new BadRequestException();
-                $e->addUserMessage('DISABLE_SELF');
-                throw $e;
-            }
-        } else if ($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
-                ]);
-            }
-        } else if ($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)) {
-            $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;
-    }
-}
+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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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);
+    }
+
+
+    /**
+     * Sets the users public key
+     * Request type: POST
+     * @throws NotFoundException
+     * @throws BadRequestException
+     * @throws ForbiddenException
+     */
+    public function setPublicKey($request, $response, $args) {
+        $requestedUser = $this->getUserFromParams($args);
+
+        if (!$requestedUser) {
+            throw new NotFoundException($request, $response);
+        }
+
+        $PublicKey = $request->getParsedBody()["PublicKey"];
+
+        if ($this->ci->currentUser->id === $requestedUser->id && (Capsule::table('public_keys')
+                ->where('user_id', "=", $requestedUser->id)
+                ->exists()) === FALSE) {
+            Capsule::table('public_keys')
+                ->insert(['user_id' => $requestedUser->id, 'key' => substr(substr($PublicKey, 100), 0, -40)]);
+            return $response->withStatus(200);
+        } else if ($this->ci->currentUser->id === $requestedUser->id) {
+            Capsule::table('public_keys')
+                ->where('user_id', $requestedUser->id)
+                ->update(['key' => substr(substr($PublicKey, 100), 0, -40)]);
+            return $response->withStatus(200);
+        } else {
+            throw new ForbiddenException();
+        }
+    }
+
+    /**
+     * 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
+     * @throws BadRequestException
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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();
+        $result["avatar"] = $user->avatar;
+
+        // 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
+     * @throws ForbiddenException
+     */
+    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
+     * @throws BadRequestException
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     */
+    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
+        $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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     * @throws BadRequestException
+     */
+    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
+     * @throws ForbiddenException
+     */
+    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');
+    }
+
+    /**
+     * Gets the users public key
+     * Request type: GET
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    public function getPublicKey($request, $response, $args) {
+        $requestedUser = $this->getUserFromParams($args);
+
+        if (!$requestedUser) {
+            throw new NotFoundException($request, $response);
+        }
+
+        if ((Capsule::table('public_keys')
+            ->where('user_id', "=", $requestedUser->id)
+            ->exists())) {
+
+            $RawPublicKey = Capsule::table('public_keys')
+                ->where('user_id', "=", $requestedUser->id)
+                ->value('key');
+            $PublicKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: OpenPGP.js v3.0.9\nComment: https://openpgpjs.org\n\n" . $RawPublicKey . "\n-----END PGP PUBLIC KEY BLOCK-----";
+
+            $ContentType = explode(',', $request->getHeaderLine('Accept'))[0];
+            switch ($ContentType) {
+                case 'application/json':
+                    $response->write(json_encode(array('user_id' => $requestedUser->id, 'PublicKey' => $PublicKey)));
+                    break;
+                case 'text/html':
+                    $response->write("
" . $PublicKey);
+                    break;
+                default:
+                    $response->write($PublicKey);
+            }
+            return $response->withStatus(200);
+        } else {
+            throw new NotFoundException($request, $response);
+        }
+    }
+
+    /**
+     * Gets the users which are following the requested user
+     * Request type: GET
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    public function getFollowers($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, 'uri_user', [
+            'user' => $user
+        ])) {
+            throw new ForbiddenException();
+        }
+
+        $UsersFollowers = Capsule::table('user_follow')
+            ->where('user_id', "=", $user->id)
+            ->join("users", "users.id", "=", "user_follow.followed_by_id")
+            ->select("user_follow.followed_by_id as id", "users.user_name as username")
+            ->get();
+
+        $result = $UsersFollowers->toArray();
+
+        return $response->withJson($result, 200, JSON_PRETTY_PRINT);
+    }
+
+    /**
+     * Get users which the user follows
+     * Request type: GET
+     * @throws ForbiddenException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    public function getFollows($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 (($user->id == $currentUser->id) || (!$authorizer->checkAccess($currentUser, 'uri_user', [
+                'user' => $user
+            ]))) {
+            throw new ForbiddenException();
+        }
+
+        $UsersFollows = Capsule::table('user_follow')
+            ->where('followed_by_id', "=", $user->id)
+            ->join("users", "users.id", "=", "user_follow.user_id")
+            ->select("user_follow.user_id as id", "users.user_name as username")
+            ->get();
+
+        $result = $UsersFollows->toArray();
+
+        return $response->withJson($result, 200, JSON_PRETTY_PRINT);
+    }
+
+    /**
+     * Get users which the user follows and which are following the user
+     * Request type: GET
+     * @throws NotFoundException
+     * @throws ForbiddenException
+     * @throws BadRequestException
+     */
+    public function getFriends($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, 'uri_user', [
+            'user' => $user
+        ])) {
+            throw new ForbiddenException();
+        }
+
+        $UsersFriends = Capsule::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1");
+
+        /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+        $classMapper = $this->ci->classMapper;
+
+        foreach ($UsersFriends as $Key => $UsersFriendId) { // NOT THAT EFFICIENT...
+            $UsersFriendInformation = $classMapper->createInstance('user')// raw select doesnt work with instance
+            ->where('id', $UsersFriendId->id)
+                ->get();
+
+            $UsersFriends[$Key]->id = $UsersFriendInformation[0]->id;
+            $UsersFriends[$Key]->username = $UsersFriendInformation[0]->user_name;
+            $UsersFriends[$Key]->avatar = $UsersFriendInformation[0]->avatar;
+            $UsersFriends[$Key]->full_name = $UsersFriendInformation[0]->full_name;
+        }
+
+        $result = $UsersFriends;
+
+        if (sizeof($result) > 0) { // USER HAS FRIENDS
+            return $response->withJson($result, 200, JSON_PRETTY_PRINT);
+        } else {
+            throw new NotFoundException($request, $response);
+        }
+    }
+
+
+    /**
+     * 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
+     * @throws NotFoundException
+     * @throws ForbiddenException
+     * @throws BadRequestException
+     * @throws BadRequestException
+     */
+    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';
+            } else if ($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
+     * @throws ForbiddenException
+     * @throws BadRequestException
+     * @throws BadRequestException
+     * @throws BadRequestException
+     * @throws BadRequestException
+     * @throws BadRequestException
+     * @throws NotFoundException
+     * @throws BadRequestException
+     */
+    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)) {
+            // 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;
+            } else if (
+                ($user->id == $currentUser->id) &&
+                ($fieldValue == '0')
+            ) {
+                $e = new BadRequestException();
+                $e->addUserMessage('DISABLE_SELF');
+                throw $e;
+            }
+        } else if ($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
+                ]);
+            }
+        } else if ($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)) {
+            $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/Controller/WormholeController.php b/main/app/sprinkles/admin/src/Controller/WormholeController.php
index ec33f3e..e25f890 100644
--- a/main/app/sprinkles/admin/src/Controller/WormholeController.php
+++ b/main/app/sprinkles/admin/src/Controller/WormholeController.php
@@ -1,147 +1,147 @@
-verifyAccessToken($args)) {
-            $user_id = $args['user_id'];
-            $session_id = $args['session_id'];
-            $session_file = file_get_contents("../app/sessions/" . $session_id);
-            $session_user_id = unserialize(substr($session_file, strpos($session_file, "account|") + 8))["current_user_id"];
-            if ($session_user_id == $user_id) {
-                return $response->withStatus(200);
-            } else {
-                throw new NotFoundException();
-            }
-        }
-    }
-
-    /**
-     * @param $request
-     * @param Response $response
-     * @param $args
-     * @return Response
-     * @throws BadRequestException
-     * @throws NotFoundException
-     */
-    public function newMessage($request, Response $response, $args) {
-        if ($this->verifyAccessToken($args)) {
-            $sender_id = $args['sender_id'];
-            $receiver_id = $args['receiver_id'];
-            $message = $request->getParsedBody()["message"];
-            if (($sender_id != $receiver_id) && $message) {
-                $MessageId = DB::table('chat_messages')
-                    ->insertGetId(['sender_id' => $sender_id, 'receiver_id' => $receiver_id, 'message' => $message], 'message_id');
-                $response->write($MessageId);
-                return $response->withStatus(200);
-            } else {
-                throw new BadRequestException();
-            }
-        }
-    }
-
-    /**
-     * @param Request $request
-     * @param Response $response
-     * @param $args
-     * @return Response
-     * @throws NotFoundException
-     */
-    public function getInfo(Request $request, Response $response, $args) {
-        /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
-        $classMapper = $this->ci->classMapper;
-
-        if ($this->verifyAccessToken($args)) {
-            $user = $classMapper->staticMethod('user', 'where', 'id', $args['user_id'])
-                ->first();
-            if (!$user) {
-                throw new NotFoundException($request, $response);
-            }
-
-            $UsersFollower = DB::table('user_follow')
-                ->where('user_id', $user->id)
-                ->join("users", "users.id", "=", "user_follow.followed_by_id")
-                ->select("user_follow.followed_by_id as id", "users.user_name as username")
-                ->get();
-
-            $UsersFollows = DB::table('user_follow')
-                ->where('followed_by_id', $user->id)
-                ->join("users", "users.id", "=", "user_follow.user_id")
-                ->select("user_follow.user_id as id", "users.user_name as username")
-                ->get();
-
-            $UsersFriends = DB::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1");
-            /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
-            $classMapper = $this->ci->classMapper;
-            foreach ($UsersFriends as $Key => $UsersFriendId) { // NOT THAT EFFICIENT...
-                $UsersFriendInformation = $classMapper->createInstance('user')// select doesnt work with instance
-                    ->where('id', $UsersFriendId->id)
-                    ->get();
-                $UsersFriends[$Key]->id = $UsersFriendInformation[0]->id;
-                $UsersFriends[$Key]->username = $UsersFriendInformation[0]->user_name;
-                $UsersFriends[$Key]->avatar = $UsersFriendInformation[0]->avatar;
-                $UsersFriends[$Key]->full_name = $UsersFriendInformation[0]->full_name;
-            }
-
-            $result = $user->toArray();
-            $result["avatar"] = $user->avatar;
-            $result["followers"] = $UsersFollower;
-            $result["follows"] = $UsersFollows;
-            $result["friends"] = $UsersFriends;
-            return $response->withJson($result, 200, JSON_PRETTY_PRINT);
-        }
-    }
-
-    /**
-     * @param $args
-     * @return bool
-     * @throws NotFoundException
-     */
-    private function verifyAccessToken($args) {
-        $currentUser = $this->ci->currentUser; // FOR DATABASE QUERY
-        $access_token = $args['access_token'];
-        if (DB::table('access_token')
-            ->where('id', 1)
-            ->where('token', '=', $access_token)
-            ->exists()) {
-            return TRUE;
-        } else {
-            throw new NotFoundException(); // IT'S A FORBIDDEN
-        }
-    }
+verifyAccessToken($args)) {
+            $user_id = $args['user_id'];
+            $session_id = $args['session_id'];
+            $session_file = file_get_contents("../app/sessions/" . $session_id);
+            $session_user_id = unserialize(substr($session_file, strpos($session_file, "account|") + 8))["current_user_id"];
+            if ($session_user_id == $user_id) {
+                return $response->withStatus(200);
+            } else {
+                throw new NotFoundException();
+            }
+        }
+    }
+
+    /**
+     * @param $request
+     * @param Response $response
+     * @param $args
+     * @return Response
+     * @throws BadRequestException
+     * @throws NotFoundException
+     */
+    public function newMessage($request, Response $response, $args) {
+        if ($this->verifyAccessToken($args)) {
+            $sender_id = $args['sender_id'];
+            $receiver_id = $args['receiver_id'];
+            $message = $request->getParsedBody()["message"];
+            if (($sender_id != $receiver_id) && $message) {
+                $MessageId = DB::table('chat_messages')
+                    ->insertGetId(['sender_id' => $sender_id, 'receiver_id' => $receiver_id, 'message' => $message], 'message_id');
+                $response->write($MessageId);
+                return $response->withStatus(200);
+            } else {
+                throw new BadRequestException();
+            }
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @param Response $response
+     * @param $args
+     * @return Response
+     * @throws NotFoundException
+     */
+    public function getInfo(Request $request, Response $response, $args) {
+        /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+        $classMapper = $this->ci->classMapper;
+
+        if ($this->verifyAccessToken($args)) {
+            $user = $classMapper->staticMethod('user', 'where', 'id', $args['user_id'])
+                ->first();
+            if (!$user) {
+                throw new NotFoundException($request, $response);
+            }
+
+            $UsersFollower = DB::table('user_follow')
+                ->where('user_id', $user->id)
+                ->join("users", "users.id", "=", "user_follow.followed_by_id")
+                ->select("user_follow.followed_by_id as id", "users.user_name as username")
+                ->get();
+
+            $UsersFollows = DB::table('user_follow')
+                ->where('followed_by_id', $user->id)
+                ->join("users", "users.id", "=", "user_follow.user_id")
+                ->select("user_follow.user_id as id", "users.user_name as username")
+                ->get();
+
+            $UsersFriends = DB::select("SELECT id FROM (SELECT user_id AS id FROM user_follow WHERE followed_by_id = $user->id UNION ALL SELECT followed_by_id FROM user_follow WHERE user_id = $user->id) t GROUP BY id HAVING COUNT(id) > 1");
+            /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */
+            $classMapper = $this->ci->classMapper;
+            foreach ($UsersFriends as $Key => $UsersFriendId) { // NOT THAT EFFICIENT...
+                $UsersFriendInformation = $classMapper->createInstance('user')// select doesnt work with instance
+                    ->where('id', $UsersFriendId->id)
+                    ->get();
+                $UsersFriends[$Key]->id = $UsersFriendInformation[0]->id;
+                $UsersFriends[$Key]->username = $UsersFriendInformation[0]->user_name;
+                $UsersFriends[$Key]->avatar = $UsersFriendInformation[0]->avatar;
+                $UsersFriends[$Key]->full_name = $UsersFriendInformation[0]->full_name;
+            }
+
+            $result = $user->toArray();
+            $result["avatar"] = $user->avatar;
+            $result["followers"] = $UsersFollower;
+            $result["follows"] = $UsersFollows;
+            $result["friends"] = $UsersFriends;
+            return $response->withJson($result, 200, JSON_PRETTY_PRINT);
+        }
+    }
+
+    /**
+     * @param $args
+     * @return bool
+     * @throws NotFoundException
+     */
+    private function verifyAccessToken($args) {
+        $currentUser = $this->ci->currentUser; // FOR DATABASE QUERY
+        $access_token = $args['access_token'];
+        if (DB::table('access_token')
+            ->where('id', 1)
+            ->where('token', '=', $access_token)
+            ->exists()) {
+            return TRUE;
+        } else {
+            throw new NotFoundException(); // IT'S A FORBIDDEN
+        }
+    }
 }
\ No newline at end of file
-- 
cgit v1.2.3