refactor(kernel): unify HTTP and CLI application lifecycle
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use KTXC\Application\KernelOptions;
|
||||
use KTXC\Application\ProjectPaths;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Module\ModuleAutoloader;
|
||||
use KTXC\Runtime\Console\ConsoleRuntime;
|
||||
use KTXC\Runtime\Http\HttpRuntime;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
final class Application
|
||||
{
|
||||
private readonly ProjectPaths $paths;
|
||||
private readonly array $config;
|
||||
private readonly Kernel $kernel;
|
||||
private readonly HttpRuntime $http;
|
||||
private readonly ConsoleRuntime $console;
|
||||
|
||||
public function __construct(
|
||||
string $projectDir,
|
||||
?ClassLoader $composerLoader = null,
|
||||
?string $environment = null,
|
||||
?bool $debug = null,
|
||||
) {
|
||||
$this->paths = ProjectPaths::resolve($projectDir);
|
||||
$this->config = $this->loadConfig();
|
||||
|
||||
$environment ??= $this->config['environment'] ?? 'prod';
|
||||
$debug ??= (bool) ($this->config['debug'] ?? false);
|
||||
|
||||
$this->kernel = new Kernel(
|
||||
new KernelOptions(
|
||||
$this->paths,
|
||||
$environment,
|
||||
$debug,
|
||||
),
|
||||
$this->config,
|
||||
);
|
||||
|
||||
(new ModuleAutoloader(
|
||||
$this->moduleDir(),
|
||||
$composerLoader,
|
||||
))->register();
|
||||
|
||||
$this->http = new HttpRuntime($this->kernel, $debug);
|
||||
$this->console = new ConsoleRuntime($this->kernel);
|
||||
}
|
||||
|
||||
public static function create(
|
||||
string $projectDir,
|
||||
?ClassLoader $composerLoader = null,
|
||||
?string $environment = null,
|
||||
?bool $debug = null,
|
||||
): self {
|
||||
return new self($projectDir, $composerLoader, $environment, $debug);
|
||||
}
|
||||
|
||||
public function runHttp(): void
|
||||
{
|
||||
try {
|
||||
$this->http->run();
|
||||
} finally {
|
||||
$this->kernel->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public function handleHttp(Request $request): Response
|
||||
{
|
||||
return $this->http->handle($request);
|
||||
}
|
||||
|
||||
public function runConsole(): int
|
||||
{
|
||||
try {
|
||||
return $this->console->run();
|
||||
} finally {
|
||||
$this->kernel->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$this->kernel->shutdown();
|
||||
}
|
||||
|
||||
public function kernel(): KernelInterface
|
||||
{
|
||||
return $this->kernel;
|
||||
}
|
||||
|
||||
public function container(): ContainerInterface
|
||||
{
|
||||
return $this->kernel->container();
|
||||
}
|
||||
|
||||
public function environment(): string
|
||||
{
|
||||
return $this->kernel->environment();
|
||||
}
|
||||
|
||||
public function debug(): bool
|
||||
{
|
||||
return $this->kernel->debug();
|
||||
}
|
||||
|
||||
public function projectDir(): string
|
||||
{
|
||||
return $this->paths->project;
|
||||
}
|
||||
|
||||
public function moduleDir(): string
|
||||
{
|
||||
return $this->paths->modules();
|
||||
}
|
||||
|
||||
public function config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
$value = $this->config;
|
||||
foreach (explode('.', $key) as $part) {
|
||||
if (!is_array($value) || !array_key_exists($part, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$part];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function loadConfig(): array
|
||||
{
|
||||
$path = $this->paths->configuration() . '/system.php';
|
||||
if (!is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$config = require $path;
|
||||
if (!is_array($config)) {
|
||||
throw new \RuntimeException("Configuration file must return an array: {$path}");
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application\Execution;
|
||||
|
||||
final readonly class ExecutionContext
|
||||
{
|
||||
public function __construct(
|
||||
public RuntimeType $runtime,
|
||||
public string $executionId,
|
||||
public string $correlationId,
|
||||
public ?string $causationId = null,
|
||||
public ?string $requestId = null,
|
||||
public ?string $operationName = null,
|
||||
public array $traceMetadata = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromDescriptor(ExecutionDescriptor $descriptor): self
|
||||
{
|
||||
return new self(
|
||||
$descriptor->runtime,
|
||||
$descriptor->executionId,
|
||||
$descriptor->correlationId,
|
||||
operationName: $descriptor->operationName,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application\Execution;
|
||||
|
||||
final readonly class ExecutionDescriptor
|
||||
{
|
||||
public function __construct(
|
||||
public RuntimeType $runtime,
|
||||
public string $executionId,
|
||||
public string $correlationId,
|
||||
public ?string $operationName = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function http(?string $operationName = null): self
|
||||
{
|
||||
$id = self::id();
|
||||
|
||||
return new self(RuntimeType::HTTP, $id, $id, $operationName);
|
||||
}
|
||||
|
||||
public static function cli(?string $operationName = null): self
|
||||
{
|
||||
$id = self::id();
|
||||
|
||||
return new self(RuntimeType::CLI, $id, $id, $operationName);
|
||||
}
|
||||
|
||||
private static function id(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application\Execution;
|
||||
|
||||
final readonly class ExecutionOutcome
|
||||
{
|
||||
private function __construct(
|
||||
public bool $successful,
|
||||
public mixed $result = null,
|
||||
public ?\Throwable $error = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function success(mixed $result = null): self
|
||||
{
|
||||
return new self(true, $result);
|
||||
}
|
||||
|
||||
public static function failure(\Throwable $error, mixed $result = null): self
|
||||
{
|
||||
return new self(false, $result, $error);
|
||||
}
|
||||
|
||||
public static function incomplete(): self
|
||||
{
|
||||
return new self(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application\Execution;
|
||||
|
||||
use KTXC\Context\IdentityContext;
|
||||
use KTXC\Context\TenantContext;
|
||||
|
||||
final class ExecutionScope
|
||||
{
|
||||
private bool $terminated = false;
|
||||
|
||||
public function __construct(
|
||||
public readonly ExecutionDescriptor $descriptor,
|
||||
public readonly ExecutionContext $context,
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly IdentityContext $identityContext,
|
||||
) {
|
||||
$this->tenantContext->clear();
|
||||
$this->identityContext->clear();
|
||||
}
|
||||
|
||||
public function markTerminated(): void
|
||||
{
|
||||
if ($this->terminated) {
|
||||
throw new \LogicException('The execution scope has already been terminated.');
|
||||
}
|
||||
|
||||
$this->terminated = true;
|
||||
}
|
||||
|
||||
public function terminated(): bool
|
||||
{
|
||||
return $this->terminated;
|
||||
}
|
||||
|
||||
public function dispose(): void
|
||||
{
|
||||
$this->identityContext->clear();
|
||||
$this->tenantContext->clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application\Execution;
|
||||
|
||||
enum RuntimeType: string
|
||||
{
|
||||
case HTTP = 'http';
|
||||
case CLI = 'cli';
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application\Execution;
|
||||
|
||||
final readonly class TerminationReport
|
||||
{
|
||||
/**
|
||||
* @param list<\Throwable> $failures
|
||||
*/
|
||||
public function __construct(
|
||||
public int $deferredProcessed = 0,
|
||||
public int $deferredRemaining = 0,
|
||||
public array $failures = [],
|
||||
public bool $deadlineExceeded = false,
|
||||
public bool $limitExceeded = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application;
|
||||
|
||||
final readonly class KernelOptions
|
||||
{
|
||||
public function __construct(
|
||||
public ProjectPaths $paths,
|
||||
public string $environment = 'prod',
|
||||
public bool $debug = false,
|
||||
) {
|
||||
if ($this->environment === '') {
|
||||
throw new \InvalidArgumentException('Kernel environment cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Application;
|
||||
|
||||
final readonly class ProjectPaths
|
||||
{
|
||||
private function __construct(
|
||||
public string $project,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function resolve(string $start): self
|
||||
{
|
||||
$directory = is_file($start) ? dirname($start) : $start;
|
||||
$directory = rtrim($directory, DIRECTORY_SEPARATOR);
|
||||
|
||||
while ($directory !== dirname($directory)) {
|
||||
if (is_file($directory . '/composer.json')) {
|
||||
return new self(realpath($directory) ?: $directory);
|
||||
}
|
||||
$directory = dirname($directory);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("Unable to resolve project root from {$start}.");
|
||||
}
|
||||
|
||||
public function configuration(): string
|
||||
{
|
||||
return $this->project . '/config';
|
||||
}
|
||||
|
||||
public function modules(): string
|
||||
{
|
||||
return $this->project . '/modules';
|
||||
}
|
||||
|
||||
public function cache(string $environment): string
|
||||
{
|
||||
return $this->project . '/var/cache/' . $environment;
|
||||
}
|
||||
|
||||
public function logs(): string
|
||||
{
|
||||
return $this->project . '/var/log';
|
||||
}
|
||||
|
||||
public function runtime(): string
|
||||
{
|
||||
return $this->project . '/var';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Console\Event;
|
||||
|
||||
use KTXF\Event\EventListenerRegistry;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'events:debug',
|
||||
description: 'Display the frozen event listener registry',
|
||||
)]
|
||||
final class EventsDebugCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EventListenerRegistry $registry,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$rows = [];
|
||||
foreach ($this->registry->definitions() as $listener) {
|
||||
$rows[] = [
|
||||
$listener->module,
|
||||
$listener->event,
|
||||
$listener->service . '::' . $listener->method,
|
||||
$listener->delivery->value,
|
||||
$listener->priority,
|
||||
$listener->failurePolicy->value,
|
||||
];
|
||||
}
|
||||
|
||||
$io->table(
|
||||
['Module', 'Event', 'Listener', 'Delivery', 'Priority', 'Failure'],
|
||||
$rows,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Context;
|
||||
|
||||
use KTXC\Models\Identity\User;
|
||||
|
||||
final class IdentityContext implements IdentityContextInterface
|
||||
{
|
||||
private ?User $identity = null;
|
||||
|
||||
public function initialize(User $identity): void
|
||||
{
|
||||
if ($this->identity !== null) {
|
||||
throw new \LogicException('The execution identity has already been initialized.');
|
||||
}
|
||||
|
||||
$this->identity = $identity;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->identity = null;
|
||||
}
|
||||
|
||||
public function present(): bool
|
||||
{
|
||||
return $this->identity !== null;
|
||||
}
|
||||
|
||||
public function identity(): ?User
|
||||
{
|
||||
return $this->identity;
|
||||
}
|
||||
|
||||
public function identifier(): ?string
|
||||
{
|
||||
return $this->identity?->getId();
|
||||
}
|
||||
|
||||
public function requireIdentifier(): string
|
||||
{
|
||||
return $this->identifier()
|
||||
?? throw new \LogicException('This operation requires an identity context.');
|
||||
}
|
||||
|
||||
public function label(): ?string
|
||||
{
|
||||
return $this->identity?->getLabel();
|
||||
}
|
||||
|
||||
public function mailAddress(): ?string
|
||||
{
|
||||
return $this->identity?->getIdentity();
|
||||
}
|
||||
|
||||
public function nameFirst(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function nameLast(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return $this->identity?->getPermissions() ?? [];
|
||||
}
|
||||
|
||||
public function roles(): array
|
||||
{
|
||||
return $this->identity?->getRoles() ?? [];
|
||||
}
|
||||
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
$permissions = $this->permissions();
|
||||
if (in_array($permission, $permissions, true) || in_array('*', $permissions, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($permissions as $userPermission) {
|
||||
if (str_ends_with($userPermission, '.*')) {
|
||||
$prefix = substr($userPermission, 0, -2);
|
||||
if (str_starts_with($permission, $prefix . '.')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasRole(string $role): bool
|
||||
{
|
||||
return in_array($role, $this->roles(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Context;
|
||||
|
||||
use KTXC\Models\Identity\User;
|
||||
|
||||
interface IdentityContextInterface
|
||||
{
|
||||
public function present(): bool;
|
||||
public function identity(): ?User;
|
||||
public function identifier(): ?string;
|
||||
public function requireIdentifier(): string;
|
||||
public function label(): ?string;
|
||||
public function mailAddress(): ?string;
|
||||
public function nameFirst(): ?string;
|
||||
public function nameLast(): ?string;
|
||||
public function permissions(): array;
|
||||
public function roles(): array;
|
||||
public function hasPermission(string $permission): bool;
|
||||
public function hasRole(string $role): bool;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Context;
|
||||
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
use KTXC\Models\Tenant\TenantObject;
|
||||
use KTXC\Service\TenantService;
|
||||
|
||||
final class TenantContext implements TenantContextInterface
|
||||
{
|
||||
private ?TenantObject $tenant = null;
|
||||
private ?string $domain = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly TenantService $tenantService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolveDomain(string $domain): bool
|
||||
{
|
||||
$this->clear();
|
||||
$tenant = $this->tenantService->fetchByDomain($domain);
|
||||
if ($tenant === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->domain = $domain;
|
||||
$this->tenant = $tenant;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function resolveIdentifier(string $identifier): bool
|
||||
{
|
||||
$this->clear();
|
||||
$tenant = $this->tenantService->fetchById($identifier);
|
||||
if ($tenant === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->domain = $identifier;
|
||||
$this->tenant = $tenant;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->tenant = null;
|
||||
$this->domain = null;
|
||||
}
|
||||
|
||||
public function present(): bool
|
||||
{
|
||||
return $this->tenant !== null;
|
||||
}
|
||||
|
||||
public function configured(): bool
|
||||
{
|
||||
return $this->present();
|
||||
}
|
||||
|
||||
public function enabled(): bool
|
||||
{
|
||||
return $this->tenant?->getEnabled() ?? false;
|
||||
}
|
||||
|
||||
public function domain(): ?string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function identifier(): ?string
|
||||
{
|
||||
return $this->tenant?->getIdentifier();
|
||||
}
|
||||
|
||||
public function requireIdentifier(): string
|
||||
{
|
||||
return $this->identifier()
|
||||
?? throw new \LogicException('This operation requires a tenant context.');
|
||||
}
|
||||
|
||||
public function label(): ?string
|
||||
{
|
||||
return $this->tenant?->getLabel();
|
||||
}
|
||||
|
||||
public function configuration(): ?TenantConfiguration
|
||||
{
|
||||
return $this->tenant?->getConfiguration();
|
||||
}
|
||||
|
||||
public function settings(): array
|
||||
{
|
||||
return $this->tenant?->getSettings() ?? [];
|
||||
}
|
||||
|
||||
public function identityProviders(): array
|
||||
{
|
||||
return $this->tenant?->getConfiguration()['identity']['providers'] ?? [];
|
||||
}
|
||||
|
||||
public function identityProviderConfig(string $providerId): ?array
|
||||
{
|
||||
return $this->identityProviders()[$providerId] ?? null;
|
||||
}
|
||||
|
||||
public function isIdentityProviderEnabled(string $providerId): bool
|
||||
{
|
||||
$config = $this->identityProviderConfig($providerId);
|
||||
|
||||
return $config !== null && ($config['enabled'] ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Context;
|
||||
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
|
||||
interface TenantContextInterface
|
||||
{
|
||||
public function present(): bool;
|
||||
public function configured(): bool;
|
||||
public function enabled(): bool;
|
||||
public function domain(): ?string;
|
||||
public function identifier(): ?string;
|
||||
public function requireIdentifier(): string;
|
||||
public function label(): ?string;
|
||||
public function configuration(): ?TenantConfiguration;
|
||||
public function settings(): array;
|
||||
public function identityProviders(): array;
|
||||
public function identityProviderConfig(string $providerId): ?array;
|
||||
public function isIdentityProviderEnabled(string $providerId): bool;
|
||||
}
|
||||
@@ -10,14 +10,14 @@ use KTXC\Http\Response\RedirectResponse;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AnonymousRoute;
|
||||
use KTXC\Service\SecurityService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Http\Request\Request;
|
||||
|
||||
class DefaultController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SecurityService $securityService,
|
||||
private readonly SessionIdentity $identity,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
#[Inject('rootDir')] private readonly string $rootDir,
|
||||
) {}
|
||||
|
||||
@@ -25,7 +25,7 @@ class DefaultController extends ControllerAbstract
|
||||
public function home(Request $request): Response
|
||||
{
|
||||
// If an authenticated identity is available, serve the private app
|
||||
if ($this->identity->identifier()) {
|
||||
if ($this->identityContext->identifier()) {
|
||||
return new FileResponse(
|
||||
$this->rootDir . '/public/private.html',
|
||||
Response::HTTP_OK,
|
||||
@@ -116,7 +116,7 @@ class DefaultController extends ControllerAbstract
|
||||
public function catchAll(Request $request, string $path = ''): Response
|
||||
{
|
||||
// If an authenticated identity is available, serve the private app
|
||||
if ($this->identity->identifier()) {
|
||||
if ($this->identityContext->identifier()) {
|
||||
return new FileResponse(
|
||||
$this->rootDir . '/public/private.html',
|
||||
Response::HTTP_OK,
|
||||
|
||||
@@ -8,16 +8,16 @@ use KTXC\L10N\LocaleResolver;
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXC\Security\Authorization\PermissionChecker;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
class InitController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenant,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly ModuleManager $moduleManager,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly PermissionChecker $permissionChecker,
|
||||
@@ -54,21 +54,21 @@ class InitController extends ControllerAbstract
|
||||
|
||||
// tenant
|
||||
$configuration['tenant'] = [
|
||||
'id' => $this->tenant->identifier(),
|
||||
'domain' => $this->tenant->domain(),
|
||||
'label' => $this->tenant->label(),
|
||||
'id' => $this->tenantContext->identifier(),
|
||||
'domain' => $this->tenantContext->domain(),
|
||||
'label' => $this->tenantContext->label(),
|
||||
];
|
||||
|
||||
// user
|
||||
$configuration['user'] = [
|
||||
'auth' => [
|
||||
'identifier' => $this->userIdentity->identifier(),
|
||||
'identity' => $this->userIdentity->identity()->getIdentity(),
|
||||
'label' => $this->userIdentity->label(),
|
||||
'roles' => $this->userIdentity->identity()->getRoles(),
|
||||
'permissions' => $this->userIdentity->identity()->getPermissions(),
|
||||
'identifier' => $this->identityContext->identifier(),
|
||||
'identity' => $this->identityContext->identity()->getIdentity(),
|
||||
'label' => $this->identityContext->label(),
|
||||
'roles' => $this->identityContext->identity()->getRoles(),
|
||||
'permissions' => $this->identityContext->identity()->getPermissions(),
|
||||
],
|
||||
'profile' => $this->userService->getEditableFields($this->userIdentity->identifier()),
|
||||
'profile' => $this->userService->getEditableFields($this->identityContext->identifier()),
|
||||
'settings' => $this->userService->fetchSettings([], true),
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\TenantService;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
@@ -19,7 +19,7 @@ use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
class TenantSettingsController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly TenantService $tenantService,
|
||||
) {}
|
||||
|
||||
@@ -36,7 +36,7 @@ class TenantSettingsController extends ControllerAbstract
|
||||
)]
|
||||
public function read(): JsonResponse
|
||||
{
|
||||
$settings = $this->tenantService->fetchSettings($this->tenantIdentity->identifier());
|
||||
$settings = $this->tenantService->fetchSettings($this->tenantContext->identifier());
|
||||
|
||||
return new JsonResponse($settings, JsonResponse::HTTP_OK);
|
||||
}
|
||||
@@ -65,9 +65,9 @@ class TenantSettingsController extends ControllerAbstract
|
||||
)]
|
||||
public function update(array $data): JsonResponse
|
||||
{
|
||||
$this->tenantService->storeSettings($this->tenantIdentity->identifier(), $data);
|
||||
$this->tenantService->storeSettings($this->tenantContext->identifier(), $data);
|
||||
|
||||
$updatedSettings = $this->tenantService->fetchSettings($this->tenantIdentity->identifier(), array_keys($data));
|
||||
$updatedSettings = $this->tenantService->fetchSettings($this->tenantContext->identifier(), array_keys($data));
|
||||
|
||||
return new JsonResponse($updatedSettings, JsonResponse::HTTP_OK);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -17,8 +17,8 @@ use Psr\Log\LoggerInterface;
|
||||
class UserAccountsController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly LoggerInterface $logger
|
||||
) {}
|
||||
@@ -31,7 +31,7 @@ class UserAccountsController extends ControllerAbstract
|
||||
{
|
||||
try {
|
||||
// Check admin permission
|
||||
if (!$this->userIdentity->hasPermission('user.admin')) {
|
||||
if (!$this->identityContext->hasPermission('user.admin')) {
|
||||
return new JsonResponse([
|
||||
'status' => 'error',
|
||||
'data' => ['code' => 403, 'message' => 'Insufficient permissions']
|
||||
@@ -125,7 +125,7 @@ class UserAccountsController extends ControllerAbstract
|
||||
*/
|
||||
private function userCreate(array $data): array
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.create')) {
|
||||
if (!$this->identityContext->hasPermission('user.create')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to create users');
|
||||
}
|
||||
|
||||
@@ -142,9 +142,9 @@ class UserAccountsController extends ControllerAbstract
|
||||
];
|
||||
|
||||
$this->logger->info('Creating user', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'identity' => $userData['identity'],
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
'actor' => $this->identityContext->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->createUser($userData);
|
||||
@@ -155,7 +155,7 @@ class UserAccountsController extends ControllerAbstract
|
||||
*/
|
||||
private function userUpdate(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.update')) {
|
||||
if (!$this->identityContext->hasPermission('user.update')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to update users');
|
||||
}
|
||||
|
||||
@@ -186,9 +186,9 @@ class UserAccountsController extends ControllerAbstract
|
||||
}
|
||||
|
||||
$this->logger->info('Updating user', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'uid' => $uid,
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
'actor' => $this->identityContext->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->updateUser($uid, $updates);
|
||||
@@ -199,21 +199,21 @@ class UserAccountsController extends ControllerAbstract
|
||||
*/
|
||||
private function userDelete(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.delete')) {
|
||||
if (!$this->identityContext->hasPermission('user.delete')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to delete users');
|
||||
}
|
||||
|
||||
$uid = $data['uid'] ?? throw new \InvalidArgumentException('User ID required');
|
||||
|
||||
// Prevent self-deletion
|
||||
if ($uid === $this->userIdentity->identifier()) {
|
||||
if ($uid === $this->identityContext->identifier()) {
|
||||
throw new \InvalidArgumentException('Cannot delete your own account');
|
||||
}
|
||||
|
||||
$this->logger->info('Deleting user', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'uid' => $uid,
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
'actor' => $this->identityContext->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->deleteUser($uid);
|
||||
@@ -228,7 +228,7 @@ class UserAccountsController extends ControllerAbstract
|
||||
*/
|
||||
private function userProviderUnlink(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.admin')) {
|
||||
if (!$this->identityContext->hasPermission('user.admin')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions');
|
||||
}
|
||||
|
||||
@@ -241,9 +241,9 @@ class UserAccountsController extends ControllerAbstract
|
||||
];
|
||||
|
||||
$this->logger->info('Unlinking provider', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'uid' => $uid,
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
'actor' => $this->identityContext->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->updateUser($uid, $updates);
|
||||
|
||||
@@ -4,16 +4,16 @@ namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
class UserProfileController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserAccountsService $userService
|
||||
) {}
|
||||
|
||||
@@ -30,7 +30,7 @@ class UserProfileController extends ControllerAbstract
|
||||
)]
|
||||
public function read(): JsonResponse
|
||||
{
|
||||
$userId = $this->userIdentity->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
|
||||
// Get profile with editability metadata
|
||||
$profile = $this->userService->getEditableFields($userId);
|
||||
@@ -63,7 +63,7 @@ class UserProfileController extends ControllerAbstract
|
||||
)]
|
||||
public function update(array $data): JsonResponse
|
||||
{
|
||||
$userId = $this->userIdentity->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
|
||||
// storeProfile automatically filters out provider-managed fields
|
||||
$this->userService->storeProfile($userId, $data);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Service\UserRolesService;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
@@ -17,8 +17,8 @@ use Psr\Log\LoggerInterface;
|
||||
class UserRolesController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserRolesService $roleService,
|
||||
private readonly LoggerInterface $logger
|
||||
) {}
|
||||
@@ -31,7 +31,7 @@ class UserRolesController extends ControllerAbstract
|
||||
{
|
||||
try {
|
||||
// Check role admin permission
|
||||
if (!$this->userIdentity->hasPermission('role.admin')) {
|
||||
if (!$this->identityContext->hasPermission('role.admin')) {
|
||||
return new JsonResponse([
|
||||
'status' => 'error',
|
||||
'data' => ['code' => 403, 'message' => 'Insufficient permissions']
|
||||
@@ -137,7 +137,7 @@ class UserRolesController extends ControllerAbstract
|
||||
*/
|
||||
private function roleCreate(array $data): array
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('role.manage')) {
|
||||
if (!$this->identityContext->hasPermission('role.manage')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to create roles');
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ class UserRolesController extends ControllerAbstract
|
||||
*/
|
||||
private function roleUpdate(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('role.manage')) {
|
||||
if (!$this->identityContext->hasPermission('role.manage')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to update roles');
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ class UserRolesController extends ControllerAbstract
|
||||
*/
|
||||
private function roleDelete(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('role.manage')) {
|
||||
if (!$this->identityContext->hasPermission('role.manage')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to delete roles');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ namespace KTXC\Controllers;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
class UserSettingsController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserAccountsService $userService
|
||||
) {}
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ namespace KTXC\Http\Middleware;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Service\SecurityService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\Context\IdentityContext;
|
||||
use KTXF\Cache\BlobCacheInterface;
|
||||
use KTXF\Cache\EphemeralCacheInterface;
|
||||
use KTXF\Cache\PersistentCacheInterface;
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
@@ -19,7 +22,10 @@ class AuthenticationMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SecurityService $securityService,
|
||||
private readonly SessionIdentity $sessionIdentity
|
||||
private readonly IdentityContext $identityContext,
|
||||
private readonly EphemeralCacheInterface $ephemeralCache,
|
||||
private readonly PersistentCacheInterface $persistentCache,
|
||||
private readonly BlobCacheInterface $blobCache,
|
||||
) {}
|
||||
|
||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||
@@ -29,7 +35,11 @@ class AuthenticationMiddleware implements MiddlewareInterface
|
||||
|
||||
// Initialize session identity if authentication succeeded
|
||||
if ($identity) {
|
||||
$this->sessionIdentity->initialize($identity, true);
|
||||
$this->identityContext->initialize($identity);
|
||||
$identityId = $this->identityContext->identifier();
|
||||
$this->ephemeralCache->setUserContext($identityId);
|
||||
$this->persistentCache->setUserContext($identityId);
|
||||
$this->blobCache->setUserContext($identityId);
|
||||
}
|
||||
|
||||
// Continue to next middleware (authentication is optional at this stage)
|
||||
|
||||
@@ -6,7 +6,7 @@ use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Routing\Router;
|
||||
use KTXC\Routing\Route;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Security\Authorization\PermissionChecker;
|
||||
|
||||
/**
|
||||
@@ -17,7 +17,7 @@ class RouterMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Router $router,
|
||||
private readonly SessionIdentity $sessionIdentity,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly PermissionChecker $permissionChecker
|
||||
) {}
|
||||
|
||||
@@ -32,7 +32,7 @@ class RouterMiddleware implements MiddlewareInterface
|
||||
}
|
||||
|
||||
// Check if route requires authentication
|
||||
if ($match->authenticated && $this->sessionIdentity->identity() === null) {
|
||||
if ($match->authenticated && $this->identityContext->identity() === null) {
|
||||
return new Response(
|
||||
Response::$statusTexts[Response::HTTP_UNAUTHORIZED],
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
|
||||
@@ -4,7 +4,10 @@ namespace KTXC\Http\Middleware;
|
||||
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXF\Cache\BlobCacheInterface;
|
||||
use KTXF\Cache\EphemeralCacheInterface;
|
||||
use KTXF\Cache\PersistentCacheInterface;
|
||||
|
||||
/**
|
||||
* Tenant resolution middleware
|
||||
@@ -13,16 +16,23 @@ use KTXC\SessionTenant;
|
||||
class TenantMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $sessionTenant
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly EphemeralCacheInterface $ephemeralCache,
|
||||
private readonly PersistentCacheInterface $persistentCache,
|
||||
private readonly BlobCacheInterface $blobCache,
|
||||
) {}
|
||||
|
||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||
{
|
||||
// Configure tenant from request host
|
||||
$this->sessionTenant->configure($request->getHost());
|
||||
$this->tenantContext->resolveDomain($request->getHost());
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$this->ephemeralCache->setTenantContext($tenantId);
|
||||
$this->persistentCache->setTenantContext($tenantId);
|
||||
$this->blobCache->setTenantContext($tenantId);
|
||||
|
||||
// Check if tenant is configured and enabled
|
||||
if (!$this->sessionTenant->configured() || !$this->sessionTenant->enabled()) {
|
||||
if (!$this->tenantContext->configured() || !$this->tenantContext->enabled()) {
|
||||
return new Response(
|
||||
Response::$statusTexts[Response::HTTP_UNAUTHORIZED],
|
||||
Response::HTTP_UNAUTHORIZED
|
||||
|
||||
+140
-124
@@ -9,13 +9,15 @@
|
||||
|
||||
namespace KTXC;
|
||||
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Http\Middleware\MiddlewarePipeline;
|
||||
use KTXC\Http\Middleware\TenantMiddleware;
|
||||
use KTXC\Http\Middleware\FirewallMiddleware;
|
||||
use KTXC\Http\Middleware\AuthenticationMiddleware;
|
||||
use KTXC\Http\Middleware\RouterMiddleware;
|
||||
use KTXC\Application\KernelOptions;
|
||||
use KTXC\Application\Execution\ExecutionDescriptor;
|
||||
use KTXC\Application\Execution\ExecutionOutcome;
|
||||
use KTXC\Application\Execution\ExecutionScope;
|
||||
use KTXC\Application\Execution\TerminationReport;
|
||||
use KTXC\Context\IdentityContext;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Injection\Builder;
|
||||
use KTXC\Injection\Container;
|
||||
use Psr\Container\ContainerInterface;
|
||||
@@ -23,7 +25,10 @@ use KTXC\Module\ModuleManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use KTXC\Logger\LoggerFactory;
|
||||
use KTXC\Logger\TenantAwareLogger;
|
||||
use KTXF\Event\EventBus;
|
||||
use KTXF\Event\DeferredEventProcessorInterface;
|
||||
use KTXF\Event\EventDispatcher;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\EventListenerRegistry;
|
||||
use KTXF\Cache\EphemeralCacheInterface;
|
||||
use KTXF\Cache\PersistentCacheInterface;
|
||||
use KTXF\Cache\BlobCacheInterface;
|
||||
@@ -31,7 +36,7 @@ use KTXF\Cache\Store\FileEphemeralCache;
|
||||
use KTXF\Cache\Store\FilePersistentCache;
|
||||
use KTXF\Cache\Store\FileBlobCache;
|
||||
|
||||
class Kernel
|
||||
class Kernel implements KernelInterface
|
||||
{
|
||||
public const VERSION = '1.0.0';
|
||||
public const VERSION_ID = 10000;
|
||||
@@ -45,26 +50,16 @@ class Kernel
|
||||
protected ?float $startTime = null;
|
||||
protected ?ContainerInterface $container = null;
|
||||
protected ?LoggerInterface $logger = null;
|
||||
protected ?MiddlewarePipeline $pipeline = null;
|
||||
private bool $errorHandlerInstalled = false;
|
||||
private ?ExecutionScope $activeScope = null;
|
||||
|
||||
private string $projectDir;
|
||||
private array $config;
|
||||
|
||||
public function __construct(
|
||||
protected string $environment = 'prod',
|
||||
protected bool $debug = false,
|
||||
private readonly KernelOptions $options,
|
||||
array $config = [],
|
||||
?string $projectDir = null,
|
||||
) {
|
||||
if (!$environment) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this)));
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
|
||||
if ($projectDir !== null) {
|
||||
$this->projectDir = $projectDir;
|
||||
}
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
@@ -77,10 +72,10 @@ class Kernel
|
||||
private function initialize(): void
|
||||
{
|
||||
|
||||
if ($this->debug) {
|
||||
if ($this->debug()) {
|
||||
$this->startTime = microtime(true);
|
||||
}
|
||||
if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) {
|
||||
if ($this->debug() && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) {
|
||||
if (\function_exists('putenv')) {
|
||||
putenv('SHELL_VERBOSITY=3');
|
||||
}
|
||||
@@ -129,23 +124,7 @@ class Kernel
|
||||
return true;
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions
|
||||
set_exception_handler(function (\Throwable $exception) {
|
||||
$this->logger->error('Exception caught: ' . $exception->getMessage(), [
|
||||
'exception' => $exception,
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
]);
|
||||
|
||||
if ($this->debug) {
|
||||
echo '<pre>Uncaught Exception: ' . $exception . '</pre>';
|
||||
} else {
|
||||
echo 'An unexpected error occurred. Please try again later.';
|
||||
}
|
||||
|
||||
exit(1);
|
||||
});
|
||||
$this->errorHandlerInstalled = true;
|
||||
|
||||
// Handle fatal errors
|
||||
register_shutdown_function(function () {
|
||||
@@ -161,11 +140,6 @@ class Kernel
|
||||
|
||||
$this->logger->error($message, $error);
|
||||
|
||||
if ($this->debug) {
|
||||
echo '<pre>' . $message . '</pre>';
|
||||
} else {
|
||||
echo 'A fatal error occurred. Please try again later.';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -180,9 +154,9 @@ class Kernel
|
||||
/** @var ModuleManager $moduleManager */
|
||||
$moduleManager = $this->container->get(ModuleManager::class);
|
||||
$moduleManager->modulesBoot();
|
||||
|
||||
// Build middleware pipeline
|
||||
$this->pipeline = $this->buildMiddlewarePipeline();
|
||||
$this->container
|
||||
->get(EventListenerRegistry::class)
|
||||
->freeze($this->container);
|
||||
|
||||
$this->booted = true;
|
||||
}
|
||||
@@ -199,52 +173,108 @@ class Kernel
|
||||
if (false === $this->initialized) {
|
||||
return;
|
||||
}
|
||||
if ($this->activeScope !== null && !$this->activeScope->terminated()) {
|
||||
throw new \LogicException('Cannot shut down the kernel while an execution scope is active.');
|
||||
}
|
||||
|
||||
$this->initialized = false;
|
||||
$this->booted = false;
|
||||
$this->container = null;
|
||||
if ($this->errorHandlerInstalled) {
|
||||
restore_error_handler();
|
||||
$this->errorHandlerInstalled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope
|
||||
{
|
||||
if (!$this->booted) {
|
||||
$this->boot();
|
||||
}
|
||||
if ($this->activeScope !== null) {
|
||||
throw new \LogicException('The kernel already has an active execution scope.');
|
||||
}
|
||||
|
||||
// Use middleware pipeline to handle the request
|
||||
return $this->pipeline->handle($request);
|
||||
$scope = new ExecutionScope(
|
||||
$descriptor,
|
||||
\KTXC\Application\Execution\ExecutionContext::fromDescriptor($descriptor),
|
||||
$this->container->get(TenantContext::class),
|
||||
$this->container->get(IdentityContext::class),
|
||||
);
|
||||
$this->container
|
||||
->get(DeferredEventProcessorInterface::class)
|
||||
->beginExecution($descriptor->executionId);
|
||||
$this->activeScope = $scope;
|
||||
|
||||
return $scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the middleware pipeline
|
||||
*/
|
||||
protected function buildMiddlewarePipeline(): MiddlewarePipeline
|
||||
public function terminateExecution(
|
||||
ExecutionScope $scope,
|
||||
ExecutionOutcome $outcome,
|
||||
): TerminationReport
|
||||
{
|
||||
$pipeline = new MiddlewarePipeline($this->container);
|
||||
|
||||
// Register middleware in execution order
|
||||
$pipeline->pipe(TenantMiddleware::class);
|
||||
$pipeline->pipe(FirewallMiddleware::class);
|
||||
$pipeline->pipe(AuthenticationMiddleware::class);
|
||||
$pipeline->pipe(RouterMiddleware::class);
|
||||
|
||||
return $pipeline;
|
||||
}
|
||||
if ($scope->terminated()) {
|
||||
return new TerminationReport();
|
||||
}
|
||||
if ($this->activeScope !== $scope) {
|
||||
throw new \LogicException('Cannot terminate a scope that is not active.');
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$remaining = 0;
|
||||
$deadlineExceeded = false;
|
||||
$limitExceeded = false;
|
||||
$failures = [];
|
||||
|
||||
/**
|
||||
* Process deferred events at the end of the request
|
||||
*/
|
||||
public function processEvents(): void
|
||||
{
|
||||
try {
|
||||
if ($this->container && $this->container->has(EventBus::class)) {
|
||||
/** @var EventBus $eventBus */
|
||||
$eventBus = $this->container->get(EventBus::class);
|
||||
$eventBus->processDeferred();
|
||||
if ($this->container && $this->container->has(DeferredEventProcessorInterface::class)) {
|
||||
$result = $this->container
|
||||
->get(DeferredEventProcessorInterface::class)
|
||||
->processDeferred($scope->descriptor->executionId);
|
||||
$processed = $result->processed;
|
||||
$remaining = $result->remaining;
|
||||
$deadlineExceeded = $result->deadlineExceeded;
|
||||
$limitExceeded = $result->limitExceeded;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('Event processing error: ' . $e->getMessage());
|
||||
$failures[] = $e;
|
||||
try {
|
||||
$this->container
|
||||
?->get(DeferredEventProcessorInterface::class)
|
||||
->discardDeferred($scope->descriptor->executionId);
|
||||
} catch (\Throwable $discardError) {
|
||||
$failures[] = $discardError;
|
||||
}
|
||||
$this->logger?->error('Deferred event processing failed.', [
|
||||
'exception' => $e,
|
||||
'execution_id' => $scope->descriptor->executionId,
|
||||
'runtime' => $scope->descriptor->runtime->value,
|
||||
]);
|
||||
} finally {
|
||||
if ($this->container !== null) {
|
||||
foreach ([
|
||||
EphemeralCacheInterface::class,
|
||||
PersistentCacheInterface::class,
|
||||
BlobCacheInterface::class,
|
||||
] as $cacheType) {
|
||||
$cache = $this->container->get($cacheType);
|
||||
$cache->setUserContext(null);
|
||||
$cache->setTenantContext(null);
|
||||
}
|
||||
}
|
||||
$scope->dispose();
|
||||
$scope->markTerminated();
|
||||
$this->activeScope = null;
|
||||
}
|
||||
|
||||
return new TerminationReport(
|
||||
deferredProcessed: $processed,
|
||||
deferredRemaining: $remaining,
|
||||
failures: $failures,
|
||||
deadlineExceeded: $deadlineExceeded,
|
||||
limitExceeded: $limitExceeded,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,13 +286,13 @@ class Kernel
|
||||
{
|
||||
return [
|
||||
'kernel.project_dir' => realpath($this->folderRoot()) ?: $this->folderRoot(),
|
||||
'kernel.environment' => $this->environment,
|
||||
'kernel.environment' => $this->environment(),
|
||||
'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%',
|
||||
'kernel.runtime_mode' => '%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%',
|
||||
'kernel.runtime_mode.web' => '%env(bool:default::key:web:default:kernel.runtime_mode:)%',
|
||||
'kernel.runtime_mode.cli' => '%env(not:default:kernel.runtime_mode.web:)%',
|
||||
'kernel.runtime_mode.worker' => '%env(bool:default::key:worker:default:kernel.runtime_mode:)%',
|
||||
'kernel.debug' => $this->debug,
|
||||
'kernel.debug' => $this->debug(),
|
||||
'kernel.build_dir' => realpath($this->getBuildDir()) ?: $this->getBuildDir(),
|
||||
'kernel.cache_dir' => realpath($this->getCacheDir()) ?: $this->getCacheDir(),
|
||||
'kernel.logs_dir' => realpath($this->getLogDir()) ?: $this->getLogDir(),
|
||||
@@ -272,12 +302,12 @@ class Kernel
|
||||
|
||||
public function environment(): string
|
||||
{
|
||||
return $this->environment;
|
||||
return $this->options->environment;
|
||||
}
|
||||
|
||||
public function debug(): bool
|
||||
{
|
||||
return $this->debug;
|
||||
return $this->options->debug;
|
||||
}
|
||||
|
||||
public function container(): ContainerInterface
|
||||
@@ -291,7 +321,7 @@ class Kernel
|
||||
|
||||
public function getStartTime(): float
|
||||
{
|
||||
return $this->debug && null !== $this->startTime ? $this->startTime : -\INF;
|
||||
return $this->debug() && null !== $this->startTime ? $this->startTime : -\INF;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,24 +329,7 @@ class Kernel
|
||||
*/
|
||||
public function folderRoot(): string
|
||||
{
|
||||
if (!isset($this->projectDir)) {
|
||||
$r = new \ReflectionObject($this);
|
||||
|
||||
if (!is_file($dir = $r->getFileName())) {
|
||||
throw new \LogicException(\sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name));
|
||||
}
|
||||
|
||||
$dir = $rootDir = \dirname($dir);
|
||||
while (!is_file($dir.'/composer.json')) {
|
||||
if ($dir === \dirname($dir)) {
|
||||
return $this->projectDir = $rootDir;
|
||||
}
|
||||
$dir = \dirname($dir);
|
||||
}
|
||||
$this->projectDir = $dir;
|
||||
}
|
||||
|
||||
return $this->projectDir;
|
||||
return $this->options->paths->project;
|
||||
}
|
||||
|
||||
|
||||
@@ -325,12 +338,12 @@ class Kernel
|
||||
*/
|
||||
private function getConfigDir(): string
|
||||
{
|
||||
return $this->folderRoot().'/config';
|
||||
return $this->options->paths->configuration();
|
||||
}
|
||||
|
||||
public function getCacheDir(): string
|
||||
{
|
||||
return $this->folderRoot().'/var/cache/'.$this->environment;
|
||||
return $this->options->paths->cache($this->environment());
|
||||
}
|
||||
|
||||
public function getBuildDir(): string
|
||||
@@ -340,7 +353,7 @@ class Kernel
|
||||
|
||||
public function getLogDir(): string
|
||||
{
|
||||
return $this->folderRoot().'/var/log';
|
||||
return $this->options->paths->logs();
|
||||
}
|
||||
|
||||
public function getCharset(): string
|
||||
@@ -382,7 +395,7 @@ class Kernel
|
||||
// Service definitions
|
||||
$projectDir = $this->folderRoot();
|
||||
$moduleDir = $projectDir . '/modules';
|
||||
$environment = $this->environment;
|
||||
$environment = $this->environment();
|
||||
|
||||
$builder->addDefinitions([
|
||||
|
||||
@@ -395,6 +408,9 @@ class Kernel
|
||||
// Without this alias, PHP-DI will happily autowire a new empty Container when asked
|
||||
Container::class => \DI\get(ContainerInterface::class),
|
||||
|
||||
TenantContextInterface::class => \DI\get(TenantContext::class),
|
||||
IdentityContextInterface::class => \DI\get(IdentityContext::class),
|
||||
|
||||
LoggerInterface::class => function (ContainerInterface $c) use ($projectDir) {
|
||||
$logConfig = $this->config['log'] ?? [];
|
||||
|
||||
@@ -405,7 +421,7 @@ class Kernel
|
||||
|
||||
return new TenantAwareLogger(
|
||||
$this->logger,
|
||||
$c->get(SessionTenant::class),
|
||||
$c->get(TenantContextInterface::class),
|
||||
$logDir,
|
||||
$channel,
|
||||
$level,
|
||||
@@ -413,8 +429,8 @@ class Kernel
|
||||
);
|
||||
},
|
||||
|
||||
// EventBus as singleton for consistent event handling
|
||||
EventBus::class => \DI\create(EventBus::class),
|
||||
EventDispatcherInterface::class => \DI\get(EventDispatcher::class),
|
||||
DeferredEventProcessorInterface::class => \DI\get(EventDispatcher::class),
|
||||
// Ephemeral Cache - for short-lived data (sessions, rate limits, challenges)
|
||||
EphemeralCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
|
||||
$storeType = $c->has('cache.ephemeral') ? $c->get('cache.ephemeral') : 'file';
|
||||
@@ -433,13 +449,13 @@ class Kernel
|
||||
$cache = new $storeClass($projectDir);
|
||||
|
||||
// Set tenant/user context if available
|
||||
if ($c->has(SessionTenant::class)) {
|
||||
$tenant = $c->get(SessionTenant::class);
|
||||
$cache->setTenantContext($tenant->identifier());
|
||||
if ($c->has(TenantContextInterface::class)) {
|
||||
$tenantContext = $c->get(TenantContextInterface::class);
|
||||
$cache->setTenantContext($tenantContext->identifier());
|
||||
}
|
||||
if ($c->has(SessionIdentity::class)) {
|
||||
$identity = $c->get(SessionIdentity::class);
|
||||
$cache->setUserContext($identity->identifier());
|
||||
if ($c->has(IdentityContextInterface::class)) {
|
||||
$identityContext = $c->get(IdentityContextInterface::class);
|
||||
$cache->setUserContext($identityContext->identifier());
|
||||
}
|
||||
|
||||
return $cache;
|
||||
@@ -462,13 +478,13 @@ class Kernel
|
||||
$cache = new $storeClass($projectDir);
|
||||
|
||||
// Set tenant/user context if available
|
||||
if ($c->has(SessionTenant::class)) {
|
||||
$tenant = $c->get(SessionTenant::class);
|
||||
$cache->setTenantContext($tenant->identifier());
|
||||
if ($c->has(TenantContextInterface::class)) {
|
||||
$tenantContext = $c->get(TenantContextInterface::class);
|
||||
$cache->setTenantContext($tenantContext->identifier());
|
||||
}
|
||||
if ($c->has(SessionIdentity::class)) {
|
||||
$identity = $c->get(SessionIdentity::class);
|
||||
$cache->setUserContext($identity->identifier());
|
||||
if ($c->has(IdentityContextInterface::class)) {
|
||||
$identityContext = $c->get(IdentityContextInterface::class);
|
||||
$cache->setUserContext($identityContext->identifier());
|
||||
}
|
||||
|
||||
return $cache;
|
||||
@@ -491,13 +507,13 @@ class Kernel
|
||||
$cache = new $storeClass($projectDir);
|
||||
|
||||
// Set tenant/user context if available
|
||||
if ($c->has(SessionTenant::class)) {
|
||||
$tenant = $c->get(SessionTenant::class);
|
||||
$cache->setTenantContext($tenant->identifier());
|
||||
if ($c->has(TenantContextInterface::class)) {
|
||||
$tenantContext = $c->get(TenantContextInterface::class);
|
||||
$cache->setTenantContext($tenantContext->identifier());
|
||||
}
|
||||
if ($c->has(SessionIdentity::class)) {
|
||||
$identity = $c->get(SessionIdentity::class);
|
||||
$cache->setUserContext($identity->identifier());
|
||||
if ($c->has(IdentityContextInterface::class)) {
|
||||
$identityContext = $c->get(IdentityContextInterface::class);
|
||||
$cache->setUserContext($identityContext->identifier());
|
||||
}
|
||||
|
||||
return $cache;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC;
|
||||
|
||||
use KTXC\Application\Execution\ExecutionDescriptor;
|
||||
use KTXC\Application\Execution\ExecutionOutcome;
|
||||
use KTXC\Application\Execution\ExecutionScope;
|
||||
use KTXC\Application\Execution\TerminationReport;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
interface KernelInterface
|
||||
{
|
||||
public function boot(): void;
|
||||
|
||||
public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope;
|
||||
|
||||
public function terminateExecution(
|
||||
ExecutionScope $scope,
|
||||
ExecutionOutcome $outcome,
|
||||
): TerminationReport;
|
||||
|
||||
public function shutdown(): void;
|
||||
|
||||
public function container(): ContainerInterface;
|
||||
|
||||
public function environment(): string;
|
||||
|
||||
public function debug(): bool;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ use Psr\Log\NullLogger;
|
||||
*
|
||||
* Per-tenant routing (TenantAwareLogger) is applied separately inside the
|
||||
* DI container definition in Kernel::configureContainer(), because that
|
||||
* requires access to the SessionTenant singleton which lives in the container.
|
||||
* requires access to the TenantContextInterface singleton which lives in the container.
|
||||
*
|
||||
* Supported config keys inside $config['log']:
|
||||
*
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace KTXC\Logger;
|
||||
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ use Psr\Log\LoggerInterface;
|
||||
* {logDir}/tenant/{tenantIdentifier}/{channel}.jsonl
|
||||
* Messages that carry tenant "system" always go to the global logger.
|
||||
*
|
||||
* Both behaviours rely on a live SessionTenant reference that is populated lazily
|
||||
* Both behaviours rely on a live TenantContextInterface reference that is populated lazily
|
||||
* by TenantMiddleware — the same pattern the cache stores use in Kernel::configureContainer().
|
||||
*/
|
||||
class TenantAwareLogger implements LoggerInterface
|
||||
@@ -30,7 +30,7 @@ class TenantAwareLogger implements LoggerInterface
|
||||
|
||||
/**
|
||||
* @param LoggerInterface $globalLogger Fallback logger (also used when perTenant = false).
|
||||
* @param SessionTenant $sessionTenant Live reference configured by TenantMiddleware.
|
||||
* @param TenantContextInterface $tenantContext Live reference configured by TenantMiddleware.
|
||||
* @param string $logDir Base log directory (e.g. /var/www/app/var/log).
|
||||
* @param string $channel Log file basename (e.g. 'app' → app.jsonl).
|
||||
* @param string $minLevel Minimum PSR-3 level for lazily-created per-tenant loggers.
|
||||
@@ -38,7 +38,7 @@ class TenantAwareLogger implements LoggerInterface
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly LoggerInterface $globalLogger,
|
||||
private readonly SessionTenant $sessionTenant,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly string $logDir,
|
||||
private readonly string $channel = 'app',
|
||||
private readonly string $minLevel = 'debug',
|
||||
@@ -60,8 +60,8 @@ class TenantAwareLogger implements LoggerInterface
|
||||
{
|
||||
// Resolve current tenant id; fall back to 'system' for CLI / boot phase.
|
||||
$tenantId = 'system';
|
||||
if ($this->sessionTenant->configured()) {
|
||||
$tenantId = $this->sessionTenant->identifier() ?? 'system';
|
||||
if ($this->tenantContext->configured()) {
|
||||
$tenantId = $this->tenantContext->identifier() ?? 'system';
|
||||
}
|
||||
|
||||
// Inject tenant id as a reserved context key that concrete loggers extract.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
namespace KTXC\Module;
|
||||
|
||||
use KTXC\Service\FirewallService;
|
||||
use KTXF\Event\DeliveryMode;
|
||||
use KTXF\Event\EventListenerRegistry;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
use KTXF\Module\ModuleBrowserInterface;
|
||||
use KTXF\Module\ModuleConsoleInterface;
|
||||
use KTXF\Module\ModuleInstanceAbstract;
|
||||
@@ -13,7 +17,36 @@ use KTXF\Module\ModuleInstanceAbstract;
|
||||
*/
|
||||
class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, ModuleBrowserInterface
|
||||
{
|
||||
public function __construct() {}
|
||||
public function __construct(
|
||||
private readonly EventListenerRegistry $events,
|
||||
) {
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->events->listen(
|
||||
'core',
|
||||
SecurityEvent::AUTH_FAILURE,
|
||||
FirewallService::class,
|
||||
'handleAuthFailure',
|
||||
priority: 100,
|
||||
);
|
||||
|
||||
foreach ([
|
||||
SecurityEvent::AUTH_FAILURE,
|
||||
SecurityEvent::AUTH_SUCCESS,
|
||||
SecurityEvent::ACCESS_DENIED,
|
||||
SecurityEvent::BRUTE_FORCE_DETECTED,
|
||||
] as $event) {
|
||||
$this->events->listen(
|
||||
'core',
|
||||
$event,
|
||||
FirewallService::class,
|
||||
'logSecurityEvent',
|
||||
DeliveryMode::Deferred,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(): string
|
||||
{
|
||||
@@ -99,6 +132,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
public function registerCI(): array
|
||||
{
|
||||
return [
|
||||
\KTXC\Console\Event\EventsDebugCommand::class,
|
||||
\KTXC\Console\Module\ModuleListCommand::class,
|
||||
\KTXC\Console\Module\ModuleEnableCommand::class,
|
||||
\KTXC\Console\Module\ModuleDisableCommand::class,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace KTXC\Module;
|
||||
|
||||
use KTXC\Server;
|
||||
use Composer\Autoload\ClassLoader;
|
||||
|
||||
/**
|
||||
* Custom autoloader for modules that allows PascalCase namespaces
|
||||
@@ -20,7 +20,10 @@ class ModuleAutoloader
|
||||
private array $namespaceMap = [];
|
||||
private bool $scanned = false;
|
||||
|
||||
public function __construct(string $modulesRoot)
|
||||
public function __construct(
|
||||
string $modulesRoot,
|
||||
private readonly ?ClassLoader $composerLoader = null,
|
||||
)
|
||||
{
|
||||
$this->modulesRoot = rtrim($modulesRoot, '/');
|
||||
}
|
||||
@@ -73,10 +76,9 @@ class ModuleAutoloader
|
||||
}
|
||||
|
||||
// Register module namespaces with Composer ClassLoader
|
||||
$composerLoader = Server::getComposerLoader();
|
||||
if ($composerLoader !== null) {
|
||||
if ($this->composerLoader !== null) {
|
||||
foreach ($this->namespaceMap as $namespace => $folderName) {
|
||||
$composerLoader->addPsr4(
|
||||
$this->composerLoader->addPsr4(
|
||||
'KTXM\\' . $namespace . '\\',
|
||||
$this->modulesRoot . '/' . $folderName . '/lib/'
|
||||
);
|
||||
|
||||
@@ -276,12 +276,13 @@ class ModuleManager
|
||||
try {
|
||||
$module->boot();
|
||||
$this->logger->debug('Module booted', ['handle' => $handle]);
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Module boot failed: ' . $handle, [
|
||||
'exception' => $e,
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Runtime\Console;
|
||||
|
||||
use KTXC\Application\Execution\ExecutionDescriptor;
|
||||
use KTXC\Application\Execution\ExecutionOutcome;
|
||||
use KTXC\Kernel;
|
||||
use KTXC\KernelInterface;
|
||||
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;
|
||||
use Symfony\Component\Console\Command\LazyCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class ConsoleRuntime
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(
|
||||
?InputInterface $input = null,
|
||||
?OutputInterface $output = null,
|
||||
): int
|
||||
{
|
||||
$scope = $this->kernel->beginExecution(ExecutionDescriptor::cli());
|
||||
$outcome = ExecutionOutcome::incomplete();
|
||||
|
||||
try {
|
||||
$exitCode = $this->application()->run($input, $output);
|
||||
$outcome = ExecutionOutcome::success($exitCode);
|
||||
|
||||
return $exitCode;
|
||||
} catch (\Throwable $error) {
|
||||
$outcome = ExecutionOutcome::failure($error);
|
||||
throw $error;
|
||||
} finally {
|
||||
$this->kernel->terminateExecution($scope, $outcome);
|
||||
}
|
||||
}
|
||||
|
||||
private function application(): ConsoleApplication
|
||||
{
|
||||
$container = $this->kernel->container();
|
||||
$console = new ConsoleApplication('Vallarx Console', Kernel::VERSION);
|
||||
$console->setAutoExit(false);
|
||||
|
||||
/** @var ModuleManager $moduleManager */
|
||||
$moduleManager = $container->get(ModuleManager::class);
|
||||
foreach ($moduleManager->list() as $module) {
|
||||
$instance = $module->instance();
|
||||
if (!$instance instanceof ModuleConsoleInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($instance->registerCI() as $commandClass) {
|
||||
$this->registerLazyCommand($console, $container, $commandClass);
|
||||
}
|
||||
}
|
||||
|
||||
return $console;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $commandClass
|
||||
*/
|
||||
private function registerLazyCommand(
|
||||
ConsoleApplication $console,
|
||||
ContainerInterface $container,
|
||||
string $commandClass,
|
||||
): void {
|
||||
if (!class_exists($commandClass)) {
|
||||
throw new \RuntimeException("Command class not found: {$commandClass}");
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($commandClass);
|
||||
$attributes = $reflection->getAttributes(AsCommand::class);
|
||||
if ($attributes === []) {
|
||||
throw new \RuntimeException("Command {$commandClass} is missing #[AsCommand].");
|
||||
}
|
||||
|
||||
$attribute = $attributes[0]->newInstance();
|
||||
$console->add(new LazyCommand(
|
||||
$attribute->name,
|
||||
[],
|
||||
$attribute->description ?? '',
|
||||
$attribute->hidden ?? false,
|
||||
static fn() => $container->get($commandClass),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Runtime\Http;
|
||||
|
||||
use KTXC\Application\Execution\ExecutionDescriptor;
|
||||
use KTXC\Application\Execution\ExecutionOutcome;
|
||||
use KTXC\Http\Middleware\AuthenticationMiddleware;
|
||||
use KTXC\Http\Middleware\FirewallMiddleware;
|
||||
use KTXC\Http\Middleware\MiddlewarePipeline;
|
||||
use KTXC\Http\Middleware\RouterMiddleware;
|
||||
use KTXC\Http\Middleware\TenantMiddleware;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\KernelInterface;
|
||||
|
||||
final class HttpRuntime
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly bool $debug,
|
||||
) {
|
||||
}
|
||||
|
||||
public function run(?Request $request = null): Response
|
||||
{
|
||||
return $this->execute(
|
||||
$request ?? Request::createFromGlobals(),
|
||||
send: true,
|
||||
);
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return $this->execute($request, send: false);
|
||||
}
|
||||
|
||||
private function execute(Request $request, bool $send): Response
|
||||
{
|
||||
$scope = null;
|
||||
$outcome = ExecutionOutcome::incomplete();
|
||||
|
||||
try {
|
||||
$scope = $this->kernel->beginExecution(ExecutionDescriptor::http());
|
||||
$response = $this->pipeline()->handle($request);
|
||||
$outcome = ExecutionOutcome::success($response);
|
||||
if ($send) {
|
||||
$response->send();
|
||||
}
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable $error) {
|
||||
$response = $this->errorResponse($error);
|
||||
$outcome = ExecutionOutcome::failure($error, $response);
|
||||
if ($send) {
|
||||
$response->send();
|
||||
}
|
||||
|
||||
return $response;
|
||||
} finally {
|
||||
if ($scope !== null) {
|
||||
$this->kernel->terminateExecution($scope, $outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function pipeline(): MiddlewarePipeline
|
||||
{
|
||||
$pipeline = new MiddlewarePipeline($this->kernel->container());
|
||||
$pipeline->pipe(TenantMiddleware::class);
|
||||
$pipeline->pipe(FirewallMiddleware::class);
|
||||
$pipeline->pipe(AuthenticationMiddleware::class);
|
||||
$pipeline->pipe(RouterMiddleware::class);
|
||||
|
||||
return $pipeline;
|
||||
}
|
||||
|
||||
private function errorResponse(\Throwable $error): Response
|
||||
{
|
||||
error_log(sprintf(
|
||||
'Application error: %s in %s:%d',
|
||||
$error->getMessage(),
|
||||
$error->getFile(),
|
||||
$error->getLine(),
|
||||
));
|
||||
|
||||
$content = $this->debug
|
||||
? '<pre>' . htmlspecialchars((string) $error) . '</pre>'
|
||||
: 'An error occurred. Please try again later.';
|
||||
|
||||
return new Response($content, Response::HTTP_INTERNAL_SERVER_ERROR, [
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use KTXC\Security\Authentication\AuthenticationRequest;
|
||||
use KTXC\Security\Authentication\AuthenticationResponse;
|
||||
use KTXC\Service\TokenService;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Cache\CacheScope;
|
||||
use KTXF\Cache\EphemeralCacheInterface;
|
||||
use KTXF\Security\Authentication\AuthenticationProviderInterface;
|
||||
@@ -26,13 +26,13 @@ class AuthenticationManager
|
||||
private string $securityCode;
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenant,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly EphemeralCacheInterface $cache,
|
||||
private readonly ProviderManager $providerManager,
|
||||
private readonly TokenService $tokenService,
|
||||
private readonly UserAccountsService $userService,
|
||||
) {
|
||||
$this->securityCode = $this->tenant->configuration()->security()->code();
|
||||
$this->securityCode = $this->tenantContext->configuration()->security()->code();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -75,7 +75,7 @@ class AuthenticationManager
|
||||
$methods = $this->methodsConfigured();
|
||||
|
||||
$session = AuthenticationSession::create(
|
||||
$this->tenant->identifier(),
|
||||
$this->tenantContext->identifier(),
|
||||
AuthenticationSession::STATE_FRESH
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ class AuthenticationManager
|
||||
// Filter to non-redirect methods since redirects don't need identity first
|
||||
$methods = $this->methodsConfigured();
|
||||
$methods = array_values(array_filter($methods, fn($m) => $m['method'] !== 'redirect'));
|
||||
$require = $this->tenant->configuration()->authentication()->methodsMinimal();
|
||||
$require = $this->tenantContext->configuration()->authentication()->methodsMinimal();
|
||||
|
||||
// Store identity in session without validating to prevent enumeration
|
||||
$session->setMethods(array_column($methods, 'id'), $require);
|
||||
@@ -429,7 +429,7 @@ class AuthenticationManager
|
||||
$session->methodCompleted($method);
|
||||
|
||||
// Check if MFA is required
|
||||
$require = $this->tenant->configuration()->authentication()->methodsMinimal();
|
||||
$require = $this->tenantContext->configuration()->authentication()->methodsMinimal();
|
||||
if ($require > 1) {
|
||||
$remainingMethods = $this->methodsConfigured([$method]);
|
||||
// Filter out redirect methods - they can't be used as secondary factors
|
||||
@@ -523,7 +523,7 @@ class AuthenticationManager
|
||||
|
||||
$accessToken = $this->tokenService->createToken(
|
||||
[
|
||||
'tenant' => $this->tenant->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'identifier' => $user->getId(),
|
||||
'identity' => $user->getIdentity(),
|
||||
'label' => $user->getLabel(),
|
||||
@@ -585,7 +585,7 @@ class AuthenticationManager
|
||||
*/
|
||||
private function getProviderConfig(string $method): array
|
||||
{
|
||||
$providers = $this->tenant->configuration()->authentication()->providers();
|
||||
$providers = $this->tenantContext->configuration()->authentication()->providers();
|
||||
return $providers[$method]['config'] ?? [];
|
||||
}
|
||||
|
||||
@@ -635,7 +635,7 @@ class AuthenticationManager
|
||||
*/
|
||||
private function methodsConfigured(array $methodsCompleted = []): array
|
||||
{
|
||||
$tenantProviders = $this->tenant->configuration()->authentication()->providers();
|
||||
$tenantProviders = $this->tenantContext->configuration()->authentication()->providers();
|
||||
$methods = [];
|
||||
|
||||
foreach ($tenantProviders as $providerId => $providerConfiguration) {
|
||||
@@ -669,7 +669,7 @@ class AuthenticationManager
|
||||
private function createTokens(User $user, bool $mfaVerified = false): array
|
||||
{
|
||||
$payload = [
|
||||
'tenant' => $this->tenant->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'identifier' => $user->getId(),
|
||||
'identity' => $user->getIdentity(),
|
||||
'label' => $user->getLabel(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace KTXC\Security\Authorization;
|
||||
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
|
||||
/**
|
||||
* Permission Checker
|
||||
@@ -11,7 +11,7 @@ use KTXC\SessionIdentity;
|
||||
class PermissionChecker
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionIdentity $sessionIdentity
|
||||
private readonly IdentityContextInterface $identityContext
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ class PermissionChecker
|
||||
*/
|
||||
public function can(string $permission, mixed $resource = null): bool
|
||||
{
|
||||
$identity = $this->sessionIdentity->identity();
|
||||
$identity = $this->identityContext->identity();
|
||||
|
||||
if (!$identity) {
|
||||
return false;
|
||||
@@ -113,7 +113,7 @@ class PermissionChecker
|
||||
*/
|
||||
public function getUserPermissions(): array
|
||||
{
|
||||
$identity = $this->sessionIdentity->identity();
|
||||
$identity = $this->identityContext->identity();
|
||||
|
||||
if (!$identity) {
|
||||
return [];
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC;
|
||||
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Module\ModuleAutoloader;
|
||||
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;
|
||||
use Symfony\Component\Console\Command\LazyCommand;
|
||||
|
||||
/**
|
||||
* Server class - entry point for the framework
|
||||
* Handles configuration loading and kernel lifecycle
|
||||
*/
|
||||
class Server
|
||||
{
|
||||
private static $composerLoader = null;
|
||||
private static ?self $instance = null;
|
||||
|
||||
private Kernel $kernel;
|
||||
private array $config;
|
||||
private string $rootDir;
|
||||
|
||||
public function __construct(string $rootDir, ?string $environment = null, ?bool $debug = null)
|
||||
{
|
||||
self::$instance = $this;
|
||||
|
||||
$this->rootDir = $this->resolveProjectRoot($rootDir);
|
||||
|
||||
// Load configuration
|
||||
$this->config = $this->loadConfig();
|
||||
|
||||
// Determine environment and debug mode
|
||||
$environment = $environment ?? $this->config['environment'] ?? 'prod';
|
||||
$debug = $debug ?? $this->config['debug'] ?? false;
|
||||
|
||||
// Create kernel with configuration
|
||||
$this->kernel = new Kernel($environment, $debug, $this->config, $rootDir);
|
||||
|
||||
// Register module autoloader for both HTTP and CLI contexts
|
||||
$moduleAutoloader = new ModuleAutoloader($this->moduleDir());
|
||||
$moduleAutoloader->register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the application - handle incoming request and send response
|
||||
*/
|
||||
public function runHttp(): void
|
||||
{
|
||||
try {
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $this->handle($request);
|
||||
$response->send();
|
||||
$this->terminate();
|
||||
} catch (\Throwable $e) {
|
||||
// Last resort error handling for kernel initialization failures
|
||||
error_log('Application error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
$content = $this->kernel->debug()
|
||||
? '<pre>' . htmlspecialchars((string) $e) . '</pre>'
|
||||
: 'An error occurred. Please try again later.';
|
||||
$response = new Response($content, Response::HTTP_INTERNAL_SERVER_ERROR, [
|
||||
'Content-Type' => 'text/html; charset=UTF-8',
|
||||
]);
|
||||
$response->send();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run as a console application (CLI runtime).
|
||||
*/
|
||||
public function runConsole(): int
|
||||
{
|
||||
$this->kernel()->boot();
|
||||
$container = $this->container();
|
||||
|
||||
$console = new ConsoleApplication('Vallarx Console', Kernel::VERSION);
|
||||
|
||||
/** @var ModuleManager $moduleManager */
|
||||
$moduleManager = $container->get(ModuleManager::class);
|
||||
|
||||
foreach ($moduleManager->list() as $module) {
|
||||
$instance = $module->instance();
|
||||
if (!$instance instanceof ModuleConsoleInterface) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
foreach ($instance->registerCI() as $commandClass) {
|
||||
if (!class_exists($commandClass)) {
|
||||
fwrite(STDERR, "Warning: Command class not found: {$commandClass}\n");
|
||||
continue;
|
||||
}
|
||||
$this->registerLazyCommand($console, $container, $commandClass);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "Warning: Failed to load commands from module {$module->handle()}: {$e->getMessage()}\n");
|
||||
}
|
||||
}
|
||||
|
||||
return $console->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return $this->kernel->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the application - process deferred events
|
||||
*/
|
||||
public function terminate(): void
|
||||
{
|
||||
$this->kernel->processEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the kernel instance
|
||||
*/
|
||||
public function kernel(): Kernel
|
||||
{
|
||||
return $this->kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the container instance
|
||||
*/
|
||||
public function container(): ContainerInterface
|
||||
{
|
||||
return $this->kernel->container();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application root directory
|
||||
*/
|
||||
public function rootDir(): string
|
||||
{
|
||||
return $this->rootDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modules directory
|
||||
*/
|
||||
public function moduleDir(): string
|
||||
{
|
||||
return $this->rootDir . '/modules';
|
||||
}
|
||||
|
||||
public function varDir(): string
|
||||
{
|
||||
return $this->rootDir . '/var';
|
||||
}
|
||||
|
||||
public function logDir(): string
|
||||
{
|
||||
return $this->varDir() . '/logs';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration value
|
||||
*/
|
||||
public function config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
// Support dot notation: 'database.uri'
|
||||
$keys = explode('.', $key);
|
||||
$value = $this->config;
|
||||
|
||||
foreach ($keys as $k) {
|
||||
if (!is_array($value) || !array_key_exists($k, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$k];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment
|
||||
*/
|
||||
public function environment(): string
|
||||
{
|
||||
return $this->kernel->environment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if debug mode is enabled
|
||||
*/
|
||||
public function debug(): bool
|
||||
{
|
||||
return $this->kernel->debug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from config directory
|
||||
*/
|
||||
protected function loadConfig(): array
|
||||
{
|
||||
$configFile = $this->rootDir . '/config/system.php';
|
||||
|
||||
if (!file_exists($configFile)) {
|
||||
error_log('Configuration file not found: ' . $configFile);
|
||||
return [];
|
||||
}
|
||||
|
||||
$config = include $configFile;
|
||||
|
||||
if (!is_array($config)) {
|
||||
throw new \RuntimeException('Configuration file must return an array');
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the project root directory.
|
||||
*
|
||||
* Some entrypoints may pass the public/ directory or another subdirectory.
|
||||
* We walk up the directory tree until we find composer.json.
|
||||
*/
|
||||
private function resolveProjectRoot(string $startDir): string
|
||||
{
|
||||
$dir = rtrim($startDir, '/');
|
||||
if ($dir === '') {
|
||||
return $startDir;
|
||||
}
|
||||
|
||||
// If startDir is a file path, use its directory.
|
||||
if (is_file($dir)) {
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
|
||||
$current = $dir;
|
||||
while (true) {
|
||||
if (is_file($current . '/composer.json')) {
|
||||
return $current;
|
||||
}
|
||||
|
||||
$parent = dirname($current);
|
||||
if ($parent === $current) {
|
||||
// Reached filesystem root
|
||||
return $dir;
|
||||
}
|
||||
$current = $parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Composer ClassLoader instance
|
||||
*/
|
||||
public static function setComposerLoader($loader): void
|
||||
{
|
||||
self::$composerLoader = $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Composer ClassLoader instance
|
||||
*/
|
||||
public static function getComposerLoader()
|
||||
{
|
||||
return self::$composerLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Application instance
|
||||
*/
|
||||
public static function getInstance(): ?self
|
||||
{
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single command via lazy loading using its #[AsCommand] attribute.
|
||||
*/
|
||||
private function registerLazyCommand(
|
||||
ConsoleApplication $console,
|
||||
ContainerInterface $container,
|
||||
string $commandClass
|
||||
): void {
|
||||
try {
|
||||
$ref = new \ReflectionClass($commandClass);
|
||||
$attrs = $ref->getAttributes(AsCommand::class);
|
||||
|
||||
if (empty($attrs)) {
|
||||
fwrite(STDERR, "Warning: Command {$commandClass} missing #[AsCommand] attribute\n");
|
||||
return;
|
||||
}
|
||||
|
||||
$attr = $attrs[0]->newInstance();
|
||||
$console->add(new LazyCommand(
|
||||
$attr->name,
|
||||
[],
|
||||
$attr->description ?? '',
|
||||
$attr->hidden ?? false,
|
||||
fn() => $container->get($commandClass)
|
||||
));
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "Warning: Failed to register command {$commandClass}: {$e->getMessage()}\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace KTXC\Service;
|
||||
use KTXC\Db\DataStore;
|
||||
use KTXC\Db\Collection;
|
||||
use KTXC\Db\UTCDateTime;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
|
||||
class ConfigurationService
|
||||
{
|
||||
@@ -24,7 +24,7 @@ class ConfigurationService
|
||||
|
||||
public function __construct(
|
||||
DataStore $store,
|
||||
private readonly SessionTenant $tenant
|
||||
private readonly TenantContextInterface $tenantContext
|
||||
) {
|
||||
// DataStore provides selectCollection method
|
||||
$this->collection = $store->selectCollection(self::TABLE_NAME);
|
||||
@@ -36,10 +36,10 @@ class ConfigurationService
|
||||
*/
|
||||
public function get(string $path, string $key, mixed $default = null, ?string $tenant = null): mixed
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
if ($tenant === null && !$this->tenantContext->configured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
$tenant = $this->tenantContext->identifier();
|
||||
}
|
||||
|
||||
$doc = $this->collection->findOne(['did' => $tenant, 'path' => $path, 'key' => $key]);
|
||||
@@ -54,10 +54,10 @@ class ConfigurationService
|
||||
*/
|
||||
public function set(string $path, string $key, mixed $value, mixed $default = null, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
if ($tenant === null && !$this->tenantContext->configured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
$tenant = $this->tenantContext->identifier();
|
||||
}
|
||||
|
||||
$type = $this->determineType($value);
|
||||
@@ -84,10 +84,10 @@ class ConfigurationService
|
||||
*/
|
||||
public function getByPath(?string $path = null, bool $subset = false, ?string $tenant = null): array
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
if ($tenant === null && !$this->tenantContext->configured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
$tenant = $this->tenantContext->identifier();
|
||||
}
|
||||
|
||||
$filter = ['did' => $tenant];
|
||||
@@ -116,10 +116,10 @@ class ConfigurationService
|
||||
*/
|
||||
public function delete(string $path, string $key, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
if ($tenant === null && !$this->tenantContext->configured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
$tenant = $this->tenantContext->identifier();
|
||||
}
|
||||
|
||||
$this->collection->deleteOne(['did' => $tenant, 'path' => $path, 'key' => $key]);
|
||||
@@ -131,10 +131,10 @@ class ConfigurationService
|
||||
*/
|
||||
public function deleteByPath(string $path, bool $includeSubPaths = false, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
if ($tenant === null && !$this->tenantContext->configured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
$tenant = $this->tenantContext->identifier();
|
||||
}
|
||||
|
||||
$filter = ['did' => $tenant];
|
||||
@@ -155,10 +155,10 @@ class ConfigurationService
|
||||
*/
|
||||
public function exists(string $path, string $key, ?string $tenant = null): bool
|
||||
{
|
||||
if ($tenant === null && !$this->tenant->isConfigured()) {
|
||||
if ($tenant === null && !$this->tenantContext->configured()) {
|
||||
throw new \InvalidArgumentException('Tenant must be configured or provided explicitly.');
|
||||
} elseif ($tenant === null) {
|
||||
$tenant = $this->tenant->identifier();
|
||||
$tenant = $this->tenantContext->identifier();
|
||||
}
|
||||
|
||||
return $this->collection->countDocuments(['did' => $tenant, 'path' => $path, 'key' => $key]) > 0;
|
||||
|
||||
@@ -8,8 +8,8 @@ use KTXC\Http\Request\Request;
|
||||
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||
use KTXC\Models\Firewall\FirewallLogObject;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Event\EventBus;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
use KTXF\IpUtils;
|
||||
|
||||
@@ -41,33 +41,9 @@ class FirewallService
|
||||
|
||||
public function __construct(
|
||||
private readonly FirewallStore $store,
|
||||
private readonly SessionTenant $tenant,
|
||||
private readonly EventBus $eventBus
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly EventDispatcherInterface $events,
|
||||
) {
|
||||
// Listen for auth failures to detect brute force
|
||||
$this->eventBus->subscribe(
|
||||
SecurityEvent::AUTH_FAILURE,
|
||||
[$this, 'handleAuthFailure'],
|
||||
100 // High priority
|
||||
);
|
||||
|
||||
// Log all security events asynchronously
|
||||
$this->eventBus->subscribeAsync(
|
||||
SecurityEvent::AUTH_FAILURE,
|
||||
[$this, 'logSecurityEvent']
|
||||
);
|
||||
$this->eventBus->subscribeAsync(
|
||||
SecurityEvent::AUTH_SUCCESS,
|
||||
[$this, 'logSecurityEvent']
|
||||
);
|
||||
$this->eventBus->subscribeAsync(
|
||||
SecurityEvent::ACCESS_DENIED,
|
||||
[$this, 'logSecurityEvent']
|
||||
);
|
||||
$this->eventBus->subscribeAsync(
|
||||
SecurityEvent::BRUTE_FORCE_DETECTED,
|
||||
[$this, 'logSecurityEvent']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +76,7 @@ class FirewallService
|
||||
return new FirewallAnalyzeResult(true);
|
||||
}
|
||||
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
return new FirewallAnalyzeResult(true);
|
||||
}
|
||||
@@ -158,7 +134,7 @@ class FirewallService
|
||||
public function handleAuthFailure(SecurityEvent $event): void
|
||||
{
|
||||
$ipAddress = $event->getIpAddress();
|
||||
$tenantId = $event->getTenantId() ?? $this->tenant->identifier();
|
||||
$tenantId = $event->getTenantId() ?? $this->tenantContext->identifier();
|
||||
|
||||
if (!$ipAddress || !$tenantId) {
|
||||
return;
|
||||
@@ -198,8 +174,8 @@ class FirewallService
|
||||
): void {
|
||||
// Publish brute force event
|
||||
$event = SecurityEvent::bruteForceDetected($ipAddress, $failureCount, $windowSeconds);
|
||||
$event->setTenantId($this->tenant->identifier());
|
||||
$this->eventBus->publish($event);
|
||||
$event->setTenantId($this->tenantContext->identifier());
|
||||
$this->events->dispatch($event);
|
||||
|
||||
// Auto-block the IP
|
||||
$blockDuration = $this->getConfig(
|
||||
@@ -220,7 +196,7 @@ class FirewallService
|
||||
*/
|
||||
public function logSecurityEvent(SecurityEvent $event): void
|
||||
{
|
||||
$tenantId = $event->getTenantId() ?? $this->tenant->identifier();
|
||||
$tenantId = $event->getTenantId() ?? $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
return;
|
||||
}
|
||||
@@ -283,8 +259,8 @@ class FirewallService
|
||||
$rule->getId(),
|
||||
$rule->getReason()
|
||||
);
|
||||
$event->setTenantId($this->tenant->identifier());
|
||||
$this->eventBus->publish($event);
|
||||
$event->setTenantId($this->tenantContext->identifier());
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -300,7 +276,7 @@ class FirewallService
|
||||
?string $createdBy = null,
|
||||
?int $durationSeconds = null
|
||||
): FirewallRuleObject {
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
|
||||
}
|
||||
@@ -340,7 +316,7 @@ class FirewallService
|
||||
$event->setIpAddress($ipAddress)
|
||||
->setReason($reason)
|
||||
->setTenantId($tenantId);
|
||||
$this->eventBus->publish($event);
|
||||
$this->events->dispatch($event);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
@@ -353,7 +329,7 @@ class FirewallService
|
||||
?string $reason = null,
|
||||
?string $createdBy = null
|
||||
): FirewallRuleObject {
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
|
||||
}
|
||||
@@ -376,7 +352,7 @@ class FirewallService
|
||||
$event->setIpAddress($ipAddress)
|
||||
->setReason($reason)
|
||||
->setTenantId($tenantId);
|
||||
$this->eventBus->publish($event);
|
||||
$this->events->dispatch($event);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
@@ -389,7 +365,7 @@ class FirewallService
|
||||
?string $reason = null,
|
||||
?string $createdBy = null
|
||||
): FirewallRuleObject {
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
|
||||
}
|
||||
@@ -419,7 +395,7 @@ class FirewallService
|
||||
?string $createdBy = null,
|
||||
?int $durationSeconds = null
|
||||
): FirewallRuleObject {
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
|
||||
}
|
||||
@@ -448,7 +424,7 @@ class FirewallService
|
||||
$event->setDeviceFingerprint($fingerprint)
|
||||
->setReason($reason)
|
||||
->setTenantId($tenantId);
|
||||
$this->eventBus->publish($event);
|
||||
$this->events->dispatch($event);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
@@ -464,7 +440,7 @@ class FirewallService
|
||||
}
|
||||
|
||||
// Verify tenant ownership
|
||||
if ($rule->getTenantId() !== $this->tenant->identifier()) {
|
||||
if ($rule->getTenantId() !== $this->tenantContext->identifier()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -485,7 +461,7 @@ class FirewallService
|
||||
}
|
||||
|
||||
// Verify tenant ownership
|
||||
if ($rule->getTenantId() !== $this->tenant->identifier()) {
|
||||
if ($rule->getTenantId() !== $this->tenantContext->identifier()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -501,7 +477,7 @@ class FirewallService
|
||||
*/
|
||||
public function listRules(bool $activeOnly = true): array
|
||||
{
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
return [];
|
||||
}
|
||||
@@ -518,7 +494,7 @@ class FirewallService
|
||||
?string $result = null,
|
||||
int $limit = 100
|
||||
): array {
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
return [];
|
||||
}
|
||||
@@ -531,7 +507,7 @@ class FirewallService
|
||||
*/
|
||||
public function getBlockedCount(?\DateTimeImmutable $since = null): int
|
||||
{
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
if (!$tenantId) {
|
||||
return 0;
|
||||
}
|
||||
@@ -556,7 +532,7 @@ class FirewallService
|
||||
*/
|
||||
private function getConfig(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$config = $this->tenant->configuration();
|
||||
$config = $this->tenantContext->configuration();
|
||||
$parts = explode('.', $key);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
@@ -576,7 +552,7 @@ class FirewallService
|
||||
private function getActiveRules(): array
|
||||
{
|
||||
if ($this->rulesCache === null) {
|
||||
$tenantId = $this->tenant->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$this->rulesCache = $tenantId
|
||||
? $this->store->listRules($tenantId, true)
|
||||
: [];
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace KTXC\Service;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Models\Identity\User;
|
||||
use KTXC\Resource\ProviderManager;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Security\Authentication\AuthenticationProviderInterface;
|
||||
|
||||
/**
|
||||
@@ -23,12 +23,12 @@ class SecurityService
|
||||
private string $securityCode;
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $sessionTenant,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly TokenService $tokenService,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly ProviderManager $providerManager,
|
||||
) {
|
||||
$this->securityCode = $this->sessionTenant->configuration()->security()->code();
|
||||
$this->securityCode = $this->tenantContext->configuration()->security()->code();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +118,7 @@ class SecurityService
|
||||
continue;
|
||||
}
|
||||
$context = new \KTXF\Security\Authentication\ProviderContext(
|
||||
tenantId: $this->sessionTenant->identifier(),
|
||||
tenantId: $this->tenantContext->identifier(),
|
||||
userIdentity: $identity,
|
||||
);
|
||||
$result = $provider->verify($context, $credentials);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Cache\CacheScope;
|
||||
use KTXF\Cache\EphemeralCacheInterface;
|
||||
|
||||
@@ -26,7 +26,7 @@ class TokenService
|
||||
private string $algorithm = 'HS256';
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $sessionTenant,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly EphemeralCacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Models\Identity\User;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Stores\UserAccountsStore;
|
||||
|
||||
class UserAccountsService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private readonly UserAccountsStore $userStore
|
||||
) {
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class UserAccountsService
|
||||
*/
|
||||
public function listUsers(array $filters = []): array
|
||||
{
|
||||
$users = $this->userStore->listUsers($this->tenantIdentity->identifier(), $filters);
|
||||
$users = $this->userStore->listUsers($this->tenantContext->identifier(), $filters);
|
||||
|
||||
// Remove sensitive data
|
||||
foreach ($users as &$user) {
|
||||
@@ -38,7 +38,7 @@ class UserAccountsService
|
||||
|
||||
public function fetchByIdentity(string $identifier): User | null
|
||||
{
|
||||
$data = $this->userStore->fetchByIdentity($this->tenantIdentity->identifier(), $identifier);
|
||||
$data = $this->userStore->fetchByIdentity($this->tenantContext->identifier(), $identifier);
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
@@ -50,32 +50,32 @@ class UserAccountsService
|
||||
|
||||
public function fetchByIdentifier(string $identifier): array | null
|
||||
{
|
||||
return $this->userStore->fetchByIdentifier($this->tenantIdentity->identifier(), $identifier);
|
||||
return $this->userStore->fetchByIdentifier($this->tenantContext->identifier(), $identifier);
|
||||
}
|
||||
|
||||
public function fetchByIdentityRaw(string $identifier): array | null
|
||||
{
|
||||
return $this->userStore->fetchByIdentity($this->tenantIdentity->identifier(), $identifier);
|
||||
return $this->userStore->fetchByIdentity($this->tenantContext->identifier(), $identifier);
|
||||
}
|
||||
|
||||
public function fetchByProviderSubject(string $provider, string $subject): ?array
|
||||
{
|
||||
return $this->userStore->fetchByProviderSubject($this->tenantIdentity->identifier(), $provider, $subject);
|
||||
return $this->userStore->fetchByProviderSubject($this->tenantContext->identifier(), $provider, $subject);
|
||||
}
|
||||
|
||||
public function createUser(array $userData): array
|
||||
{
|
||||
return $this->userStore->createUser($this->tenantIdentity->identifier(), $userData);
|
||||
return $this->userStore->createUser($this->tenantContext->identifier(), $userData);
|
||||
}
|
||||
|
||||
public function updateUser(string $uid, array $updates): bool
|
||||
{
|
||||
return $this->userStore->updateUser($this->tenantIdentity->identifier(), $uid, $updates);
|
||||
return $this->userStore->updateUser($this->tenantContext->identifier(), $uid, $updates);
|
||||
}
|
||||
|
||||
public function deleteUser(string $uid): bool
|
||||
{
|
||||
return $this->userStore->deleteUser($this->tenantIdentity->identifier(), $uid);
|
||||
return $this->userStore->deleteUser($this->tenantContext->identifier(), $uid);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -84,7 +84,7 @@ class UserAccountsService
|
||||
|
||||
public function fetchProfile(string $uid): ?array
|
||||
{
|
||||
return $this->userStore->fetchProfile($this->tenantIdentity->identifier(), $uid);
|
||||
return $this->userStore->fetchProfile($this->tenantContext->identifier(), $uid);
|
||||
}
|
||||
|
||||
public function storeProfile(string $uid, array $profileFields): bool
|
||||
@@ -109,7 +109,7 @@ class UserAccountsService
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->userStore->storeProfile($this->tenantIdentity->identifier(), $uid, $editableFields);
|
||||
return $this->userStore->storeProfile($this->tenantContext->identifier(), $uid, $editableFields);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -118,12 +118,12 @@ class UserAccountsService
|
||||
|
||||
public function fetchSettings(array $settings = [], bool $flatten = false): array | null
|
||||
{
|
||||
return $this->userStore->fetchSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings, $flatten);
|
||||
return $this->userStore->fetchSettings($this->tenantContext->identifier(), $this->identityContext->identifier(), $settings, $flatten);
|
||||
}
|
||||
|
||||
public function storeSettings(array $settings): bool
|
||||
{
|
||||
return $this->userStore->storeSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings);
|
||||
return $this->userStore->storeSettings($this->tenantContext->identifier(), $this->identityContext->identifier(), $settings);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Stores\UserRolesStore;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
@@ -12,7 +12,7 @@ use Psr\Log\LoggerInterface;
|
||||
class UserRolesService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly UserRolesStore $roleStore,
|
||||
private readonly LoggerInterface $logger
|
||||
) {}
|
||||
@@ -26,7 +26,7 @@ class UserRolesService
|
||||
*/
|
||||
public function listRoles(): array
|
||||
{
|
||||
return $this->roleStore->listRoles($this->tenantIdentity->identifier());
|
||||
return $this->roleStore->listRoles($this->tenantContext->identifier());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ class UserRolesService
|
||||
*/
|
||||
public function getRole(string $rid): ?array
|
||||
{
|
||||
return $this->roleStore->fetchByRid($this->tenantIdentity->identifier(), $rid);
|
||||
return $this->roleStore->fetchByRid($this->tenantContext->identifier(), $rid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,11 +45,11 @@ class UserRolesService
|
||||
$this->validateRoleData($roleData);
|
||||
|
||||
$this->logger->info('Creating role', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'label' => $roleData['label'] ?? 'Unnamed'
|
||||
]);
|
||||
|
||||
return $this->roleStore->createRole($this->tenantIdentity->identifier(), $roleData);
|
||||
return $this->roleStore->createRole($this->tenantContext->identifier(), $roleData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,11 +70,11 @@ class UserRolesService
|
||||
$this->validateRoleData($updates, false);
|
||||
|
||||
$this->logger->info('Updating role', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'rid' => $rid
|
||||
]);
|
||||
|
||||
return $this->roleStore->updateRole($this->tenantIdentity->identifier(), $rid, $updates);
|
||||
return $this->roleStore->updateRole($this->tenantContext->identifier(), $rid, $updates);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,17 +93,17 @@ class UserRolesService
|
||||
}
|
||||
|
||||
// Check if role is assigned to users
|
||||
$userCount = $this->roleStore->countUsersInRole($this->tenantIdentity->identifier(), $rid);
|
||||
$userCount = $this->roleStore->countUsersInRole($this->tenantContext->identifier(), $rid);
|
||||
if ($userCount > 0) {
|
||||
throw new \InvalidArgumentException("Cannot delete role assigned to {$userCount} user(s)");
|
||||
}
|
||||
|
||||
$this->logger->info('Deleting role', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'tenant' => $this->tenantContext->identifier(),
|
||||
'rid' => $rid
|
||||
]);
|
||||
|
||||
return $this->roleStore->deleteRole($this->tenantIdentity->identifier(), $rid);
|
||||
return $this->roleStore->deleteRole($this->tenantContext->identifier(), $rid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +111,7 @@ class UserRolesService
|
||||
*/
|
||||
public function getRoleUserCount(string $rid): int
|
||||
{
|
||||
return $this->roleStore->countUsersInRole($this->tenantIdentity->identifier(), $rid);
|
||||
return $this->roleStore->countUsersInRole($this->tenantContext->identifier(), $rid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC;
|
||||
|
||||
use KTXC\Models\Identity\User;
|
||||
|
||||
class SessionIdentity
|
||||
{
|
||||
private bool $identityLock = false;
|
||||
private ?User $identityData = null;
|
||||
|
||||
public function initialize(User $identity, bool $lock = true): void
|
||||
{
|
||||
if ($this->identityLock) {
|
||||
throw new \RuntimeException('Identity is already locked and cannot be changed.');
|
||||
}
|
||||
|
||||
$this->identityData = $identity;
|
||||
$this->identityLock = $lock;
|
||||
}
|
||||
|
||||
public function identity(): ?User
|
||||
{
|
||||
return $this->identityData;
|
||||
}
|
||||
|
||||
public function identifier(): ?string
|
||||
{
|
||||
return $this->identityData?->getId();
|
||||
}
|
||||
|
||||
public function label(): ?string
|
||||
{
|
||||
return $this->identityData?->getLabel();
|
||||
}
|
||||
|
||||
public function mailAddress(): ?string
|
||||
{
|
||||
return $this->identityData?->getIdentity();
|
||||
}
|
||||
|
||||
public function nameFirst(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function nameLast(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function permissions(): array
|
||||
{
|
||||
return $this->identityData?->getPermissions() ?? [];
|
||||
}
|
||||
|
||||
public function roles(): array
|
||||
{
|
||||
return $this->identityData?->getRoles() ?? [];
|
||||
}
|
||||
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
$permissions = $this->permissions();
|
||||
|
||||
// Exact match
|
||||
if (in_array($permission, $permissions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard match
|
||||
foreach ($permissions as $userPerm) {
|
||||
if (str_ends_with($userPerm, '.*')) {
|
||||
$prefix = substr($userPerm, 0, -2);
|
||||
if (str_starts_with($permission, $prefix . '.')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full wildcard
|
||||
if (in_array('*', $permissions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasRole(string $role): bool
|
||||
{
|
||||
return in_array($role, $this->roles());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC;
|
||||
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
use KTXC\Models\Tenant\TenantObject;
|
||||
use KTXC\Service\TenantService;
|
||||
|
||||
class SessionTenant
|
||||
{
|
||||
private ?TenantObject $tenant = null;
|
||||
private ?string $domain = null;
|
||||
private bool $configured = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly TenantService $tenantService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Configure the tenant information
|
||||
* This method is called by the SecurityMiddleware after validation
|
||||
*/
|
||||
public function configure(string $domain): void
|
||||
{
|
||||
if ($this->configured) {
|
||||
return;
|
||||
}
|
||||
$tenant = $this->tenantService->fetchByDomain($domain);
|
||||
if ($tenant) {
|
||||
$this->domain = $domain;
|
||||
$this->tenant = $tenant;
|
||||
$this->configured = true;
|
||||
} else {
|
||||
$this->domain = null;
|
||||
$this->tenant = null;
|
||||
$this->configured = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the tenant by its identifier (for console / CLI usage).
|
||||
*/
|
||||
public function configureById(string $identifier): void
|
||||
{
|
||||
if ($this->configured) {
|
||||
return;
|
||||
}
|
||||
$tenant = $this->tenantService->fetchById($identifier);
|
||||
if ($tenant) {
|
||||
$this->domain = $identifier;
|
||||
$this->tenant = $tenant;
|
||||
$this->configured = true;
|
||||
} else {
|
||||
$this->domain = null;
|
||||
$this->tenant = null;
|
||||
$this->configured = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the tenant configured
|
||||
*/
|
||||
public function configured(): bool
|
||||
{
|
||||
return $this->configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the tenant enabled
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return $this->tenant?->getEnabled() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tenant domain
|
||||
*/
|
||||
public function domain(): ?string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tenant identifier
|
||||
*/
|
||||
public function identifier(): ?string
|
||||
{
|
||||
return $this->tenant?->getIdentifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tenant label
|
||||
*/
|
||||
public function label(): ?string
|
||||
{
|
||||
return $this->tenant?->getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tenant configuration
|
||||
*/
|
||||
public function configuration(): TenantConfiguration
|
||||
{
|
||||
return $this->tenant?->getConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current tenant settings
|
||||
*/
|
||||
public function settings(): array
|
||||
{
|
||||
return $this->tenant?->getSettings() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all identity providers configuration for this tenant
|
||||
* @return array<string, array> Map of provider ID to provider config
|
||||
*/
|
||||
public function identityProviders(): array
|
||||
{
|
||||
return $this->tenant?->getConfiguration()['identity']['providers'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for a specific identity provider
|
||||
*
|
||||
* @param string $providerId Provider identifier (e.g., 'default', 'oidc')
|
||||
* @return array|null Provider configuration or null if not found
|
||||
*/
|
||||
public function identityProviderConfig(string $providerId): ?array
|
||||
{
|
||||
$providers = $this->identityProviders();
|
||||
return $providers[$providerId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an identity provider is enabled for this tenant
|
||||
*/
|
||||
public function isIdentityProviderEnabled(string $providerId): bool
|
||||
{
|
||||
$config = $this->identityProviderConfig($providerId);
|
||||
return $config !== null && ($config['enabled'] ?? false);
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -1,11 +1,9 @@
|
||||
<?php
|
||||
|
||||
use KTXC\Server;
|
||||
use KTXC\Application;
|
||||
|
||||
// Capture Composer ClassLoader instance for compatibility
|
||||
$composerLoader = require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$server = new Server(dirname(__DIR__));
|
||||
Server::setComposerLoader($composerLoader);
|
||||
|
||||
$server->runHttp();
|
||||
$application = Application::create(dirname(__DIR__), $composerLoader);
|
||||
$application->runHttp();
|
||||
|
||||
Reference in New Issue
Block a user