From e3fd235139e4fd6739c164142afc232a018bd25b Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 7 Jun 2026 06:33:16 +0000 Subject: [PATCH] Unify SSO and surface legacy ResellerClub hosting orders on the dashboard. Add silent OAuth and session keepalive, and show imported RC orders alongside native hosting accounts. Co-authored-by: Cursor --- app/Console/Commands/ImportHostingCommand.php | 12 ++++ .../Controllers/Auth/SsoLoginController.php | 68 +++++++++++++++---- .../Hosting/OverviewController.php | 39 +++++++++-- app/Models/HostingOrder.php | 27 +++++--- .../Hosting/ResellerClubHostingService.php | 4 +- bootstrap/app.php | 4 ++ .../views/hosting/accounts-list.blade.php | 42 ++++++++++++ resources/views/hosting/dashboard.blade.php | 25 ++++++- resources/views/layouts/email.blade.php | 1 + resources/views/layouts/hosting.blade.php | 1 + .../views/partials/sso-keepalive.blade.php | 4 ++ 11 files changed, 196 insertions(+), 31 deletions(-) create mode 100644 resources/views/partials/sso-keepalive.blade.php diff --git a/app/Console/Commands/ImportHostingCommand.php b/app/Console/Commands/ImportHostingCommand.php index ee8caa8..e69d5b9 100644 --- a/app/Console/Commands/ImportHostingCommand.php +++ b/app/Console/Commands/ImportHostingCommand.php @@ -222,6 +222,7 @@ class ImportHostingCommand extends Command unset($row['id'], $row['owner_public_id'], $row['owner_email']); $row['user_id'] = $owner->id; $row['platform_id'] = $platformId; + $row['meta'] = $this->normalizeMeta($row['meta'] ?? null); if ($commit) { HostingOrder::updateOrCreate(['platform_id' => $platformId], $row); @@ -335,6 +336,17 @@ class ImportHostingCommand extends Command ]); } + /** @return array|null */ + private function normalizeMeta(mixed $meta): ?array + { + while (is_string($meta)) { + $decoded = json_decode($meta, true); + $meta = json_last_error() === JSON_ERROR_NONE ? $decoded : null; + } + + return is_array($meta) ? $meta : null; + } + /** @param array $row */ private function mapProductId(array &$row): void { diff --git a/app/Http/Controllers/Auth/SsoLoginController.php b/app/Http/Controllers/Auth/SsoLoginController.php index de73378..e18b849 100644 --- a/app/Http/Controllers/Auth/SsoLoginController.php +++ b/app/Http/Controllers/Auth/SsoLoginController.php @@ -19,19 +19,21 @@ class SsoLoginController extends Controller { public function connect(Request $request): RedirectResponse { + $intended = (string) $request->query('redirect', route('hosting.dashboard')); + if (Auth::check()) { - return redirect()->route('hosting.dashboard'); + return $this->safeRedirect($intended, route('hosting.dashboard')); } $verifier = Str::random(64); $state = Str::random(40); $request->session()->put('sso.verifier', $verifier); $request->session()->put('sso.state', $state); - $request->session()->put('sso.intended', $request->query('redirect', route('hosting.dashboard'))); + $request->session()->put('sso.intended', $intended); $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='); - $query = http_build_query([ + $query = [ 'response_type' => 'code', 'client_id' => (string) config('services.ladill_sso.client_id'), 'redirect_uri' => (string) config('services.ladill_sso.redirect'), @@ -39,18 +41,40 @@ class SsoLoginController extends Controller 'state' => $state, 'code_challenge' => $challenge, 'code_challenge_method' => 'S256', - ]); + ]; - return redirect()->away(rtrim((string) config('services.ladill_sso.issuer'), '/').'/oauth/authorize?'.$query); + if (! $request->boolean('interactive')) { + $query['prompt'] = 'none'; + } + + return redirect()->away(rtrim((string) config('services.ladill_sso.issuer'), '/').'/oauth/authorize?'.http_build_query($query)); } public function callback(Request $request): RedirectResponse { - $authLogin = 'https://'.config('app.auth_domain').'/login'; + $intended = (string) $request->session()->get('sso.intended', route('hosting.dashboard')); - if ($request->filled('error') || ! $request->filled('code') + if ($request->filled('error')) { + if (in_array($request->query('error'), ['login_required', 'interaction_required', 'consent_required'], true) + && ! $request->boolean('interactive')) { + return redirect()->route('sso.connect', [ + 'redirect' => $intended, + 'interactive' => 1, + ]); + } + + return redirect()->route('sso.connect', [ + 'redirect' => $intended, + 'interactive' => 1, + ]); + } + + if (! $request->filled('code') || $request->query('state') !== $request->session()->pull('sso.state')) { - return redirect()->away($authLogin); + return redirect()->route('sso.connect', [ + 'redirect' => $intended, + 'interactive' => 1, + ]); } $issuer = rtrim((string) config('services.ladill_sso.issuer'), '/'); @@ -64,12 +88,18 @@ class SsoLoginController extends Controller 'code_verifier' => (string) $request->session()->pull('sso.verifier'), ]); if ($tokenRes->failed()) { - return redirect()->away($authLogin); + return redirect()->route('sso.connect', [ + 'redirect' => $intended, + 'interactive' => 1, + ]); } $claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo'); if ($claims->failed() || ! $claims->json('sub')) { - return redirect()->away($authLogin); + return redirect()->route('sso.connect', [ + 'redirect' => $intended, + 'interactive' => 1, + ]); } $user = User::updateOrCreate( @@ -81,14 +111,13 @@ class SsoLoginController extends Controller ], ); - // Accept any pending team invites for this email on first sign-in. \App\Models\HostingTeamMember::linkPendingInvitesFor($user); Auth::login($user, remember: true); $request->session()->regenerate(); $request->session()->forget('mailbox_link_banner_dismissed'); - return redirect()->intended($request->session()->pull('sso.intended', route('hosting.dashboard'))); + return $this->safeRedirect($intended, route('hosting.dashboard')); } public function logout(Request $request): RedirectResponse @@ -111,8 +140,6 @@ class SsoLoginController extends Controller $request->session()->regenerateToken(); } - // SLO chain: if the central sequencer passed a (ladill.com) return URL, - // continue the top-level redirect chain to the next app; else acknowledge. $return = (string) $request->query('return', ''); $root = (string) config('app.platform_domain', 'ladill.com'); $host = parse_url($return, PHP_URL_HOST); @@ -137,4 +164,17 @@ class SsoLoginController extends Controller return strtolower((string) $user->email) !== $mailbox; } + + private function safeRedirect(string $url, string $fallback): RedirectResponse + { + $host = parse_url($url, PHP_URL_HOST); + $root = (string) config('app.platform_domain', 'ladill.com'); + + if (is_string($host) && str_starts_with($url, 'https://') + && ($host === $root || str_ends_with($host, '.'.$root))) { + return redirect()->away($url); + } + + return redirect()->away($fallback); + } } diff --git a/app/Http/Controllers/Hosting/OverviewController.php b/app/Http/Controllers/Hosting/OverviewController.php index 6ee8c68..9c9285c 100644 --- a/app/Http/Controllers/Hosting/OverviewController.php +++ b/app/Http/Controllers/Hosting/OverviewController.php @@ -7,9 +7,11 @@ 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 Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Collection as SupportCollection; use Illuminate\Http\Request; use Illuminate\View\View; @@ -20,6 +22,7 @@ class OverviewController extends Controller { $user = $request->user(); $accounts = $this->sharedHostingAccounts($user); + $legacyOrders = $this->legacySharedHostingOrders($user); $accountIds = $accounts->pluck('id'); $pendingOrders = CustomerHostingOrder::query() @@ -36,17 +39,24 @@ class OverviewController extends Controller ->take(5) ->get(); - $linkedDomains = $accountIds->isEmpty() + $linkedDomains = ($accountIds->isEmpty() ? 0 - : HostedSite::query()->whereIn('hosting_account_id', $accountIds)->count(); + : HostedSite::query()->whereIn('hosting_account_id', $accountIds)->count()) + + $legacyOrders->filter(fn (HostingOrder $order) => filled($order->domain_name))->count(); return view('hosting.dashboard', [ 'accounts' => $accounts, - 'activeCount' => $accounts->where('status', HostingAccount::STATUS_ACTIVE)->count(), + 'legacyOrders' => $legacyOrders, + 'activeCount' => $accounts->where('status', HostingAccount::STATUS_ACTIVE)->count() + + $legacyOrders->where('status', HostingOrder::STATUS_ACTIVE)->count(), 'expiringCount' => $accounts ->where('status', HostingAccount::STATUS_ACTIVE) ->filter(fn (HostingAccount $account) => $account->expires_at !== null && $account->expires_at->between(now(), now()->addDays(30))) - ->count(), + ->count() + + $legacyOrders + ->where('status', HostingOrder::STATUS_ACTIVE) + ->filter(fn (HostingOrder $order) => $order->expires_at !== null && $order->expires_at->between(now(), now()->addDays(30))) + ->count(), 'pendingOrderCount' => CustomerHostingOrder::query() ->forUser($user->id) ->whereHas('product', fn ($q) => $q->whereIn('type', $this->sharedTypes())) @@ -59,15 +69,19 @@ class OverviewController extends Controller ->count(), 'linkedDomains' => $linkedDomains, 'recent' => $accounts->take(5), + 'recentLegacyOrders' => $legacyOrders->take(5), 'pendingOrders' => $pendingOrders, ]); } public function accounts(Request $request): View { + $user = $request->user(); + return view('hosting.accounts-list', [ 'title' => 'Your Hosting Accounts', - 'accounts' => $this->sharedHostingAccounts($request->user()), + 'accounts' => $this->sharedHostingAccounts($user), + 'legacyOrders' => $this->legacySharedHostingOrders($user), ]); } @@ -113,4 +127,19 @@ class OverviewController extends Controller return $hostingAccounts->merge($orderAccounts)->unique('id')->values(); } + + /** @return SupportCollection */ + private function legacySharedHostingOrders(User $user): SupportCollection + { + return HostingOrder::query() + ->where('user_id', $user->id) + ->whereIn('hosting_type', [ + HostingOrder::TYPE_SINGLE_DOMAIN, + HostingOrder::TYPE_MULTI_DOMAIN, + HostingOrder::TYPE_RESELLER, + ]) + ->whereNotIn('status', [HostingOrder::STATUS_CANCELLED]) + ->latest() + ->get(); + } } diff --git a/app/Models/HostingOrder.php b/app/Models/HostingOrder.php index 807986a..b1c7b04 100644 --- a/app/Models/HostingOrder.php +++ b/app/Models/HostingOrder.php @@ -345,17 +345,28 @@ class HostingOrder extends Model } } + /** + * @return array + */ + private function normalizedMeta(): array + { + $meta = $this->getAttributes()['meta'] ?? $this->meta ?? []; + + while (is_string($meta)) { + $decoded = json_decode($meta, true); + $meta = json_last_error() === JSON_ERROR_NONE ? $decoded : []; + } + + return is_array($meta) ? $meta : []; + } + private function metaValue(array $keys, mixed $default = null): mixed { - $meta = $this->meta ?? []; - $sources = []; + $meta = $this->normalizedMeta(); + $sources = [$meta]; - if (is_array($meta)) { - $sources[] = $meta; - - if (is_array($meta['raw'] ?? null)) { - $sources[] = $meta['raw']; - } + if (is_array($meta['raw'] ?? null)) { + $sources[] = $meta['raw']; } foreach ($sources as $source) { diff --git a/app/Services/Hosting/ResellerClubHostingService.php b/app/Services/Hosting/ResellerClubHostingService.php index dd0f0a9..8709624 100644 --- a/app/Services/Hosting/ResellerClubHostingService.php +++ b/app/Services/Hosting/ResellerClubHostingService.php @@ -489,7 +489,7 @@ class ResellerClubHostingService 'status' => $this->mapRcStatus($rcOrder['currentstatus'] ?? $rcOrder['orderstatus'] ?? $rcOrder['status'] ?? Arr::get($raw, 'entity.currentstatus') ?? Arr::get($raw, 'orders.orderstatus') ?? 'pending'), 'ip_address' => $rcOrder['ipaddress'] ?? $rcOrder['ip'] ?? $rcOrder['ip_address'] ?? Arr::get($raw, 'server.ipaddress') ?? Arr::get($raw, 'server.ip') ?? $raw['ipaddress'] ?? $raw['ip'] ?? null, 'cpanel_url' => $rcOrder['cpanelurl'] ?? $rcOrder['cpanel_url'] ?? $rcOrder['tempurl'] ?? Arr::get($raw, 'server.cpanelurl') ?? Arr::get($raw, 'server.controlpanelurl') ?? Arr::get($raw, 'server.tempurl') ?? $raw['cpanelurl'] ?? $raw['tempurl'] ?? null, - 'cpanel_username' => $rcOrder['cpanelusername'] ?? $rcOrder['cpanel_username'] ?? Arr::get($raw, 'server.cpanelusername') ?? Arr::get($raw, 'server.username') ?? $raw['cpanelusername'] ?? null, + 'cpanel_username' => $rcOrder['cpanelusername'] ?? $rcOrder['cpanel_username'] ?? $rcOrder['siteadminusername'] ?? Arr::get($raw, 'server.cpanelusername') ?? Arr::get($raw, 'server.username') ?? $raw['cpanelusername'] ?? $raw['siteadminusername'] ?? null, 'bandwidth_mb' => $this->unsignedLimitOrNull($rcOrder['bandwidth'] ?? Arr::get($raw, 'plan.bandwidth') ?? $raw['bandwidth'] ?? null), 'disk_space_mb' => $this->unsignedLimitOrNull($rcOrder['diskspace'] ?? Arr::get($raw, 'plan.diskspace') ?? $raw['diskspace'] ?? null), 'expires_at' => $this->hostingTimestamp($rcOrder['endtime'] ?? $rcOrder['end_time'] ?? Arr::get($raw, 'orders.endtime') ?? $raw['endtime'] ?? null), @@ -543,7 +543,7 @@ class ResellerClubHostingService 'status' => $data['currentstatus'] ?? $data['orderstatus'] ?? $data['status'] ?? Arr::get($data, 'entity.currentstatus') ?? Arr::get($data, 'orders.orderstatus') ?? 'Unknown', 'ip_address' => $data['ipaddress'] ?? $data['ip_address'] ?? $data['ip'] ?? Arr::get($data, 'server.ipaddress') ?? Arr::get($data, 'server.ip') ?? null, 'cpanel_url' => $data['cpanelurl'] ?? $data['cpanel_url'] ?? $data['controlpanelurl'] ?? $data['tempurl'] ?? Arr::get($data, 'server.cpanelurl') ?? Arr::get($data, 'server.controlpanelurl') ?? Arr::get($data, 'server.tempurl') ?? null, - 'cpanel_username' => $data['cpanelusername'] ?? $data['cpanel_username'] ?? $data['username'] ?? Arr::get($data, 'server.cpanelusername') ?? Arr::get($data, 'server.username') ?? null, + 'cpanel_username' => $data['cpanelusername'] ?? $data['cpanel_username'] ?? $data['siteadminusername'] ?? $data['username'] ?? Arr::get($data, 'server.cpanelusername') ?? Arr::get($data, 'server.username') ?? null, 'bandwidth' => $data['bandwidth'] ?? $data['bandwidth_mb'] ?? Arr::get($data, 'plan.bandwidth') ?? null, 'disk_space' => $data['diskspace'] ?? $data['disk_space_mb'] ?? Arr::get($data, 'plan.diskspace') ?? null, 'creation_time' => $data['creationtime'] ?? $data['creation_time'] ?? Arr::get($data, 'orders.creationtime') ?? null, diff --git a/bootstrap/app.php b/bootstrap/app.php index 80ef26f..0dffcc8 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -12,6 +13,9 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [ + 'redirect' => $request->fullUrl(), + ])); $middleware->web(append: [ \App\Http\Middleware\SetActingAccount::class, ]); diff --git a/resources/views/hosting/accounts-list.blade.php b/resources/views/hosting/accounts-list.blade.php index f7464aa..263ab29 100644 --- a/resources/views/hosting/accounts-list.blade.php +++ b/resources/views/hosting/accounts-list.blade.php @@ -60,6 +60,48 @@ @endforeach + + @foreach ($legacyOrders ?? [] as $order) + +
+
+
+ + + +
+
+

+ {{ $order->domain_name ?: 'Legacy hosting' }} +

+
+ {{ $order->cpanel_username ?: 'Legacy account' }} + · + {{ $order->plan_name ?? 'Legacy hosting' }} + + {{ ucfirst($order->status) }} + + Legacy +
+
+
+
+ + + + +
+
+
+ @endforeach
diff --git a/resources/views/hosting/dashboard.blade.php b/resources/views/hosting/dashboard.blade.php index 07ee685..a363eb7 100644 --- a/resources/views/hosting/dashboard.blade.php +++ b/resources/views/hosting/dashboard.blade.php @@ -29,11 +29,11 @@

Hosting accounts

- @if ($accounts->count() > 5) + @if ($accounts->count() + ($recentLegacyOrders ?? collect())->count() > 5) View all @endif
- @if ($recent->isEmpty()) + @if ($recent->isEmpty() && ($recentLegacyOrders ?? collect())->isEmpty()) @endif
diff --git a/resources/views/layouts/email.blade.php b/resources/views/layouts/email.blade.php index 9ecf02b..30b71d1 100644 --- a/resources/views/layouts/email.blade.php +++ b/resources/views/layouts/email.blade.php @@ -56,5 +56,6 @@ ]) @include('partials.afia') @endauth +@include('partials.sso-keepalive') diff --git a/resources/views/layouts/hosting.blade.php b/resources/views/layouts/hosting.blade.php index 3de0095..7d71fd4 100644 --- a/resources/views/layouts/hosting.blade.php +++ b/resources/views/layouts/hosting.blade.php @@ -55,5 +55,6 @@ ]) @include('partials.afia') @endauth +@include('partials.sso-keepalive') diff --git a/resources/views/partials/sso-keepalive.blade.php b/resources/views/partials/sso-keepalive.blade.php new file mode 100644 index 0000000..ecd5d4a --- /dev/null +++ b/resources/views/partials/sso-keepalive.blade.php @@ -0,0 +1,4 @@ +@if (auth()->check()) + {{-- Same-site iframe keeps the shared auth.ladill.com session warm while using this app. --}} + +@endif