diff --git a/composer.json b/composer.json index ba94377..f8567a1 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "type": "userfrosting-worker", "description": "A sprinkle to add a worker thread.", "require": { - "symfony/lock": "*" + "symfony/lock": "*", + "twig/intl-extra": "^3.6" }, "autoload": { "files" : [ diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php new file mode 100644 index 0000000..8d158a3 --- /dev/null +++ b/locale/en_US/messages.php @@ -0,0 +1,25 @@ + [ + 1 => 'Job', + 2 => 'Jobs', + + 'NAME' => 'Job name', + 'PAGE_DESCRIPTION' => 'This page shows a list of programmed jobs that the UserFrosting Worker sprinkle knows about.', + ], + + 'SPRINKLE' => 'Sprinkle', +]; diff --git a/routes/jobs.php b/routes/jobs.php new file mode 100644 index 0000000..262b5fd --- /dev/null +++ b/routes/jobs.php @@ -0,0 +1,22 @@ +group('/jobs', function () { + $this->get('', 'UserFrosting\Sprinkle\Worker\Controller\JobController:pageList') + ->setName('uri_permissions'); +})->add('authGuard')->add(new NoCache()); + +$app->group('/api/jobs', function () { + $this->get('', 'UserFrosting\Sprinkle\Worker\Controller\JobController:getList'); +})->add('authGuard')->add(new NoCache()); diff --git a/src/Controller/JobController.php b/src/Controller/JobController.php new file mode 100644 index 0000000..bf2bc1c --- /dev/null +++ b/src/Controller/JobController.php @@ -0,0 +1,102 @@ +getQueryParams(); + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_jobs')) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Jobs\JobInspector $jobInspector */ + $jobInspector = $this->ci->jobInspector; + + $jobs = $jobInspector->getAvailableJobDefinitions(); + + // 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($jobs, 200, JSON_PRETTY_PRINT); + } + + /** + * Renders the job listing page. + * + * This page renders a table of jobs, with dropdown menus for admin actions for each job. + * Actions typically include: run job + * This page requires authentication. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws ForbiddenException If user is not authorized to access page + */ + public function pageList(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_jobs')) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Jobs\JobInspector $jobInspector */ + $jobInspector = $this->ci->jobInspector; + + $jobs = $jobInspector->getAvailableJobDefinitions(); + + return $this->ci->view->render($response, 'pages/jobs.html.twig', [ + 'jobs' => $jobs + ]); + } +} \ No newline at end of file diff --git a/src/Database/Seeds/WorkerPermissions.php b/src/Database/Seeds/WorkerPermissions.php new file mode 100644 index 0000000..534ff11 --- /dev/null +++ b/src/Database/Seeds/WorkerPermissions.php @@ -0,0 +1,95 @@ +getPermissions(); + $this->savePermissions($permissions); + + // Add default mappings to permissions + $this->syncPermissionsRole($permissions); + } + + /** + * @return array Permissions to seed + */ + protected function getPermissions() + { + return [ + 'uri_job' => new Permission([ + 'slug' => 'uri_job', + 'name' => 'View job', + 'conditions' => 'always()', + 'description' => 'View the job page of any job.', + ]), + 'uri_jobs' => new Permission([ + 'slug' => 'uri_jobs', + 'name' => 'Jobs management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a table of jobs.', + ]), + ]; + } + + /** + * Save permissions. + * + * @param array $permissions + */ + protected function savePermissions(array &$permissions) + { + foreach ($permissions as $slug => $permission) { + // Trying to find if the permission already exist + $existingPermission = Permission::where(['slug' => $permission->slug, 'conditions' => $permission->conditions])->first(); + + // Don't save if already exist, use existing permission reference + // otherwise to re-sync permissions and roles + if ($existingPermission == null) { + $permission->save(); + } else { + $permissions[$slug] = $existingPermission; + } + } + } + + /** + * Sync permissions with default roles. + * + * @param array $permissions + */ + protected function syncPermissionsRole(array $permissions) + { + $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); + if ($roleSiteAdmin) { + $roleSiteAdmin->permissions()->syncWithoutDetaching([ + $permissions['uri_jobs']->id, + $permissions['uri_job']->id, + ]); + } + } +} diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php index d6998a3..d76ecf1 100644 --- a/src/ServicesProvider/ServicesProvider.php +++ b/src/ServicesProvider/ServicesProvider.php @@ -11,8 +11,10 @@ namespace UserFrosting\Sprinkle\Worker\ServicesProvider; use Illuminate\Container\Container; use Psr\Container\ContainerInterface; +use Twig\Extra\Intl\IntlExtension; use UserFrosting\Sprinkle\Worker\Worker\Worker; use UserFrosting\Sprinkle\Core\ServicesProvider\BaseServicesProvider; +use UserFrosting\Sprinkle\Worker\Jobs\JobInspector; /** * Worker services provider. @@ -49,5 +51,20 @@ class ServicesProvider $container['jobInspector'] = function ($c) { return new JobInspector($c); }; + + /* + * Extends the 'view' service with the HasRole extension for Twig. + * + * @return \Slim\Views\Twig + */ + $container->extend('view', function ($view, $c) { + $twig = $view->getEnvironment(); + + if (!$twig->hasExtension('Twig\Extra\Intl\IntlExtension')) { + $twig->addExtension(new IntlExtension()); + } + + return $view; + }); } } diff --git a/templates/navigation/sidebar-menu.html.twig b/templates/navigation/sidebar-menu.html.twig new file mode 100644 index 0000000..c690445 --- /dev/null +++ b/templates/navigation/sidebar-menu.html.twig @@ -0,0 +1,10 @@ +{% extends "@admin/navigation/sidebar-menu.html.twig" %} + +{% block navigation %} + {{ parent() }} + {% if checkAccess('uri_jobs') %} +
  • + {{ translate("JOB", 2) }} +
  • + {% endif %} +{% endblock %} diff --git a/templates/pages/jobs.html.twig b/templates/pages/jobs.html.twig new file mode 100644 index 0000000..b4c6ad8 --- /dev/null +++ b/templates/pages/jobs.html.twig @@ -0,0 +1,41 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("JOB", 2)}}{% endblock %} + +{% block page_description %}{{ translate("JOB.PAGE_DESCRIPTION")}}{% endblock %} + +{% block body_matter %} +
    +
    +
    +
    +

    {{translate('JOB', 2)}}

    +
    +
    + {% include "tables/jobs.html.twig" with { + "table" : { + "id" : "table-jobs" + } + } + %} +
    +
    +
    +
    +{% endblock %} +{% block scripts_page %} + + + + + {{ assets.js('js/form-widgets') | raw }} + +{% endblock %} diff --git a/templates/tables/jobs.html.twig b/templates/tables/jobs.html.twig new file mode 100644 index 0000000..d894b52 --- /dev/null +++ b/templates/tables/jobs.html.twig @@ -0,0 +1,39 @@ +{# This partial template renders a table of permissions, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + + + + + + + + + {% for job in jobs %} + + + + + + {% endfor %} + +
    {{translate('NAME')}}{{translate('SPRINKLE')}}
    +
    + + + {{job.name}} + +
    +
    + {{job.sprinkle}} +
    +{% endblock %} + +{% block table_pager_controls %}{% endblock %} \ No newline at end of file