## Summary - add an in-place Required Permissions assist to the onboarding Verify Access step via a Filament slideover - route permission-related verification remediation links into the assist first and keep deep-dive links opening in a new tab - add view-model and link-behavior helpers plus focused feature, browser, RBAC, and unit coverage for the new assist ## Scope - onboarding wizard Verify Access UX - Required Permissions assist rendering and link behavior - Spec 139 artifacts, contracts, and checklist updates ## Notes - branch: `139-verify-access-permissions-assist` - commit: `b4193f1` - worktree was clean at PR creation time Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #168
150 lines
4.1 KiB
PHP
150 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Verification;
|
|
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
|
|
final class VerificationLinkBehavior
|
|
{
|
|
/**
|
|
* @return array{
|
|
* label:string,
|
|
* url:string,
|
|
* kind:'external'|'internal-diagnostic'|'internal-inline-safe',
|
|
* opens_in_new_tab:bool,
|
|
* show_new_tab_hint:bool
|
|
* }
|
|
*/
|
|
public function describe(?string $label, ?string $url): array
|
|
{
|
|
$normalizedLabel = is_string($label) && trim($label) !== ''
|
|
? trim($label)
|
|
: 'Open link';
|
|
|
|
$normalizedUrl = is_string($url) && trim($url) !== ''
|
|
? trim($url)
|
|
: '';
|
|
|
|
$kind = $this->classify($normalizedUrl);
|
|
$opensInNewTab = $kind !== 'internal-inline-safe' && $normalizedUrl !== '';
|
|
|
|
return [
|
|
'label' => $normalizedLabel,
|
|
'url' => $normalizedUrl,
|
|
'kind' => $kind,
|
|
'opens_in_new_tab' => $opensInNewTab,
|
|
'show_new_tab_hint' => $opensInNewTab,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $check
|
|
*/
|
|
public function shouldRouteThroughAssist(array $check, bool $assistVisible): bool
|
|
{
|
|
if (! $assistVisible) {
|
|
return false;
|
|
}
|
|
|
|
$key = $check['key'] ?? null;
|
|
$key = is_string($key) ? trim($key) : '';
|
|
|
|
if ($key !== '' && str_starts_with($key, 'permissions.')) {
|
|
return true;
|
|
}
|
|
|
|
$reasonCode = $check['reason_code'] ?? null;
|
|
$reasonCode = is_string($reasonCode) ? trim($reasonCode) : '';
|
|
|
|
return in_array($reasonCode, [
|
|
ProviderReasonCodes::ProviderConsentMissing,
|
|
ProviderReasonCodes::ProviderConsentFailed,
|
|
ProviderReasonCodes::ProviderConsentRevoked,
|
|
ProviderReasonCodes::ProviderPermissionMissing,
|
|
ProviderReasonCodes::ProviderPermissionDenied,
|
|
ProviderReasonCodes::ProviderPermissionRefreshFailed,
|
|
ProviderReasonCodes::IntuneRbacPermissionMissing,
|
|
], true);
|
|
}
|
|
|
|
/**
|
|
* @return 'external'|'internal-diagnostic'|'internal-inline-safe'
|
|
*/
|
|
private function classify(string $url): string
|
|
{
|
|
if ($url === '') {
|
|
return 'internal-inline-safe';
|
|
}
|
|
|
|
$path = $this->extractPath($url);
|
|
|
|
if ($path !== null && $this->isInternalDiagnosticPath($path)) {
|
|
return 'internal-diagnostic';
|
|
}
|
|
|
|
if ($this->isExternalUrl($url)) {
|
|
return 'external';
|
|
}
|
|
|
|
return 'internal-inline-safe';
|
|
}
|
|
|
|
private function extractPath(string $url): ?string
|
|
{
|
|
$path = parse_url($url, PHP_URL_PATH);
|
|
|
|
if (is_string($path) && $path !== '') {
|
|
return '/'.ltrim($path, '/');
|
|
}
|
|
|
|
if (str_starts_with($url, '/')) {
|
|
return '/'.ltrim($url, '/');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isExternalUrl(string $url): bool
|
|
{
|
|
$scheme = parse_url($url, PHP_URL_SCHEME);
|
|
|
|
if (! is_string($scheme) || ! in_array(strtolower($scheme), ['http', 'https'], true)) {
|
|
return false;
|
|
}
|
|
|
|
$host = parse_url($url, PHP_URL_HOST);
|
|
|
|
if (! is_string($host) || $host === '') {
|
|
return true;
|
|
}
|
|
|
|
$applicationHost = parse_url(url('/'), PHP_URL_HOST);
|
|
$applicationPort = parse_url(url('/'), PHP_URL_PORT);
|
|
$urlPort = parse_url($url, PHP_URL_PORT);
|
|
|
|
if (! is_string($applicationHost) || $applicationHost === '') {
|
|
return true;
|
|
}
|
|
|
|
if (strcasecmp($host, $applicationHost) !== 0) {
|
|
return true;
|
|
}
|
|
|
|
if ($applicationPort === null || $urlPort === null) {
|
|
return false;
|
|
}
|
|
|
|
return (int) $urlPort !== (int) $applicationPort;
|
|
}
|
|
|
|
private function isInternalDiagnosticPath(string $path): bool
|
|
{
|
|
return (bool) preg_match(
|
|
'/^\/admin\/(?:tenants\/[^\/]+\/required-permissions|tenants\/[^\/]+\/provider-connections(?:\/create|\/[^\/]+(?:\/edit)?)?|provider-connections(?:\/create|\/[^\/]+(?:\/edit)?)?)$/',
|
|
$path,
|
|
);
|
|
}
|
|
}
|