## Summary - migrate provider connections to the canonical three-dimension state model: lifecycle via `is_enabled`, consent via `consent_status`, and verification via `verification_status` - remove legacy provider status and health badge paths, update admin and system directory surfaces, and align onboarding, consent callback, verification, resolver, and mutation flows with the new model - add the Spec 188 artifact set, schema migrations, guard coverage, and expanded provider-state tests across admin, system, onboarding, verification, and rendering paths ## Verification - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Auth/SystemPanelAuthTest.php tests/Feature/Filament/TenantGlobalSearchLifecycleScopeTest.php tests/Feature/ProviderConnections/ProviderConnectionEnableDisableTest.php tests/Feature/ProviderConnections/ProviderConnectionTruthCleanupSpec179Test.php` - integrated browser smoke: validated admin provider list/detail/edit, tenant provider summary, system directory tenant detail, provider-connection search exclusion, and cleaned up the temporary smoke record afterward ## Filament / implementation notes - Livewire v4.0+ compliance: preserved; this change targets Filament v5 on Livewire v4 and does not introduce older APIs - Provider registration location: unchanged; Laravel 11+ panel providers remain registered in `bootstrap/providers.php` - Globally searchable resources: `ProviderConnectionResource` remains intentionally excluded from global search; tenant global search remains enabled and continues to resolve to view pages - Destructive actions: no new destructive action surface was introduced without confirmation or authorization; existing capability checks continue to gate provider mutations - Asset strategy: unchanged; no new Filament assets were added, so deploy behavior for `php artisan filament:assets` remains unchanged - Testing plan covered: system auth, tenant global search, provider lifecycle enable/disable behavior, and provider truth cleanup cutover behavior Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #219
119 lines
4.7 KiB
PHP
119 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Providers;
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Models\Tenant;
|
|
use App\Support\Providers\ProviderConnectionType;
|
|
|
|
final class ProviderConnectionClassifier
|
|
{
|
|
public function classify(
|
|
ProviderConnection $connection,
|
|
string $source = 'migration_scan',
|
|
): ProviderConnectionClassificationResult {
|
|
$connection->loadMissing(['credential', 'tenant']);
|
|
|
|
$tenant = $connection->tenant;
|
|
$credential = $connection->credential;
|
|
$legacyIdentity = $tenant instanceof Tenant ? $tenant->legacyProviderIdentity() : ['client_id' => null, 'has_secret' => false];
|
|
$tenantClientId = trim((string) ($legacyIdentity['client_id'] ?? ''));
|
|
$credentialClientId = $this->credentialClientId($credential);
|
|
$currentConnectionType = $connection->connection_type instanceof ProviderConnectionType
|
|
? $connection->connection_type->value
|
|
: (is_string($connection->connection_type) ? $connection->connection_type : null);
|
|
$hasLegacyTenantIdentity = $tenantClientId !== '' || (bool) ($legacyIdentity['has_secret'] ?? false);
|
|
$hasDedicatedCredential = $credential instanceof ProviderCredential;
|
|
|
|
$suggestedConnectionType = ProviderConnectionType::Platform;
|
|
$reviewRequired = false;
|
|
|
|
if ($hasDedicatedCredential && ! $hasLegacyTenantIdentity) {
|
|
$suggestedConnectionType = ProviderConnectionType::Dedicated;
|
|
} elseif ($hasDedicatedCredential && $hasLegacyTenantIdentity) {
|
|
$suggestedConnectionType = ProviderConnectionType::Dedicated;
|
|
$reviewRequired = $tenantClientId === '' || $credentialClientId === '' || $tenantClientId !== $credentialClientId;
|
|
} elseif (! $hasDedicatedCredential && $hasLegacyTenantIdentity) {
|
|
$suggestedConnectionType = ProviderConnectionType::Platform;
|
|
$reviewRequired = true;
|
|
}
|
|
|
|
return new ProviderConnectionClassificationResult(
|
|
providerConnectionId: (int) $connection->getKey(),
|
|
suggestedConnectionType: $suggestedConnectionType,
|
|
reviewRequired: $reviewRequired,
|
|
signals: [
|
|
'has_dedicated_credential' => $hasDedicatedCredential,
|
|
'credential_client_id' => $credentialClientId !== '' ? $credentialClientId : null,
|
|
'has_legacy_tenant_identity' => $hasLegacyTenantIdentity,
|
|
'tenant_client_id' => $tenantClientId !== '' ? $tenantClientId : null,
|
|
'tenant_has_secret' => (bool) ($legacyIdentity['has_secret'] ?? false),
|
|
'current_connection_type' => $currentConnectionType,
|
|
'is_enabled' => (bool) $connection->is_enabled,
|
|
'consent_status' => $this->enumValue($connection->consent_status),
|
|
'verification_status' => $this->enumValue($connection->verification_status),
|
|
'last_error_reason_code' => is_string($connection->last_error_reason_code) ? $connection->last_error_reason_code : null,
|
|
],
|
|
effectiveApp: $this->effectiveAppMetadata(
|
|
suggestedConnectionType: $suggestedConnectionType,
|
|
reviewRequired: $reviewRequired,
|
|
credentialClientId: $credentialClientId,
|
|
),
|
|
source: $source,
|
|
);
|
|
}
|
|
|
|
private function credentialClientId(?ProviderCredential $credential): string
|
|
{
|
|
if (! $credential instanceof ProviderCredential || ! is_array($credential->payload)) {
|
|
return '';
|
|
}
|
|
|
|
return trim((string) ($credential->payload['client_id'] ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @return array{app_id: ?string, source: string}
|
|
*/
|
|
private function effectiveAppMetadata(
|
|
ProviderConnectionType $suggestedConnectionType,
|
|
bool $reviewRequired,
|
|
string $credentialClientId,
|
|
): array {
|
|
if ($reviewRequired) {
|
|
return [
|
|
'app_id' => null,
|
|
'source' => 'review_required',
|
|
];
|
|
}
|
|
|
|
if ($suggestedConnectionType === ProviderConnectionType::Dedicated) {
|
|
return [
|
|
'app_id' => $credentialClientId !== '' ? $credentialClientId : null,
|
|
'source' => 'dedicated_credential',
|
|
];
|
|
}
|
|
|
|
$platformClientId = trim((string) config('graph.client_id'));
|
|
|
|
return [
|
|
'app_id' => $platformClientId !== '' ? $platformClientId : null,
|
|
'source' => 'platform_config',
|
|
];
|
|
}
|
|
|
|
private function enumValue(mixed $value): ?string
|
|
{
|
|
if ($value instanceof \BackedEnum) {
|
|
return $value->value;
|
|
}
|
|
|
|
if (is_string($value) && trim($value) !== '') {
|
|
return trim($value);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|