Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 2fbddd7dbc
366 changed files with 41999 additions and 0 deletions

146
shared/lib/Event/Event.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace KTXF\Event;
/**
* Base event class for the event bus system
*/
class Event
{
private bool $propagationStopped = false;
private array $data = [];
private float $timestamp;
private ?string $tenantId = null;
private ?string $identityId = null;
public function __construct(
private readonly string $name,
array $data = []
) {
$this->data = $data;
$this->timestamp = microtime(true);
}
/**
* Get the event name
*/
public function getName(): string
{
return $this->name;
}
/**
* Get a data value by key
*/
public function get(string $key, mixed $default = null): mixed
{
return $this->data[$key] ?? $default;
}
/**
* Set a data value
*/
public function set(string $key, mixed $value): self
{
$this->data[$key] = $value;
return $this;
}
/**
* Check if a data key exists
*/
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
/**
* Get all data
*/
public function getData(): array
{
return $this->data;
}
/**
* Alias for getData() for backward compatibility
*/
public function all(): array
{
return $this->data;
}
/**
* Get the event timestamp
*/
public function getTimestamp(): float
{
return $this->timestamp;
}
/**
* Stop event propagation to subsequent listeners
*/
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
/**
* Check if propagation is stopped
*/
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Get tenant ID for multi-tenant context
*/
public function getTenantId(): ?string
{
return $this->tenantId;
}
/**
* Set tenant ID for multi-tenant context
*/
public function setTenantId(?string $tenantId): self
{
$this->tenantId = $tenantId;
return $this;
}
/**
* Get identity ID (user who triggered the event)
*/
public function getIdentityId(): ?string
{
return $this->identityId;
}
/**
* Set identity ID
*/
public function setIdentityId(?string $identityId): self
{
$this->identityId = $identityId;
return $this;
}
/**
* Convert event to array for serialization/logging
*/
public function toArray(): array
{
return [
'name' => $this->name,
'data' => $this->data,
'timestamp' => $this->timestamp,
'tenantId' => $this->tenantId,
'identityId' => $this->identityId,
];
}
}

View File

@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace KTXF\Event;
/**
* Simple event bus for decoupled pub/sub communication between services
*
* Features:
* - Priority-based listener ordering
* - Synchronous and asynchronous (deferred) event handling
* - Event propagation control
*/
class EventBus
{
/** @var array<string, array<array{callback: callable, priority: int}>> */
private array $listeners = [];
/** @var array<string, array<callable>> */
private array $asyncListeners = [];
/** @var Event[] */
private array $deferredEvents = [];
/**
* Subscribe to an event with optional priority
* Higher priority listeners are called first
*/
public function subscribe(string $eventName, callable $listener, int $priority = 0): self
{
$this->listeners[$eventName][] = [
'callback' => $listener,
'priority' => $priority,
];
// Sort by priority (higher first)
usort(
$this->listeners[$eventName],
fn($a, $b) => $b['priority'] <=> $a['priority']
);
return $this;
}
/**
* Subscribe to an event for async/deferred processing
* These handlers run at the end of the request cycle
*/
public function subscribeAsync(string $eventName, callable $listener): self
{
$this->asyncListeners[$eventName][] = $listener;
return $this;
}
/**
* Unsubscribe a listener from an event
*/
public function unsubscribe(string $eventName, callable $listener): self
{
if (isset($this->listeners[$eventName])) {
$this->listeners[$eventName] = array_filter(
$this->listeners[$eventName],
fn($item) => $item['callback'] !== $listener
);
}
if (isset($this->asyncListeners[$eventName])) {
$this->asyncListeners[$eventName] = array_filter(
$this->asyncListeners[$eventName],
fn($item) => $item !== $listener
);
}
return $this;
}
/**
* Publish an event to all subscribers
*/
public function publish(Event $event): self
{
$eventName = $event->getName();
// Execute synchronous listeners
if (isset($this->listeners[$eventName])) {
foreach ($this->listeners[$eventName] as $listenerData) {
if ($event->isPropagationStopped()) {
break;
}
try {
call_user_func($listenerData['callback'], $event);
} catch (\Throwable $e) {
// Log error but don't break the chain
error_log(sprintf(
'Event listener error for %s: %s',
$eventName,
$e->getMessage()
));
}
}
}
// Queue for async processing if there are async listeners
if (isset($this->asyncListeners[$eventName]) && !empty($this->asyncListeners[$eventName])) {
$this->deferredEvents[] = $event;
}
return $this;
}
/**
* Process deferred/async events
* Call this at the end of the request cycle
*/
public function processDeferred(): int
{
$processed = 0;
foreach ($this->deferredEvents as $event) {
$eventName = $event->getName();
if (!isset($this->asyncListeners[$eventName])) {
continue;
}
foreach ($this->asyncListeners[$eventName] as $listener) {
try {
call_user_func($listener, $event);
$processed++;
} catch (\Throwable $e) {
// Log but don't fail - these are non-critical
error_log(sprintf(
'Async event handler error for %s: %s',
$eventName,
$e->getMessage()
));
}
}
}
$this->deferredEvents = [];
return $processed;
}
/**
* Check if an event has any listeners
*/
public function hasListeners(string $eventName): bool
{
return !empty($this->listeners[$eventName]) || !empty($this->asyncListeners[$eventName]);
}
/**
* Get count of listeners for an event
*/
public function getListenerCount(string $eventName): int
{
$sync = isset($this->listeners[$eventName]) ? count($this->listeners[$eventName]) : 0;
$async = isset($this->asyncListeners[$eventName]) ? count($this->asyncListeners[$eventName]) : 0;
return $sync + $async;
}
/**
* Get count of pending deferred events
*/
public function getDeferredCount(): int
{
return count($this->deferredEvents);
}
/**
* Clear all listeners (useful for testing)
*/
public function clear(): self
{
$this->listeners = [];
$this->asyncListeners = [];
$this->deferredEvents = [];
return $this;
}
}

