feat(api-docs): extract API docs viewer into standalone module
Move the Swagger UI viewer from core (pages/admin/api-docs/) into modules/api-docs/ as a self-contained module. Create module manifest with routes, permission, sidebar.admin_nav_item slot, asset group, and authorization policy. Remove API docs ability from core OperationsAuthorizationPolicy and UiCapabilityMap. Remove hardcoded sidebar nav item — now contributed via module slot. Add admin-automation group meta to sidebar template for module nav items. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,9 +58,6 @@ return [
|
|||||||
'page-layout' => [
|
'page-layout' => [
|
||||||
'css/layout/app-page-layout.css',
|
'css/layout/app-page-layout.css',
|
||||||
],
|
],
|
||||||
'api-docs' => [
|
|
||||||
'css/vendor-overrides/swagger-ui.css',
|
|
||||||
],
|
|
||||||
'docs' => [
|
'docs' => [
|
||||||
'css/pages/app-docs.css',
|
'css/pages/app-docs.css',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -12,5 +12,5 @@
|
|||||||
* Each entry must match a directory name under modules/ containing a module.php manifest.
|
* Each entry must match a directory name under modules/ containing a module.php manifest.
|
||||||
*/
|
*/
|
||||||
return [
|
return [
|
||||||
'enabled_modules' => ['audit', 'addressbook', 'bookmarks', 'notifications'],
|
'enabled_modules' => ['audit', 'addressbook', 'bookmarks', 'notifications', 'api-docs'],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ final class OperationsAuthorizationPolicy implements AuthorizationPolicyInterfac
|
|||||||
public const ABILITY_ADMIN_MAIL_LOG_VIEW = 'ops.admin.mail_log.view';
|
public const ABILITY_ADMIN_MAIL_LOG_VIEW = 'ops.admin.mail_log.view';
|
||||||
public const ABILITY_ADMIN_STATS_VIEW = 'ops.admin.stats.view';
|
public const ABILITY_ADMIN_STATS_VIEW = 'ops.admin.stats.view';
|
||||||
public const ABILITY_ADMIN_SYSTEM_INFO_VIEW = 'ops.admin.system_info.view';
|
public const ABILITY_ADMIN_SYSTEM_INFO_VIEW = 'ops.admin.system_info.view';
|
||||||
public const ABILITY_ADMIN_API_DOCS_VIEW = 'ops.admin.api_docs.view';
|
|
||||||
public const ABILITY_ADMIN_IMPORTS_TYPE_USERS = 'ops.admin.imports.type.users';
|
public const ABILITY_ADMIN_IMPORTS_TYPE_USERS = 'ops.admin.imports.type.users';
|
||||||
public const ABILITY_ADMIN_IMPORTS_TYPE_DEPARTMENTS = 'ops.admin.imports.type.departments';
|
public const ABILITY_ADMIN_IMPORTS_TYPE_DEPARTMENTS = 'ops.admin.imports.type.departments';
|
||||||
public const ABILITY_ADMIN_USERS_ASSIGNMENTS_MANAGE = 'ops.admin.users.assignments.manage';
|
public const ABILITY_ADMIN_USERS_ASSIGNMENTS_MANAGE = 'ops.admin.users.assignments.manage';
|
||||||
@@ -51,7 +50,6 @@ final class OperationsAuthorizationPolicy implements AuthorizationPolicyInterfac
|
|||||||
self::ABILITY_ADMIN_MAIL_LOG_VIEW,
|
self::ABILITY_ADMIN_MAIL_LOG_VIEW,
|
||||||
self::ABILITY_ADMIN_STATS_VIEW,
|
self::ABILITY_ADMIN_STATS_VIEW,
|
||||||
self::ABILITY_ADMIN_SYSTEM_INFO_VIEW,
|
self::ABILITY_ADMIN_SYSTEM_INFO_VIEW,
|
||||||
self::ABILITY_ADMIN_API_DOCS_VIEW,
|
|
||||||
self::ABILITY_ADMIN_IMPORTS_TYPE_USERS,
|
self::ABILITY_ADMIN_IMPORTS_TYPE_USERS,
|
||||||
self::ABILITY_ADMIN_IMPORTS_TYPE_DEPARTMENTS,
|
self::ABILITY_ADMIN_IMPORTS_TYPE_DEPARTMENTS,
|
||||||
self::ABILITY_ADMIN_USERS_ASSIGNMENTS_MANAGE,
|
self::ABILITY_ADMIN_USERS_ASSIGNMENTS_MANAGE,
|
||||||
@@ -94,7 +92,6 @@ final class OperationsAuthorizationPolicy implements AuthorizationPolicyInterfac
|
|||||||
self::ABILITY_ADMIN_MAIL_LOG_VIEW => $this->allowIfHas($actorUserId, PermissionService::MAIL_LOG_VIEW),
|
self::ABILITY_ADMIN_MAIL_LOG_VIEW => $this->allowIfHas($actorUserId, PermissionService::MAIL_LOG_VIEW),
|
||||||
self::ABILITY_ADMIN_STATS_VIEW => $this->allowIfHas($actorUserId, PermissionService::STATS_VIEW),
|
self::ABILITY_ADMIN_STATS_VIEW => $this->allowIfHas($actorUserId, PermissionService::STATS_VIEW),
|
||||||
self::ABILITY_ADMIN_SYSTEM_INFO_VIEW => $this->allowIfHas($actorUserId, PermissionService::SYSTEM_INFO_VIEW),
|
self::ABILITY_ADMIN_SYSTEM_INFO_VIEW => $this->allowIfHas($actorUserId, PermissionService::SYSTEM_INFO_VIEW),
|
||||||
self::ABILITY_ADMIN_API_DOCS_VIEW => $this->allowIfHas($actorUserId, PermissionService::API_DOCS_VIEW),
|
|
||||||
self::ABILITY_ADMIN_IMPORTS_TYPE_USERS => $this->allowIfHas($actorUserId, PermissionService::USERS_IMPORT),
|
self::ABILITY_ADMIN_IMPORTS_TYPE_USERS => $this->allowIfHas($actorUserId, PermissionService::USERS_IMPORT),
|
||||||
self::ABILITY_ADMIN_IMPORTS_TYPE_DEPARTMENTS => $this->allowIfHas($actorUserId, PermissionService::DEPARTMENTS_IMPORT),
|
self::ABILITY_ADMIN_IMPORTS_TYPE_DEPARTMENTS => $this->allowIfHas($actorUserId, PermissionService::DEPARTMENTS_IMPORT),
|
||||||
self::ABILITY_ADMIN_USERS_ASSIGNMENTS_MANAGE => $this->allowIfHas($actorUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS),
|
self::ABILITY_ADMIN_USERS_ASSIGNMENTS_MANAGE => $this->allowIfHas($actorUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ final class UiCapabilityMap
|
|||||||
'can_view_settings' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW,
|
'can_view_settings' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW,
|
||||||
'can_view_imports' => OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_VIEW,
|
'can_view_imports' => OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_VIEW,
|
||||||
'can_view_jobs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW,
|
'can_view_jobs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW,
|
||||||
'can_view_api_docs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_API_DOCS_VIEW,
|
|
||||||
'can_view_docs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_DOCS_VIEW,
|
'can_view_docs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_DOCS_VIEW,
|
||||||
'can_view_mail_log' => OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW,
|
'can_view_mail_log' => OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW,
|
||||||
'can_view_stats' => OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW,
|
'can_view_stats' => OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW,
|
||||||
|
|||||||
5
modules/api-docs/i18n/default_de.json
Normal file
5
modules/api-docs/i18n/default_de.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"API docs": "API-Dokumentation",
|
||||||
|
"API documentation": "API-Dokumentation",
|
||||||
|
"Can view API documentation": "Kann API-Dokumentation anzeigen"
|
||||||
|
}
|
||||||
5
modules/api-docs/i18n/default_en.json
Normal file
5
modules/api-docs/i18n/default_en.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"API docs": "API docs",
|
||||||
|
"API documentation": "API documentation",
|
||||||
|
"Can view API documentation": "Can view API documentation"
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\ApiDocs;
|
||||||
|
|
||||||
|
use MintyPHP\Service\Access\AuthorizationDecision;
|
||||||
|
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
|
||||||
|
use MintyPHP\Service\Access\PermissionService;
|
||||||
|
|
||||||
|
final class ApiDocsAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||||
|
{
|
||||||
|
public const ABILITY_VIEW = 'api_docs.view';
|
||||||
|
public const PERMISSION_KEY = 'api_docs.view';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PermissionService $permissionService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(string $ability): bool
|
||||||
|
{
|
||||||
|
return $ability === self::ABILITY_VIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||||
|
{
|
||||||
|
$actorUserId = (int) ($context['actor_user_id'] ?? 0);
|
||||||
|
if ($actorUserId <= 0) {
|
||||||
|
return AuthorizationDecision::deny(403, 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->permissionService->userHas($actorUserId, self::PERMISSION_KEY)) {
|
||||||
|
return AuthorizationDecision::deny(403, 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthorizationDecision::allow();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\ApiDocs;
|
||||||
|
|
||||||
|
use MintyPHP\App\AppContainer;
|
||||||
|
use MintyPHP\App\Container\ContainerRegistrar;
|
||||||
|
use MintyPHP\Service\Access\PermissionService;
|
||||||
|
|
||||||
|
final class ApiDocsContainerRegistrar implements ContainerRegistrar
|
||||||
|
{
|
||||||
|
public function register(AppContainer $container): void
|
||||||
|
{
|
||||||
|
$container->set(ApiDocsAuthorizationPolicy::class, static fn (AppContainer $c): ApiDocsAuthorizationPolicy => new ApiDocsAuthorizationPolicy(
|
||||||
|
$c->get(PermissionService::class)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
66
modules/api-docs/module.php
Normal file
66
modules/api-docs/module.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Docs module manifest.
|
||||||
|
*
|
||||||
|
* Provides a Swagger UI viewer for the project's OpenAPI specification.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'id' => 'api-docs',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'enabled_by_default' => false,
|
||||||
|
'load_order' => 20,
|
||||||
|
'requires' => [],
|
||||||
|
|
||||||
|
'routes' => [
|
||||||
|
['path' => 'admin/api-docs', 'target' => 'api-docs'],
|
||||||
|
['path' => 'admin/api-docs/spec', 'target' => 'api-docs/spec'],
|
||||||
|
],
|
||||||
|
'public_paths' => [],
|
||||||
|
'container_registrars' => [
|
||||||
|
\MintyPHP\Module\ApiDocs\ApiDocsContainerRegistrar::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'ui_slots' => [
|
||||||
|
'sidebar.admin_nav_item' => [
|
||||||
|
[
|
||||||
|
'key' => 'api-docs',
|
||||||
|
'group' => 'admin-automation',
|
||||||
|
'label' => 'API docs',
|
||||||
|
'path' => 'admin/api-docs',
|
||||||
|
'permission' => 'api_docs.view',
|
||||||
|
'order' => 300,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'authorization_policies' => [
|
||||||
|
\MintyPHP\Module\ApiDocs\ApiDocsAuthorizationPolicy::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'search_resources' => [],
|
||||||
|
|
||||||
|
'asset_groups' => [
|
||||||
|
'api-docs' => [
|
||||||
|
'modules/api-docs/css/vendor-overrides/swagger-ui.css',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'scheduler_jobs' => [],
|
||||||
|
'layout_context_providers' => [],
|
||||||
|
'session_providers' => [],
|
||||||
|
|
||||||
|
'permissions' => [
|
||||||
|
[
|
||||||
|
'key' => 'api_docs.view',
|
||||||
|
'description' => 'Can view API documentation',
|
||||||
|
'active' => true,
|
||||||
|
'is_system' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'event_listeners' => [],
|
||||||
|
'deactivation_handler' => null,
|
||||||
|
'migrations_path' => null,
|
||||||
|
'i18n_path' => 'i18n',
|
||||||
|
];
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use MintyPHP\Buffer;
|
use MintyPHP\Buffer;
|
||||||
|
use MintyPHP\Module\ApiDocs\ApiDocsAuthorizationPolicy;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_API_DOCS_VIEW);
|
Guard::requireAbilityOrForbidden(ApiDocsAuthorizationPolicy::ABILITY_VIEW);
|
||||||
|
|
||||||
Buffer::set('title', t('API docs'));
|
Buffer::set('title', t('API docs'));
|
||||||
Buffer::set('style_groups', json_encode(['api-docs']));
|
Buffer::set('style_groups', json_encode(['api-docs']));
|
||||||
@@ -12,4 +12,4 @@ require templatePath('partials/app-breadcrumb.phtml');
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="<?php e(assetVersion('vendor/swagger-ui/swagger-ui-bundle.js')); ?>"></script>
|
<script src="<?php e(assetVersion('vendor/swagger-ui/swagger-ui-bundle.js')); ?>"></script>
|
||||||
<script type="module" src="<?php e(assetVersion('js/pages/admin-api-docs-index.js')); ?>"></script>
|
<script type="module" src="<?php e(assetVersion('modules/api-docs/js/pages/admin-api-docs-index.js')); ?>"></script>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use MintyPHP\Module\ApiDocs\ApiDocsAuthorizationPolicy;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
define('MINTY_ALLOW_OUTPUT', true);
|
define('MINTY_ALLOW_OUTPUT', true);
|
||||||
|
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_API_DOCS_VIEW);
|
Guard::requireAbilityOrForbidden(ApiDocsAuthorizationPolicy::ABILITY_VIEW);
|
||||||
|
|
||||||
$projectRoot = dirname(__DIR__, 3);
|
$specPath = defined('APP_ROOT') ? APP_ROOT . '/docs/openapi.yaml' : dirname(__DIR__, 4) . '/docs/openapi.yaml';
|
||||||
$specPath = $projectRoot . '/docs/openapi.yaml';
|
|
||||||
|
|
||||||
if (!is_file($specPath)) {
|
if (!is_file($specPath)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import url("../../vendor/swagger-ui/swagger-ui.css") layer(vendor);
|
@import url("../../../../vendor/swagger-ui/swagger-ui.css") layer(vendor);
|
||||||
|
|
||||||
@layer vendor-overrides {
|
@layer vendor-overrides {
|
||||||
.app-swagger-container {
|
.app-swagger-container {
|
||||||
@@ -14,7 +14,6 @@ $canViewPermissions = (bool) ($layoutAuth['can_view_permissions'] ?? false);
|
|||||||
$canViewSettings = (bool) ($layoutAuth['can_view_settings'] ?? false);
|
$canViewSettings = (bool) ($layoutAuth['can_view_settings'] ?? false);
|
||||||
$canViewImports = (bool) ($layoutAuth['can_view_imports'] ?? false);
|
$canViewImports = (bool) ($layoutAuth['can_view_imports'] ?? false);
|
||||||
$canViewJobs = (bool) ($layoutAuth['can_view_jobs'] ?? false);
|
$canViewJobs = (bool) ($layoutAuth['can_view_jobs'] ?? false);
|
||||||
$canViewApiDocs = (bool) ($layoutAuth['can_view_api_docs'] ?? false);
|
|
||||||
$canViewDocs = (bool) ($layoutAuth['can_view_docs'] ?? false);
|
$canViewDocs = (bool) ($layoutAuth['can_view_docs'] ?? false);
|
||||||
$canViewMailLog = (bool) ($layoutAuth['can_view_mail_log'] ?? false);
|
$canViewMailLog = (bool) ($layoutAuth['can_view_mail_log'] ?? false);
|
||||||
$canViewStats = (bool) ($layoutAuth['can_view_stats'] ?? false);
|
$canViewStats = (bool) ($layoutAuth['can_view_stats'] ?? false);
|
||||||
@@ -23,7 +22,7 @@ $docsDefaultSlug = DocsCatalogService::defaultSlug();
|
|||||||
$docsDefaultPath = 'admin/docs/' . $docsDefaultSlug;
|
$docsDefaultPath = 'admin/docs/' . $docsDefaultSlug;
|
||||||
$hasOrganization = $canViewTenants || $canViewDepartments || $canViewUsers;
|
$hasOrganization = $canViewTenants || $canViewDepartments || $canViewUsers;
|
||||||
$hasUsersSection = $canViewRoles || $canViewPermissions;
|
$hasUsersSection = $canViewRoles || $canViewPermissions;
|
||||||
$hasAutomationSection = $canViewImports || $canViewJobs || $canViewApiDocs;
|
$hasAutomationSection = $canViewImports || $canViewJobs;
|
||||||
$hasMonitoringSection = $canViewStats;
|
$hasMonitoringSection = $canViewStats;
|
||||||
$hasLogsSection = $canViewMailLog;
|
$hasLogsSection = $canViewMailLog;
|
||||||
$hasSystemSection = $canViewSettings || $canViewSystemInfo || $canViewDocs;
|
$hasSystemSection = $canViewSettings || $canViewSystemInfo || $canViewDocs;
|
||||||
@@ -102,13 +101,6 @@ $adminNavGroups = [
|
|||||||
'visible' => $canViewJobs,
|
'visible' => $canViewJobs,
|
||||||
'withTenant' => false,
|
'withTenant' => false,
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'label' => t('API docs'),
|
|
||||||
'path' => 'admin/api-docs',
|
|
||||||
'active' => navActive('admin/api-docs', true),
|
|
||||||
'visible' => $canViewApiDocs,
|
|
||||||
'withTenant' => false,
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -133,7 +125,7 @@ $adminNavGroups = [
|
|||||||
'visible' => $hasLogsSection,
|
'visible' => $hasLogsSection,
|
||||||
'items' => [
|
'items' => [
|
||||||
[
|
[
|
||||||
'label' => t('Mail logs'),
|
'label' => t('nav.logs.mail'),
|
||||||
'path' => 'admin/mail-log',
|
'path' => 'admin/mail-log',
|
||||||
'active' => navActive('admin/mail-log', true),
|
'active' => navActive('admin/mail-log', true),
|
||||||
'visible' => $canViewMailLog,
|
'visible' => $canViewMailLog,
|
||||||
@@ -199,6 +191,7 @@ if ($moduleNavItemSlots) {
|
|||||||
}
|
}
|
||||||
// Known group metadata — modules declare their group key, we map to label + icon.
|
// Known group metadata — modules declare their group key, we map to label + icon.
|
||||||
$moduleGroupMeta = [
|
$moduleGroupMeta = [
|
||||||
|
'admin-automation' => ['label' => t('Automation & integrations'), 'icon' => 'bi-gear-wide-connected'],
|
||||||
'admin-logs' => ['label' => t('Audit & logs'), 'icon' => 'bi-journal-text'],
|
'admin-logs' => ['label' => t('Audit & logs'), 'icon' => 'bi-journal-text'],
|
||||||
];
|
];
|
||||||
foreach ($moduleGroupsByKey as $groupKey => $items) {
|
foreach ($moduleGroupsByKey as $groupKey => $items) {
|
||||||
@@ -209,7 +202,7 @@ if ($moduleNavItemSlots) {
|
|||||||
// Also add mail log to the logs group if it exists as a core item
|
// Also add mail log to the logs group if it exists as a core item
|
||||||
if ($groupKey === 'admin-logs' && $canViewMailLog) {
|
if ($groupKey === 'admin-logs' && $canViewMailLog) {
|
||||||
array_unshift($items, [
|
array_unshift($items, [
|
||||||
'label' => t('Mail logs'),
|
'label' => t('nav.logs.mail'),
|
||||||
'path' => 'admin/mail-log',
|
'path' => 'admin/mail-log',
|
||||||
'active' => navActive('admin/mail-log', true),
|
'active' => navActive('admin/mail-log', true),
|
||||||
'visible' => $canViewMailLog,
|
'visible' => $canViewMailLog,
|
||||||
|
|||||||
Reference in New Issue
Block a user