From 107013bd042d831aac91dd7e7d20d0be81d87228 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Thu, 18 Jun 2026 23:42:08 +0000 Subject: [PATCH] Hosting dev-access: resolve via central identity API (shadow mode) Consolidate the inline HostingAccountMember lookups (Overview/Search/Hosting Product/Afia controllers) into HostingAccessResolver, which can source hosting developer-access from the monolith's central /identity/hosting/developer-access (correlating cPanel usernames to local account ids). Shadow mode by default (HOSTING_CENTRAL_DEV_ACCESS=false): local hosting_account_members stays authoritative and divergence from central is logged; flip the flag to cut over. Co-Authored-By: Claude Opus 4.8 --- .../Controllers/Hosting/AfiaController.php | 6 +- .../Hosting/HostingProductController.php | 6 +- .../Hosting/OverviewController.php | 6 +- .../Controllers/Hosting/SearchController.php | 6 +- .../Hosting/HostingAccessResolver.php | 88 +++++++++++++++++++ app/Services/Identity/IdentityClient.php | 17 ++++ config/hosting.php | 13 +++ 7 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 app/Services/Hosting/HostingAccessResolver.php diff --git a/app/Http/Controllers/Hosting/AfiaController.php b/app/Http/Controllers/Hosting/AfiaController.php index 499e37d..942183c 100644 --- a/app/Http/Controllers/Hosting/AfiaController.php +++ b/app/Http/Controllers/Hosting/AfiaController.php @@ -6,10 +6,10 @@ use App\Http\Controllers\Controller; use App\Models\CustomerHostingOrder; use App\Models\HostedSite; use App\Models\HostingAccount; -use App\Models\HostingAccountMember; use App\Models\HostingProduct; use App\Services\Afia\AfiaService; use App\Services\Billing\BillingClient; +use App\Services\Hosting\HostingAccessResolver; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -55,9 +55,7 @@ class AfiaController extends Controller HostingProduct::TYPE_WORDPRESS, ]; - $sharedAccountIds = HostingAccountMember::query() - ->where('user_id', $user->id) - ->pluck('hosting_account_id'); + $sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user); $accounts = HostingAccount::query() ->where(function ($query) use ($user, $sharedAccountIds) { diff --git a/app/Http/Controllers/Hosting/HostingProductController.php b/app/Http/Controllers/Hosting/HostingProductController.php index 747ebf1..109d641 100644 --- a/app/Http/Controllers/Hosting/HostingProductController.php +++ b/app/Http/Controllers/Hosting/HostingProductController.php @@ -5,11 +5,11 @@ namespace App\Http\Controllers\Hosting; use App\Http\Controllers\Controller; use App\Models\CustomerHostingOrder; use App\Models\HostingAccount; -use App\Models\HostingAccountMember; use App\Models\HostingOrder; use App\Models\HostingPlanChange; use App\Models\HostingProduct; use App\Models\RcServiceOrder; +use App\Services\Hosting\HostingAccessResolver; use App\Services\Hosting\HostingOrderFulfillmentService; use App\Services\Hosting\HostingPlanChangeService; use App\Services\Hosting\HostingRenewalCheckoutService; @@ -63,9 +63,7 @@ class HostingProductController extends Controller ->get(); // Admin-assigned Hosting Accounts - $sharedAccountIds = HostingAccountMember::query() - ->where('user_id', $user->id) - ->pluck('hosting_account_id'); + $sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user); $hostingAccounts = HostingAccount::query() ->where(function ($query) use ($user, $sharedAccountIds) { diff --git a/app/Http/Controllers/Hosting/OverviewController.php b/app/Http/Controllers/Hosting/OverviewController.php index 9c9285c..d60a4ac 100644 --- a/app/Http/Controllers/Hosting/OverviewController.php +++ b/app/Http/Controllers/Hosting/OverviewController.php @@ -6,10 +6,10 @@ use App\Http\Controllers\Controller; use App\Models\CustomerHostingOrder; use App\Models\HostedSite; use App\Models\HostingAccount; -use App\Models\HostingAccountMember; use App\Models\HostingOrder; use App\Models\HostingProduct; use App\Models\User; +use App\Services\Hosting\HostingAccessResolver; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Collection as SupportCollection; use Illuminate\Http\Request; @@ -100,9 +100,7 @@ class OverviewController extends Controller { $sharedTypes = $this->sharedTypes(); - $sharedAccountIds = HostingAccountMember::query() - ->where('user_id', $user->id) - ->pluck('hosting_account_id'); + $sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user); $hostingAccounts = HostingAccount::query() ->where(function ($query) use ($user, $sharedAccountIds) { diff --git a/app/Http/Controllers/Hosting/SearchController.php b/app/Http/Controllers/Hosting/SearchController.php index 5877cbf..fd556fc 100644 --- a/app/Http/Controllers/Hosting/SearchController.php +++ b/app/Http/Controllers/Hosting/SearchController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers\Hosting; use App\Http\Controllers\Controller; use App\Models\CustomerHostingOrder; use App\Models\HostingAccount; -use App\Models\HostingAccountMember; use App\Models\HostingProduct; +use App\Services\Hosting\HostingAccessResolver; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\View\View; @@ -49,9 +49,7 @@ class SearchController extends Controller HostingProduct::TYPE_WORDPRESS, ]; - $sharedAccountIds = HostingAccountMember::query() - ->where('user_id', $user->id) - ->pluck('hosting_account_id'); + $sharedAccountIds = app(HostingAccessResolver::class)->sharedAccountIds($user); HostingAccount::query() ->where(function ($query) use ($user, $sharedAccountIds) { diff --git a/app/Services/Hosting/HostingAccessResolver.php b/app/Services/Hosting/HostingAccessResolver.php new file mode 100644 index 0000000..f0552fe --- /dev/null +++ b/app/Services/Hosting/HostingAccessResolver.php @@ -0,0 +1,88 @@ + hosting account ids shared with the user */ + public function sharedAccountIds(User $user): Collection + { + $local = HostingAccountMember::query() + ->where('user_id', $user->id) + ->pluck('hosting_account_id'); + + $central = $this->centralAccountIds($user); + + if ($central !== null && $central->sort()->values()->all() !== $local->sort()->values()->all()) { + Log::warning('Hosting dev-access parity mismatch', [ + 'user_id' => $user->id, + 'local' => $local->values()->all(), + 'central' => $central->values()->all(), + ]); + } + + if (config('hosting.central_dev_access', false)) { + return $central ?? $local; // fail-open to local if central unreachable + } + + return $local; + } + + /** @return Collection|null null = central unreachable (fall back to local) */ + private function centralAccountIds(User $user): ?Collection + { + if (! $user->public_id) { + return collect(); + } + + return Cache::remember( + "hosting_dev_access:{$user->id}", + now()->addSeconds(60), + function () use ($user): ?Collection { + try { + $grants = $this->identity->hostingDeveloperAccess((string) $user->public_id); + $usernames = array_values(array_filter(array_map( + fn ($g) => $g['username'] ?? null, + $grants, + ))); + + if ($usernames === []) { + return collect(); + } + + return HostingAccount::query() + ->whereIn('username', $usernames) + ->pluck('id'); + } catch (\Throwable $e) { + Log::warning('Central hosting dev-access fetch failed', [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + ]); + + return null; + } + }, + ); + } +} diff --git a/app/Services/Identity/IdentityClient.php b/app/Services/Identity/IdentityClient.php index 6033f57..f271bbe 100644 --- a/app/Services/Identity/IdentityClient.php +++ b/app/Services/Identity/IdentityClient.php @@ -43,6 +43,23 @@ class IdentityClient return (array) $response->json('data', []); } + /** + * Hosting accounts (by cPanel username) the user can access as a developer, + * from the central monolith (source of truth for hosting developer access). + * + * @return list> + */ + public function hostingDeveloperAccess(string $publicId): array + { + $response = $this->request()->get($this->url('/identity/hosting/developer-access'), [ + 'user' => $publicId, + ]); + + $response->throw(); + + return (array) $response->json('data', []); + } + private function request() { return Http::withToken((string) config('identity.api_key')) diff --git a/config/hosting.php b/config/hosting.php index a0b0375..67bfd4e 100644 --- a/config/hosting.php +++ b/config/hosting.php @@ -1,6 +1,19 @@ env('HOSTING_CENTRAL_DEV_ACCESS', false), + /* |-------------------------------------------------------------------------- | Contabo API Configuration