TenantAtlas/apps/platform/app/Support/Verification/VerificationLinkBehavior.php
ahmido ce0615a9c1 Spec 182: relocate Laravel platform to apps/platform (#213)
## Summary
- move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling
- update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location
- add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation`
- integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404`

## Remaining Rollout Checks
- validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout
- confirm web, queue, and scheduler processes all start from the expected working directory in staging/production
- verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #213
2026-04-08 08:40:47 +00:00

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,
);
}
}