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