Compare commits
3 Commits
d76addc784
...
da990ef19a
| Author | SHA1 | Date | |
|---|---|---|---|
| da990ef19a | |||
| 8cedd8a18a | |||
| 9fcfe32ea7 |
@@ -6,6 +6,7 @@ use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\L10N\LocaleResolver;
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXC\Module\Configuration\BrowserModuleContext;
|
||||
use KTXC\Security\Authorization\PermissionChecker;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
@@ -30,7 +31,11 @@ class InitController extends ControllerAbstract
|
||||
$configuration = [];
|
||||
|
||||
// modules - filter by permissions
|
||||
$configuration['modules'] = [];
|
||||
$browserContext = new BrowserModuleContext(
|
||||
$this->tenantContext->requireIdentifier(),
|
||||
$this->identityContext->requireIdentifier(),
|
||||
);
|
||||
|
||||
foreach ($this->moduleManager->list(true, true) as $module) {
|
||||
// Check if user has permission to view this module
|
||||
// Allow access if user has: {module_handle}, {module_handle}.*, or * permission
|
||||
@@ -39,12 +44,12 @@ class InitController extends ControllerAbstract
|
||||
continue;
|
||||
}
|
||||
|
||||
$integrations = $module->registerBI();
|
||||
if ($integrations !== null) {
|
||||
$configuration['modules'][$handle] = $integrations;
|
||||
}
|
||||
$module->configure($browserContext);
|
||||
}
|
||||
|
||||
$configuration['modules'] = $browserContext->modules();
|
||||
$configuration = array_merge($configuration, $browserContext->configuration());
|
||||
|
||||
// localization
|
||||
$configuration['l10n'] = [
|
||||
'locale' => $this->localeResolver->resolve($request),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Module\Configuration;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextType;
|
||||
use KTXF\Module\ModuleInstanceInterface;
|
||||
|
||||
final class BrowserModuleContext implements BrowserModuleContextInterface
|
||||
{
|
||||
private const RESERVED_KEYS = ['modules', 'tenant', 'user', 'l10n'];
|
||||
|
||||
/** @var array<string,array<string,mixed>> */
|
||||
private array $modules = [];
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
private array $configuration = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $tenantIdentifier,
|
||||
private readonly string $identityIdentifier,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): ModuleContextType
|
||||
{
|
||||
return ModuleContextType::Browser;
|
||||
}
|
||||
|
||||
public function registerModule(
|
||||
ModuleInstanceInterface $module,
|
||||
string $namespace,
|
||||
?string $boot = null,
|
||||
): void {
|
||||
if (trim($namespace) === '' || ($boot !== null && trim($boot) === '')) {
|
||||
throw new InvalidArgumentException('Browser namespace must not be empty and boot path must be null or non-empty');
|
||||
}
|
||||
$handle = $module->handle();
|
||||
if (isset($this->modules[$handle])) {
|
||||
throw new LogicException("Browser module '{$handle}' is already registered");
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'handle' => $handle,
|
||||
'namespace' => $namespace,
|
||||
'version' => $module->version(),
|
||||
'label' => $module->label(),
|
||||
'author' => $module->author(),
|
||||
'description' => $module->description(),
|
||||
];
|
||||
if ($boot !== null) {
|
||||
$entry['boot'] = $boot;
|
||||
}
|
||||
|
||||
$this->modules[$handle] = $entry;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
if (trim($key) === '') {
|
||||
throw new InvalidArgumentException('Browser configuration key must not be empty');
|
||||
}
|
||||
if (in_array($key, self::RESERVED_KEYS, true) || array_key_exists($key, $this->configuration)) {
|
||||
throw new LogicException("Browser configuration '{$key}' is already reserved or registered");
|
||||
}
|
||||
|
||||
$this->configuration[$key] = $value;
|
||||
}
|
||||
|
||||
public function tenantIdentifier(): string
|
||||
{
|
||||
return $this->tenantIdentifier;
|
||||
}
|
||||
|
||||
public function identityIdentifier(): string
|
||||
{
|
||||
return $this->identityIdentifier;
|
||||
}
|
||||
|
||||
public function modules(): array
|
||||
{
|
||||
return $this->modules;
|
||||
}
|
||||
|
||||
public function configuration(): array
|
||||
{
|
||||
return $this->configuration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Module\Configuration;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXF\Module\Configuration\ConsoleModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextType;
|
||||
|
||||
final class ConsoleModuleContext implements ConsoleModuleContextInterface
|
||||
{
|
||||
/** @var array<class-string,true> */
|
||||
private array $commands = [];
|
||||
|
||||
public function type(): ModuleContextType
|
||||
{
|
||||
return ModuleContextType::Console;
|
||||
}
|
||||
|
||||
public function registerCommand(string $command): void
|
||||
{
|
||||
if (trim($command) === '') {
|
||||
throw new InvalidArgumentException('Console command class must not be empty');
|
||||
}
|
||||
|
||||
$this->commands[$command] = true;
|
||||
}
|
||||
|
||||
public function commands(): array
|
||||
{
|
||||
return array_keys($this->commands);
|
||||
}
|
||||
}
|
||||
+22
-11
@@ -4,6 +4,7 @@ namespace KTXC\Module;
|
||||
|
||||
use KTXC\Console\Firewall\FirewallMaintenanceCommand;
|
||||
use KTXC\Console\Firewall\FirewallSetupCommand;
|
||||
use KTXC\Preview\PreviewManager;
|
||||
use KTXC\Service\FirewallService;
|
||||
use KTXC\Service\SystemFirewallLogService;
|
||||
use KTXC\Service\SystemFirewallRuleService;
|
||||
@@ -25,8 +26,9 @@ use KTXC\Security\Event\RateLimitExceededEvent;
|
||||
use KTXC\Security\Event\SuspiciousActivityEvent;
|
||||
use KTXF\Event\DeliveryMode;
|
||||
use KTXF\Event\EventListenerRegistrarInterface;
|
||||
use KTXF\Module\ModuleBrowserInterface;
|
||||
use KTXF\Module\ModuleConsoleInterface;
|
||||
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ConsoleModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
use KTXF\Module\ModuleInstanceAbstract;
|
||||
|
||||
/**
|
||||
@@ -34,10 +36,11 @@ use KTXF\Module\ModuleInstanceAbstract;
|
||||
*
|
||||
* Provides core system functionality and permissions
|
||||
*/
|
||||
class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, ModuleBrowserInterface
|
||||
class Module extends ModuleInstanceAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EventListenerRegistrarInterface $events,
|
||||
private readonly PreviewManager $previews,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -213,9 +216,20 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
];
|
||||
}
|
||||
|
||||
public function registerCI(): array
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
{
|
||||
return [
|
||||
if ($context instanceof BrowserModuleContextInterface) {
|
||||
$context->set(
|
||||
'preview',
|
||||
$this->previews->availability($context->tenantIdentifier()),
|
||||
);
|
||||
}
|
||||
|
||||
if (!$context instanceof ConsoleModuleContextInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ([
|
||||
FirewallSetupCommand::class,
|
||||
FirewallMaintenanceCommand::class,
|
||||
\KTXC\Console\Event\EventsDebugCommand::class,
|
||||
@@ -240,11 +254,8 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
\KTXC\Console\Role\RoleDeleteCommand::class,
|
||||
\KTXC\Console\Role\RoleAssignCommand::class,
|
||||
\KTXC\Console\Role\RoleRevokeCommand::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function registerBI(): array
|
||||
{
|
||||
return [];
|
||||
] as $command) {
|
||||
$context->registerCommand($command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ namespace KTXC\Module;
|
||||
|
||||
use JsonSerializable;
|
||||
use KTXC\Module\Store\ModuleEntry;
|
||||
use KTXF\Module\ModuleBrowserInterface;
|
||||
use KTXF\Module\ModuleConsoleInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
use KTXF\Module\ModuleInstanceInterface;
|
||||
|
||||
/**
|
||||
@@ -175,20 +174,9 @@ class ModuleObject implements JsonSerializable
|
||||
$this->instance?->upgrade();
|
||||
}
|
||||
|
||||
public function registerBI(): array | null
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
{
|
||||
if ($this->instance instanceof ModuleBrowserInterface) {
|
||||
return $this->instance->registerBI();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function registerCI(): array | null
|
||||
{
|
||||
if ($this->instance instanceof ModuleConsoleInterface) {
|
||||
return $this->instance->registerCI();
|
||||
}
|
||||
return null;
|
||||
$this->instance?->configure($context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,6 +29,29 @@ final readonly class PreviewManager
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array{enabled:bool,storage:bool,generation:bool} */
|
||||
public function availability(string $tenantId): array
|
||||
{
|
||||
$tenant = $this->tenants->fetchById($tenantId);
|
||||
if ($tenant === null) {
|
||||
return ['enabled' => false, 'storage' => false, 'generation' => false];
|
||||
}
|
||||
|
||||
$configuration = $tenant->getConfiguration()->preview();
|
||||
$enabled = $configuration->enabled();
|
||||
$storage = $enabled
|
||||
&& $tenant->getConfiguration()->stores()->store($configuration->store()) !== null;
|
||||
$generation = $storage && $this->previewProviders() !== [];
|
||||
|
||||
if ($enabled && !$storage) {
|
||||
$this->logger->debug('Preview storage is not configured', ['tenantId' => $tenantId]);
|
||||
} elseif ($enabled && $storage && !$generation) {
|
||||
$this->logger->debug('No preview generation provider is available', ['tenantId' => $tenantId]);
|
||||
}
|
||||
|
||||
return compact('enabled', 'storage', 'generation');
|
||||
}
|
||||
|
||||
public function fetch(
|
||||
string $tenantId,
|
||||
PreviewSource $source,
|
||||
@@ -45,6 +68,7 @@ final readonly class PreviewManager
|
||||
!$configuration->enabled()
|
||||
|| $variantConfiguration === null
|
||||
|| ($source->size !== null && $source->size > $configuration->maxSourceSize())
|
||||
|| $tenant->getConfiguration()->stores()->store($configuration->store()) === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -69,6 +93,9 @@ final readonly class PreviewManager
|
||||
}
|
||||
|
||||
$provider = $this->selectProvider($source->mimeType, $request);
|
||||
if ($provider === null) {
|
||||
return null;
|
||||
}
|
||||
$result = $provider->generate($source, $request);
|
||||
$blob = $this->writeCache(
|
||||
$tenantId,
|
||||
@@ -218,11 +245,11 @@ final readonly class PreviewManager
|
||||
private function selectProvider(
|
||||
string $sourceMimeType,
|
||||
PreviewRequest $request,
|
||||
): PreviewProviderInterface {
|
||||
): ?PreviewProviderInterface {
|
||||
$sourceMimeType = MimeType::normalize($sourceMimeType);
|
||||
$candidates = [];
|
||||
|
||||
foreach ($this->providers->providers(ProviderInterface::TYPE_PREVIEW) as $identifier => $provider) {
|
||||
foreach ($this->previewProviders() as $identifier => $provider) {
|
||||
if (
|
||||
!$provider instanceof PreviewProviderInterface
|
||||
|| !$provider->supports($sourceMimeType, $request)
|
||||
@@ -234,9 +261,7 @@ final readonly class PreviewManager
|
||||
}
|
||||
|
||||
if ($candidates === []) {
|
||||
throw new PreviewGenerationException(
|
||||
"No preview provider supports {$sourceMimeType} to {$request->preferredMimeType}"
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
usort($candidates, static function (array $left, array $right): int {
|
||||
@@ -246,4 +271,13 @@ final readonly class PreviewManager
|
||||
|
||||
return $candidates[0]['provider'];
|
||||
}
|
||||
|
||||
/** @return array<string, PreviewProviderInterface> */
|
||||
private function previewProviders(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->providers->providers(ProviderInterface::TYPE_PREVIEW),
|
||||
static fn(ProviderInterface $provider): bool => $provider instanceof PreviewProviderInterface,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace KTXC\Runtime\Console;
|
||||
use KTXC\Application\Execution\ExecutionDescriptor;
|
||||
use KTXC\Kernel;
|
||||
use KTXC\KernelInterface;
|
||||
use KTXC\Module\Configuration\ConsoleModuleContext;
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXF\Module\ModuleConsoleInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Application as ConsoleApplication;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
@@ -34,15 +34,13 @@ final class ConsoleRuntime
|
||||
|
||||
/** @var ModuleManager $moduleManager */
|
||||
$moduleManager = $container->get(ModuleManager::class);
|
||||
$moduleContext = new ConsoleModuleContext();
|
||||
foreach ($moduleManager->list() as $module) {
|
||||
$instance = $module->instance();
|
||||
if (!$instance instanceof ModuleConsoleInterface) {
|
||||
continue;
|
||||
}
|
||||
$module->configure($moduleContext);
|
||||
}
|
||||
|
||||
foreach ($instance->registerCI() as $commandClass) {
|
||||
$this->registerCommand($console, $container, $commandClass);
|
||||
}
|
||||
foreach ($moduleContext->commands() as $commandClass) {
|
||||
$this->registerCommand($console, $container, $commandClass);
|
||||
}
|
||||
|
||||
return $console->run($input, $output);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createPinia } from 'pinia'
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
||||
import { useModuleStore } from '@KTXC/stores/moduleStore'
|
||||
import { useTenantStore } from '@KTXC/stores/tenantStore'
|
||||
import { usePreviewStore } from '@KTXC/stores/previewStore'
|
||||
import { useUserStore } from '@KTXC/stores/userStore'
|
||||
import { useL10nStore } from '@KTXC/stores/l10nStore'
|
||||
import { useThemeStore } from '@KTXC/stores/themeStore'
|
||||
@@ -41,6 +42,7 @@ globalWindow.Pinia = PiniaLib as unknown
|
||||
(async () => {
|
||||
const moduleStore = useModuleStore();
|
||||
const tenantStore = useTenantStore();
|
||||
const previewStore = usePreviewStore();
|
||||
const userStore = useUserStore();
|
||||
const l10nStore = useL10nStore();
|
||||
const themeStore = useThemeStore();
|
||||
@@ -49,6 +51,7 @@ globalWindow.Pinia = PiniaLib as unknown
|
||||
try {
|
||||
const payload = await fetchWrapper.get('/init');
|
||||
moduleStore.init(payload?.modules ?? {});
|
||||
previewStore.init(payload?.preview ?? null);
|
||||
tenantStore.init(payload?.tenant ?? null);
|
||||
userStore.init(payload?.user ?? {});
|
||||
layoutStore.hydrateFromSettings();
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
// Stores
|
||||
export { useModuleStore } from '../stores/moduleStore'
|
||||
export { useTenantStore } from '../stores/tenantStore'
|
||||
export { usePreviewStore } from '../stores/previewStore'
|
||||
export type { PreviewAvailability } from '../stores/previewStore'
|
||||
export { useUserStore } from '../stores/userStore'
|
||||
export { useIntegrationStore } from '../stores/integrationStore'
|
||||
export { useLayoutStore } from '../stores/layoutStore'
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface PreviewAvailability {
|
||||
enabled: boolean
|
||||
storage: boolean
|
||||
generation: boolean
|
||||
}
|
||||
|
||||
const unavailable = (): PreviewAvailability => ({
|
||||
enabled: false,
|
||||
storage: false,
|
||||
generation: false,
|
||||
})
|
||||
|
||||
export const usePreviewStore = defineStore('previewStore', () => {
|
||||
const availability = ref<PreviewAvailability>(unavailable())
|
||||
|
||||
function init(data?: Partial<PreviewAvailability> | null): void {
|
||||
availability.value = {
|
||||
enabled: data?.enabled ?? false,
|
||||
storage: data?.storage ?? false,
|
||||
generation: data?.generation ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
availability.value = unavailable()
|
||||
}
|
||||
|
||||
return {
|
||||
availability,
|
||||
init,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
Generated
+4
-4
@@ -6642,9 +6642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.6.tgz",
|
||||
"integrity": "sha512-VOsRTsNfs+FE4JXMONZkqX2yznSqC/FXG9/pgVVjsoqIjUvJg+CfFTuOlgyWVFrZYY+crk7LV9uaiN8bZREV/g==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.12.tgz",
|
||||
"integrity": "sha512-WXGbnuzqUGPZM7nF38C3jiucaLRutHZOICVW0wg1dyXppW11i+su/IT20nK+1o86nsgTdedejeEv53cJUrI30A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -6653,7 +6653,7 @@
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=2.1.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue": "^3.5.0 || ^3.6.0-0",
|
||||
"webpack-plugin-vuetify": ">=3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Module\Configuration;
|
||||
|
||||
use KTXF\Module\ModuleInstanceInterface;
|
||||
|
||||
interface BrowserModuleContextInterface extends ModuleContextInterface
|
||||
{
|
||||
public function registerModule(
|
||||
ModuleInstanceInterface $module,
|
||||
string $namespace,
|
||||
?string $boot = null,
|
||||
): void;
|
||||
|
||||
public function set(string $key, mixed $value): void;
|
||||
|
||||
public function tenantIdentifier(): string;
|
||||
|
||||
public function identityIdentifier(): string;
|
||||
|
||||
/** @return array<string,array<string,mixed>> */
|
||||
public function modules(): array;
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function configuration(): array;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Module\Configuration;
|
||||
|
||||
interface ConsoleModuleContextInterface extends ModuleContextInterface
|
||||
{
|
||||
/** @param class-string $command */
|
||||
public function registerCommand(string $command): void;
|
||||
|
||||
/** @return list<class-string> */
|
||||
public function commands(): array;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Module\Configuration;
|
||||
|
||||
interface ModuleContextInterface
|
||||
{
|
||||
public function type(): ModuleContextType;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Module\Configuration;
|
||||
|
||||
enum ModuleContextType: string
|
||||
{
|
||||
case Browser = 'browser';
|
||||
case Console = 'console';
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Module;
|
||||
|
||||
/**
|
||||
* Module Browser Interface
|
||||
*/
|
||||
interface ModuleBrowserInterface
|
||||
{
|
||||
public function registerBI(): array;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Module;
|
||||
|
||||
/**
|
||||
* Module Console Interface
|
||||
*/
|
||||
interface ModuleConsoleInterface
|
||||
{
|
||||
public function registerCI(): array;
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace KTXF\Module;
|
||||
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
|
||||
abstract class ModuleInstanceAbstract implements ModuleInstanceInterface
|
||||
{
|
||||
// mandatory methods that must be implemented by each concrete module
|
||||
@@ -46,6 +48,11 @@ abstract class ModuleInstanceAbstract implements ModuleInstanceInterface
|
||||
// Override in specific modules if needed
|
||||
}
|
||||
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
{
|
||||
// Override when the module supplies browser or console integrations.
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions provided by this module
|
||||
*
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
namespace KTXF\Module;
|
||||
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
|
||||
interface ModuleInstanceInterface
|
||||
{
|
||||
/**
|
||||
* Declare the integrations supplied by this module.
|
||||
*/
|
||||
public function configure(ModuleContextInterface $context): void;
|
||||
|
||||
/**
|
||||
* Get module version
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,10 @@ use KTXC\Console\Firewall\FirewallMaintenanceCommand;
|
||||
use KTXC\Console\Firewall\FirewallSetupCommand;
|
||||
use KTXC\Console\Event\EventsDebugCommand;
|
||||
use KTXC\Module\Module;
|
||||
use KTXC\Module\Configuration\BrowserModuleContext;
|
||||
use KTXC\Module\Configuration\ConsoleModuleContext;
|
||||
use KTXC\Preview\PreviewManager;
|
||||
use KTXC\Resource\ProviderManager;
|
||||
use KTXC\Service\FirewallService;
|
||||
use KTXC\Service\SystemFirewallRuleService;
|
||||
use KTXC\Service\SystemFirewallLogService;
|
||||
@@ -15,6 +19,8 @@ use KTXC\Service\TenantFirewallLogService;
|
||||
use KTXC\Service\TenantFirewallStatusService;
|
||||
use KTXC\Service\SystemFirewallStatusService;
|
||||
use KTXC\Service\TenantFirewallRuleService;
|
||||
use KTXC\Service\TenantService;
|
||||
use KTXC\SystemStore\SystemStoreManager;
|
||||
use KTXC\Event\EventListenerRegistry;
|
||||
use KTXC\Security\Event\AccessDeniedEvent;
|
||||
use KTXC\Security\Event\AuthenticationFailedEvent;
|
||||
@@ -32,6 +38,8 @@ use KTXF\Event\DeliveryMode;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class CoreModuleTest extends TestCase
|
||||
{
|
||||
@@ -40,7 +48,7 @@ final class CoreModuleTest extends TestCase
|
||||
public function registersListeners(): void
|
||||
{
|
||||
$registry = new EventListenerRegistry();
|
||||
$module = new Module($registry);
|
||||
$module = $this->module($registry);
|
||||
|
||||
$module->boot();
|
||||
$definitions = $registry->definitions();
|
||||
@@ -83,18 +91,21 @@ final class CoreModuleTest extends TestCase
|
||||
#[TestDox('Core exposes the event registry debug command')]
|
||||
public function exposesDebugCommand(): void
|
||||
{
|
||||
$module = new Module(new EventListenerRegistry());
|
||||
$module = $this->module();
|
||||
$context = new ConsoleModuleContext();
|
||||
$module->configure($context);
|
||||
$commands = $context->commands();
|
||||
|
||||
self::assertContains(EventsDebugCommand::class, $module->registerCI());
|
||||
self::assertContains(FirewallSetupCommand::class, $module->registerCI());
|
||||
self::assertContains(FirewallMaintenanceCommand::class, $module->registerCI());
|
||||
self::assertContains(EventsDebugCommand::class, $commands);
|
||||
self::assertContains(FirewallSetupCommand::class, $commands);
|
||||
self::assertContains(FirewallMaintenanceCommand::class, $commands);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestDox('Core registers dedicated system firewall permissions')]
|
||||
public function registersSystemFirewallPermissions(): void
|
||||
{
|
||||
$permissions = (new Module(new EventListenerRegistry()))->permissions();
|
||||
$permissions = $this->module()->permissions();
|
||||
|
||||
self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_READ, $permissions);
|
||||
self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_MANAGE, $permissions);
|
||||
@@ -107,4 +118,35 @@ final class CoreModuleTest extends TestCase
|
||||
self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ, $permissions);
|
||||
self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_SETTINGS_MANAGE, $permissions);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestDox('Core resolves preview browser configuration')]
|
||||
public function resolvesPreviewBrowserConfiguration(): void
|
||||
{
|
||||
$module = $this->module();
|
||||
$context = new BrowserModuleContext('tenant-id', 'identity-id');
|
||||
$module->configure($context);
|
||||
|
||||
self::assertSame([
|
||||
'preview' => [
|
||||
'enabled' => false,
|
||||
'storage' => false,
|
||||
'generation' => false,
|
||||
],
|
||||
], $context->configuration());
|
||||
}
|
||||
|
||||
private function module(?EventListenerRegistry $registry = null): Module
|
||||
{
|
||||
$tenants = $this->createStub(TenantService::class);
|
||||
$providers = new ProviderManager($this->createStub(ContainerInterface::class));
|
||||
$previews = new PreviewManager(
|
||||
$tenants,
|
||||
$providers,
|
||||
new SystemStoreManager($tenants, $providers),
|
||||
$this->createStub(LoggerInterface::class),
|
||||
);
|
||||
|
||||
return new Module($registry ?? new EventListenerRegistry(), $previews);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\Unit\Module;
|
||||
|
||||
use KTXC\Module\Configuration\BrowserModuleContext;
|
||||
use KTXC\Module\Configuration\ConsoleModuleContext;
|
||||
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ConsoleModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextType;
|
||||
use KTXF\Module\ModuleInstanceAbstract;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ModuleContextTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function moduleRegistersItselfWithTheActiveContext(): void
|
||||
{
|
||||
$module = new TestContextModule();
|
||||
$browser = new BrowserModuleContext('tenant-id', 'identity-id');
|
||||
$console = new ConsoleModuleContext();
|
||||
$module->configure($browser);
|
||||
$module->configure($console);
|
||||
|
||||
self::assertSame(
|
||||
[
|
||||
'test_context' => [
|
||||
'handle' => 'test_context',
|
||||
'namespace' => 'TestContext',
|
||||
'version' => '0.0.1',
|
||||
'label' => 'Test Context',
|
||||
'author' => 'Ktrix',
|
||||
'description' => 'Tests module contexts',
|
||||
'boot' => 'static/module.mjs',
|
||||
],
|
||||
],
|
||||
$browser->modules(),
|
||||
);
|
||||
self::assertSame(
|
||||
['test' => ['tenant' => 'tenant-id', 'identity' => 'identity-id']],
|
||||
$browser->configuration(),
|
||||
);
|
||||
self::assertSame(['ExampleCommand'], $console->commands());
|
||||
self::assertSame(ModuleContextType::Browser, $browser->type());
|
||||
self::assertSame(ModuleContextType::Console, $console->type());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejectsDuplicateBrowserConfigurationKeys(): void
|
||||
{
|
||||
$context = new BrowserModuleContext('tenant-id', 'identity-id');
|
||||
$context->set('test', true);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$context->set('test', false);
|
||||
}
|
||||
}
|
||||
|
||||
final class TestContextModule extends ModuleInstanceAbstract
|
||||
{
|
||||
public function handle(): string { return 'test_context'; }
|
||||
public function version(): string { return '0.0.1'; }
|
||||
public function label(): string { return 'Test Context'; }
|
||||
public function description(): string { return 'Tests module contexts'; }
|
||||
public function author(): string { return 'Ktrix'; }
|
||||
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
{
|
||||
if ($context instanceof BrowserModuleContextInterface) {
|
||||
$context->registerModule($this, 'TestContext', 'static/module.mjs');
|
||||
$context->set('test', [
|
||||
'tenant' => $context->tenantIdentifier(),
|
||||
'identity' => $context->identityIdentifier(),
|
||||
]);
|
||||
}
|
||||
if ($context instanceof ConsoleModuleContextInterface) {
|
||||
$context->registerCommand('ExampleCommand');
|
||||
$context->registerCommand('ExampleCommand');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,12 +154,7 @@ final class PreviewManagerTest extends TestCase
|
||||
$service = $this->createMock(SystemStoreServiceInterface::class);
|
||||
$service->expects(self::once())->method('stat')->willReturn(null);
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects(self::once())->method('warning')->with(
|
||||
'Preview unavailable',
|
||||
self::callback(static fn(array $context): bool =>
|
||||
($context['exception'] ?? null) instanceof PreviewGenerationException
|
||||
),
|
||||
);
|
||||
$logger->expects(self::never())->method('warning');
|
||||
|
||||
self::assertNull($this->manager(
|
||||
$provider,
|
||||
@@ -169,6 +164,50 @@ final class PreviewManagerTest extends TestCase
|
||||
)->fetch('tenant-a', $this->source()));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function missingPreviewStorageIsUnavailableWithoutLoggingOrProviderLookup(): void
|
||||
{
|
||||
$provider = $this->provider();
|
||||
$provider->expects(self::never())->method('generate');
|
||||
$providerManager = $this->createMock(ProviderManager::class);
|
||||
$providerManager->expects(self::never())->method('providers');
|
||||
$service = $this->createMock(SystemStoreServiceInterface::class);
|
||||
$service->expects(self::never())->method('stat');
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects(self::never())->method('warning');
|
||||
|
||||
self::assertNull($this->manager(
|
||||
$provider,
|
||||
$service,
|
||||
new TenantConfiguration(),
|
||||
$providerManager,
|
||||
$logger,
|
||||
)->fetch('tenant-a', $this->source()));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reportsStorageAndGenerationAvailabilitySeparately(): void
|
||||
{
|
||||
$provider = $this->provider();
|
||||
$providerManager = $this->createMock(ProviderManager::class);
|
||||
$providerManager->expects(self::once())->method('providers')->willReturn([]);
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects(self::once())->method('debug')->with(
|
||||
'No preview generation provider is available',
|
||||
['tenantId' => 'tenant-a'],
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
['enabled' => true, 'storage' => true, 'generation' => false],
|
||||
$this->manager(
|
||||
$provider,
|
||||
$this->createStub(SystemStoreServiceInterface::class),
|
||||
providerManager: $providerManager,
|
||||
logger: $logger,
|
||||
)->availability('tenant-a'),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function cacheKeysAreDeterministicOpaqueAndChangeWithSourceIdentity(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user