logout(); Flash::error('No active tenant assigned', 'login', 'no_active_tenant'); Router::redirect($redirect); } } private static function validateCurrentTenant(): void { $currentTenant = $_SESSION['current_tenant'] ?? null; if (!$currentTenant || empty($currentTenant['id'])) { return; } $tenantId = (int) $currentTenant['id']; $tenant = self::tenantService()->findById($tenantId); if ($tenant) { return; } // Current tenant was deleted — reload session so the user is switched to another active tenant or logged out. $userId = (int) ($_SESSION['user']['id'] ?? 0); if ($userId > 0) { self::authService()->loadTenantDataIntoSession($userId); } else { unset($_SESSION['current_tenant']); } } private static function refreshTenantContext(): void { $userId = (int) ($_SESSION['user']['id'] ?? 0); if ($userId <= 0) { return; } $last = (int) ($_SESSION['tenant_context_refreshed_at'] ?? 0); $now = time(); if ($last > 0 && ($now - $last) < self::TENANT_REFRESH_INTERVAL) { return; } self::authService()->loadTenantDataIntoSession($userId); $_SESSION['tenant_context_refreshed_at'] = $now; } public static function requireAbility(string $ability, string $redirect = 'admin', array $context = []): void { self::requireLogin(); self::validateCurrentTenant(); $userId = (int) ($_SESSION['user']['id'] ?? 0); if (!self::isAllowedByAbility($userId, $ability, $context)) { Flash::error('Permission denied', $redirect, 'permission_denied'); Router::redirect($redirect); } } public static function requireAbilityOrForbidden(string $ability, array $context = []): void { self::requireAbilityDecisionOrForbidden($ability, $context); } public static function requireAbilityDecisionOrForbidden(string $ability, array $context = []): AuthorizationDecision { self::requireLogin(); self::validateCurrentTenant(); $userId = (int) ($_SESSION['user']['id'] ?? 0); $decision = self::authorizationService()->authorize($ability, [ 'actor_user_id' => $userId, ...$context, ]); if (!$decision->isAllowed()) { // JSON requests get a 403/404 response body; browser requests get a redirect to the forbidden page. if (Request::wantsJson()) { http_response_code($decision->status()); header('Content-Type: application/json; charset=utf-8'); $error = trim($decision->error()); echo json_encode(['error' => $error !== '' ? $error : 'forbidden']); exit; } Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery())); } return $decision; } public static function deny(string $redirect = 'error/forbidden'): void { if (Request::wantsJson()) { http_response_code(403); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['error' => 'forbidden']); exit; } if ($redirect === 'error/forbidden') { Router::redirect('error/forbidden?url=' . urlencode(Request::pathWithQuery())); return; } Router::redirect($redirect); } private static function authorizationService(): AuthorizationService { if (!is_callable(self::$authorizationServiceResolver)) { throw new \RuntimeException('Guard is not configured for dependency: ' . AuthorizationService::class); } $service = (self::$authorizationServiceResolver)(); if (!$service instanceof AuthorizationService) { throw new \RuntimeException('Guard resolver returned invalid dependency: ' . AuthorizationService::class); } return $service; } private static function authService(): AuthService { if (!is_callable(self::$authServiceResolver)) { throw new \RuntimeException('Guard is not configured for dependency: ' . AuthService::class); } $service = (self::$authServiceResolver)(); if (!$service instanceof AuthService) { throw new \RuntimeException('Guard resolver returned invalid dependency: ' . AuthService::class); } return $service; } private static function tenantService(): TenantService { if (!is_callable(self::$tenantServiceResolver)) { throw new \RuntimeException('Guard is not configured for dependency: ' . TenantService::class); } $service = (self::$tenantServiceResolver)(); if (!$service instanceof TenantService) { throw new \RuntimeException('Guard resolver returned invalid dependency: ' . TenantService::class); } return $service; } private static function isAllowedByAbility(int $userId, string $ability, array $context = []): bool { $ability = trim($ability); if ($userId <= 0 || $ability === '') { return false; } $decision = self::authorizationService()->authorize($ability, [ 'actor_user_id' => $userId, ...$context, ]); return $decision->isAllowed(); } }