View File

@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace KTXF\Event;
/**
* Security-specific event for authentication and access control events
*/
class SecurityEvent extends Event
{
// Event names
public const AUTH_SUCCESS = 'security.auth.success';
public const AUTH_FAILURE = 'security.auth.failure';
public const AUTH_LOGOUT = 'security.auth.logout';
public const TOKEN_REFRESH = 'security.token.refresh';
public const TOKEN_REVOKED = 'security.token.revoked';
public const ACCESS_DENIED = 'security.access.denied';
public const ACCESS_GRANTED = 'security.access.granted';
public const BRUTE_FORCE_DETECTED = 'security.brute_force.detected';
public const RATE_LIMIT_EXCEEDED = 'security.rate_limit.exceeded';
public const SUSPICIOUS_ACTIVITY = 'security.suspicious.activity';
public const IP_BLOCKED = 'security.ip.blocked';
public const IP_ALLOWED = 'security.ip.allowed';
public const DEVICE_BLOCKED = 'security.device.blocked';
private ?string $ipAddress = null;
private ?string $deviceFingerprint = null;
private ?string $userAgent = null;
private ?string $requestPath = null;
private ?string $requestMethod = null;
private ?string $userId = null;
private ?string $reason = null;
private int $severity = self::SEVERITY_INFO;
// Severity levels
public const SEVERITY_DEBUG = 0;
public const SEVERITY_INFO = 1;
public const SEVERITY_WARNING = 2;
public const SEVERITY_ERROR = 3;
public const SEVERITY_CRITICAL = 4;
/**
* Create a security event with common parameters
*/
public static function create(
string $name,
?string $ipAddress = null,
?string $deviceFingerprint = null,
array $data = []
): self {
$event = new self($name, $data);
$event->ipAddress = $ipAddress;
$event->deviceFingerprint = $deviceFingerprint;
// Set default severity based on event type
$event->severity = self::getSeverityForEvent($name);
return $event;
}
/**
* Create an authentication failure event
*/
public static function authFailure(
string $ipAddress,
?string $deviceFingerprint = null,
?string $userId = null,
?string $reason = null
): self {
$event = self::create(self::AUTH_FAILURE, $ipAddress, $deviceFingerprint, [
'userId' => $userId,
'reason' => $reason,
]);
$event->userId = $userId;
$event->reason = $reason;
return $event;
}
/**
* Create an authentication success event
*/
public static function authSuccess(
string $ipAddress,
?string $deviceFingerprint = null,
string $userId = null
): self {
$event = self::create(self::AUTH_SUCCESS, $ipAddress, $deviceFingerprint, [
'userId' => $userId,
]);
$event->userId = $userId;
return $event;
}
/**
* Create a brute force detection event
*/
public static function bruteForceDetected(
string $ipAddress,
int $failureCount,
int $windowSeconds
): self {
$event = self::create(self::BRUTE_FORCE_DETECTED, $ipAddress, null, [
'failureCount' => $failureCount,
'windowSeconds' => $windowSeconds,
]);
$event->reason = sprintf(
'%d failed attempts in %d seconds',
$failureCount,
$windowSeconds
);
return $event;
}
/**
* Create a rate limit exceeded event
*/
public static function rateLimitExceeded(
string $ipAddress,
int $requestCount,
int $windowSeconds,
?string $endpoint = null
): self {
$event = self::create(self::RATE_LIMIT_EXCEEDED, $ipAddress, null, [
'requestCount' => $requestCount,
'windowSeconds' => $windowSeconds,
'endpoint' => $endpoint,
]);
$event->requestPath = $endpoint;
$event->reason = sprintf(
'%d requests in %d seconds',
$requestCount,
$windowSeconds
);
return $event;
}
/**
* Create an access denied event
*/
public static function accessDenied(
string $ipAddress,
?string $deviceFingerprint = null,
?string $ruleId = null,
?string $reason = null
): self {
$event = self::create(self::ACCESS_DENIED, $ipAddress, $deviceFingerprint, [
'ruleId' => $ruleId,
'reason' => $reason,
]);
$event->reason = $reason;
return $event;
}
/**
* Get default severity for event types
*/
private static function getSeverityForEvent(string $eventName): int
{
return match ($eventName) {
self::AUTH_SUCCESS,
self::ACCESS_GRANTED,
self::TOKEN_REFRESH => self::SEVERITY_INFO,
self::AUTH_FAILURE,
self::ACCESS_DENIED,
self::AUTH_LOGOUT,
self::TOKEN_REVOKED => self::SEVERITY_WARNING,
self::RATE_LIMIT_EXCEEDED,
self::SUSPICIOUS_ACTIVITY => self::SEVERITY_ERROR,
self::BRUTE_FORCE_DETECTED,
self::IP_BLOCKED,
self::DEVICE_BLOCKED => self::SEVERITY_CRITICAL,
default => self::SEVERITY_INFO,
};
}
// Getters and setters
public function getIpAddress(): ?string
{
return $this->ipAddress;
}
public function setIpAddress(?string $ipAddress): self
{
$this->ipAddress = $ipAddress;
return $this;
}
public function getDeviceFingerprint(): ?string
{
return $this->deviceFingerprint;
}
public function setDeviceFingerprint(?string $deviceFingerprint): self
{
$this->deviceFingerprint = $deviceFingerprint;
return $this;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function setUserAgent(?string $userAgent): self
{
$this->userAgent = $userAgent;
return $this;
}
public function getRequestPath(): ?string
{
return $this->requestPath;
}
public function setRequestPath(?string $requestPath): self
{
$this->requestPath = $requestPath;
return $this;
}
public function getRequestMethod(): ?string
{
return $this->requestMethod;
}
public function setRequestMethod(?string $requestMethod): self
{
$this->requestMethod = $requestMethod;
return $this;
}
public function getUserId(): ?string
{
return $this->userId;
}
public function setUserId(?string $userId): self
{
$this->userId = $userId;
return $this;
}
public function getReason(): ?string
{
return $this->reason;
}
public function setReason(?string $reason): self
{
$this->reason = $reason;
return $this;
}
public function getSeverity(): int
{
return $this->severity;
}
public function setSeverity(int $severity): self
{
$this->severity = $severity;
return $this;
}
public function getSeverityLabel(): string
{
return match ($this->severity) {
self::SEVERITY_DEBUG => 'DEBUG',
self::SEVERITY_INFO => 'INFO',
self::SEVERITY_WARNING => 'WARNING',
self::SEVERITY_ERROR => 'ERROR',
self::SEVERITY_CRITICAL => 'CRITICAL',
default => 'UNKNOWN',
};
}
/**
* Override toArray to include security-specific fields
*/
public function toArray(): array
{
return array_merge(parent::toArray(), [
'ipAddress' => $this->ipAddress,
'deviceFingerprint' => $this->deviceFingerprint,
'userAgent' => $this->userAgent,
'requestPath' => $this->requestPath,
'requestMethod' => $this->requestMethod,
'userId' => $this->userId,
'reason' => $this->reason,
'severity' => $this->severity,
'severityLabel' => $this->getSeverityLabel(),
]);
}
}