## 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
142 lines
5.6 KiB
PHP
142 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Providers;
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Support\Providers\ProviderConnectionType;
|
|
use App\Support\Providers\ProviderConsentStatus;
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
use App\Support\Providers\ProviderVerificationStatus;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
|
|
final class ProviderConnectionMutationService
|
|
{
|
|
public function __construct(private readonly CredentialManager $credentials) {}
|
|
|
|
public function enableDedicatedOverride(
|
|
ProviderConnection $connection,
|
|
string $clientId,
|
|
string $clientSecret,
|
|
): ProviderConnection {
|
|
$clientId = trim($clientId);
|
|
$clientSecret = trim($clientSecret);
|
|
|
|
if ($clientId === '' || $clientSecret === '') {
|
|
throw new InvalidArgumentException('Dedicated client_id and client_secret are required.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($connection, $clientId, $clientSecret): ProviderConnection {
|
|
$connection->loadMissing('credential');
|
|
|
|
$credential = $connection->credential;
|
|
$payload = $credential instanceof ProviderCredential && is_array($credential->payload)
|
|
? $credential->payload
|
|
: [];
|
|
|
|
$previousClientId = trim((string) ($payload['client_id'] ?? ''));
|
|
$needsConsentReset = $connection->connection_type !== ProviderConnectionType::Dedicated
|
|
|| $previousClientId === ''
|
|
|| $previousClientId !== $clientId;
|
|
|
|
$consentStatus = $needsConsentReset
|
|
? ProviderConsentStatus::Required
|
|
: $this->normalizeConsentStatus($connection->consent_status);
|
|
$verificationStatus = ProviderVerificationStatus::Unknown;
|
|
|
|
$connection->forceFill([
|
|
'connection_type' => ProviderConnectionType::Dedicated->value,
|
|
'consent_status' => $consentStatus->value,
|
|
'verification_status' => $verificationStatus->value,
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => $needsConsentReset
|
|
? ProviderReasonCodes::ProviderConsentMissing
|
|
: null,
|
|
'last_error_message' => null,
|
|
'scopes_granted' => $needsConsentReset ? [] : ($connection->scopes_granted ?? []),
|
|
'consent_granted_at' => $needsConsentReset ? null : $connection->consent_granted_at,
|
|
'consent_last_checked_at' => $needsConsentReset ? null : $connection->consent_last_checked_at,
|
|
'consent_error_code' => null,
|
|
'consent_error_message' => null,
|
|
])->save();
|
|
|
|
$this->credentials->upsertClientSecretCredential(
|
|
connection: $connection->fresh(),
|
|
clientId: $clientId,
|
|
clientSecret: $clientSecret,
|
|
);
|
|
|
|
return $connection->fresh(['credential']);
|
|
});
|
|
}
|
|
|
|
public function revertToPlatform(ProviderConnection $connection): ProviderConnection
|
|
{
|
|
return DB::transaction(function () use ($connection): ProviderConnection {
|
|
$connection->loadMissing('credential');
|
|
|
|
if ($connection->credential instanceof ProviderCredential) {
|
|
$connection->credential->delete();
|
|
}
|
|
|
|
$connection->forceFill([
|
|
'connection_type' => ProviderConnectionType::Platform->value,
|
|
'consent_status' => ProviderConsentStatus::Required->value,
|
|
'consent_granted_at' => null,
|
|
'consent_last_checked_at' => null,
|
|
'consent_error_code' => null,
|
|
'consent_error_message' => null,
|
|
'verification_status' => ProviderVerificationStatus::Unknown->value,
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => ProviderReasonCodes::ProviderConsentMissing,
|
|
'last_error_message' => null,
|
|
'scopes_granted' => [],
|
|
])->save();
|
|
|
|
return $connection->fresh(['credential']);
|
|
});
|
|
}
|
|
|
|
public function deleteDedicatedCredential(ProviderConnection $connection): ProviderConnection
|
|
{
|
|
return DB::transaction(function () use ($connection): ProviderConnection {
|
|
$connection->loadMissing('credential');
|
|
|
|
if ($connection->credential instanceof ProviderCredential) {
|
|
$connection->credential->delete();
|
|
}
|
|
|
|
$consentStatus = $this->normalizeConsentStatus($connection->consent_status);
|
|
$verificationStatus = ProviderVerificationStatus::Blocked;
|
|
|
|
$connection->forceFill([
|
|
'connection_type' => ProviderConnectionType::Dedicated->value,
|
|
'consent_status' => $consentStatus->value,
|
|
'verification_status' => $verificationStatus->value,
|
|
'last_health_check_at' => null,
|
|
'last_error_reason_code' => ProviderReasonCodes::DedicatedCredentialMissing,
|
|
'last_error_message' => 'Dedicated credential is missing.',
|
|
])->save();
|
|
|
|
return $connection->fresh(['credential']);
|
|
});
|
|
}
|
|
|
|
private function normalizeConsentStatus(
|
|
ProviderConsentStatus|string|null $consentStatus,
|
|
): ProviderConsentStatus {
|
|
if ($consentStatus instanceof ProviderConsentStatus) {
|
|
return $consentStatus;
|
|
}
|
|
|
|
if (is_string($consentStatus)) {
|
|
return ProviderConsentStatus::tryFrom(trim($consentStatus)) ?? ProviderConsentStatus::Required;
|
|
}
|
|
|
|
return ProviderConsentStatus::Required;
|
|
}
|
|
}
|