Unify SSO and surface legacy ResellerClub hosting orders on the dashboard.
Deploy Ladill Hosting / deploy (push) Successful in 26s

Add silent OAuth and session keepalive, and show imported RC orders alongside
native hosting accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-07 06:33:16 +00:00
co-authored by Cursor
parent c91d25eb3b
commit e3fd235139
11 changed files with 196 additions and 31 deletions
@@ -222,6 +222,7 @@ class ImportHostingCommand extends Command
unset($row['id'], $row['owner_public_id'], $row['owner_email']); unset($row['id'], $row['owner_public_id'], $row['owner_email']);
$row['user_id'] = $owner->id; $row['user_id'] = $owner->id;
$row['platform_id'] = $platformId; $row['platform_id'] = $platformId;
$row['meta'] = $this->normalizeMeta($row['meta'] ?? null);
if ($commit) { if ($commit) {
HostingOrder::updateOrCreate(['platform_id' => $platformId], $row); HostingOrder::updateOrCreate(['platform_id' => $platformId], $row);
@@ -335,6 +336,17 @@ class ImportHostingCommand extends Command
]); ]);
} }
/** @return array<string, mixed>|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<string, mixed> $row */ /** @param array<string, mixed> $row */
private function mapProductId(array &$row): void private function mapProductId(array &$row): void
{ {
@@ -19,19 +19,21 @@ class SsoLoginController extends Controller
{ {
public function connect(Request $request): RedirectResponse public function connect(Request $request): RedirectResponse
{ {
$intended = (string) $request->query('redirect', route('hosting.dashboard'));
if (Auth::check()) { if (Auth::check()) {
return redirect()->route('hosting.dashboard'); return $this->safeRedirect($intended, route('hosting.dashboard'));
} }
$verifier = Str::random(64); $verifier = Str::random(64);
$state = Str::random(40); $state = Str::random(40);
$request->session()->put('sso.verifier', $verifier); $request->session()->put('sso.verifier', $verifier);
$request->session()->put('sso.state', $state); $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)), '+/', '-_'), '='); $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$query = http_build_query([ $query = [
'response_type' => 'code', 'response_type' => 'code',
'client_id' => (string) config('services.ladill_sso.client_id'), 'client_id' => (string) config('services.ladill_sso.client_id'),
'redirect_uri' => (string) config('services.ladill_sso.redirect'), 'redirect_uri' => (string) config('services.ladill_sso.redirect'),
@@ -39,18 +41,40 @@ class SsoLoginController extends Controller
'state' => $state, 'state' => $state,
'code_challenge' => $challenge, 'code_challenge' => $challenge,
'code_challenge_method' => 'S256', '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 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')) { || $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'), '/'); $issuer = rtrim((string) config('services.ladill_sso.issuer'), '/');
@@ -64,12 +88,18 @@ class SsoLoginController extends Controller
'code_verifier' => (string) $request->session()->pull('sso.verifier'), 'code_verifier' => (string) $request->session()->pull('sso.verifier'),
]); ]);
if ($tokenRes->failed()) { 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'); $claims = Http::withToken((string) $tokenRes->json('access_token'))->acceptJson()->get($issuer.'/oauth/userinfo');
if ($claims->failed() || ! $claims->json('sub')) { if ($claims->failed() || ! $claims->json('sub')) {
return redirect()->away($authLogin); return redirect()->route('sso.connect', [
'redirect' => $intended,
'interactive' => 1,
]);
} }
$user = User::updateOrCreate( $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); \App\Models\HostingTeamMember::linkPendingInvitesFor($user);
Auth::login($user, remember: true); Auth::login($user, remember: true);
$request->session()->regenerate(); $request->session()->regenerate();
$request->session()->forget('mailbox_link_banner_dismissed'); $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 public function logout(Request $request): RedirectResponse
@@ -111,8 +140,6 @@ class SsoLoginController extends Controller
$request->session()->regenerateToken(); $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', ''); $return = (string) $request->query('return', '');
$root = (string) config('app.platform_domain', 'ladill.com'); $root = (string) config('app.platform_domain', 'ladill.com');
$host = parse_url($return, PHP_URL_HOST); $host = parse_url($return, PHP_URL_HOST);
@@ -137,4 +164,17 @@ class SsoLoginController extends Controller
return strtolower((string) $user->email) !== $mailbox; 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);
}
} }
@@ -7,9 +7,11 @@ use App\Models\CustomerHostingOrder;
use App\Models\HostedSite; use App\Models\HostedSite;
use App\Models\HostingAccount; use App\Models\HostingAccount;
use App\Models\HostingAccountMember; use App\Models\HostingAccountMember;
use App\Models\HostingOrder;
use App\Models\HostingProduct; use App\Models\HostingProduct;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
@@ -20,6 +22,7 @@ class OverviewController extends Controller
{ {
$user = $request->user(); $user = $request->user();
$accounts = $this->sharedHostingAccounts($user); $accounts = $this->sharedHostingAccounts($user);
$legacyOrders = $this->legacySharedHostingOrders($user);
$accountIds = $accounts->pluck('id'); $accountIds = $accounts->pluck('id');
$pendingOrders = CustomerHostingOrder::query() $pendingOrders = CustomerHostingOrder::query()
@@ -36,17 +39,24 @@ class OverviewController extends Controller
->take(5) ->take(5)
->get(); ->get();
$linkedDomains = $accountIds->isEmpty() $linkedDomains = ($accountIds->isEmpty()
? 0 ? 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', [ return view('hosting.dashboard', [
'accounts' => $accounts, '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 'expiringCount' => $accounts
->where('status', HostingAccount::STATUS_ACTIVE) ->where('status', HostingAccount::STATUS_ACTIVE)
->filter(fn (HostingAccount $account) => $account->expires_at !== null && $account->expires_at->between(now(), now()->addDays(30))) ->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() 'pendingOrderCount' => CustomerHostingOrder::query()
->forUser($user->id) ->forUser($user->id)
->whereHas('product', fn ($q) => $q->whereIn('type', $this->sharedTypes())) ->whereHas('product', fn ($q) => $q->whereIn('type', $this->sharedTypes()))
@@ -59,15 +69,19 @@ class OverviewController extends Controller
->count(), ->count(),
'linkedDomains' => $linkedDomains, 'linkedDomains' => $linkedDomains,
'recent' => $accounts->take(5), 'recent' => $accounts->take(5),
'recentLegacyOrders' => $legacyOrders->take(5),
'pendingOrders' => $pendingOrders, 'pendingOrders' => $pendingOrders,
]); ]);
} }
public function accounts(Request $request): View public function accounts(Request $request): View
{ {
$user = $request->user();
return view('hosting.accounts-list', [ return view('hosting.accounts-list', [
'title' => 'Your Hosting Accounts', '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 $hostingAccounts->merge($orderAccounts)->unique('id')->values();
} }
/** @return SupportCollection<int, HostingOrder> */
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();
}
} }
+19 -8
View File
@@ -345,17 +345,28 @@ class HostingOrder extends Model
} }
} }
/**
* @return array<string, mixed>
*/
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 private function metaValue(array $keys, mixed $default = null): mixed
{ {
$meta = $this->meta ?? []; $meta = $this->normalizedMeta();
$sources = []; $sources = [$meta];
if (is_array($meta)) { if (is_array($meta['raw'] ?? null)) {
$sources[] = $meta; $sources[] = $meta['raw'];
if (is_array($meta['raw'] ?? null)) {
$sources[] = $meta['raw'];
}
} }
foreach ($sources as $source) { foreach ($sources as $source) {
@@ -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'), '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, '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_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), '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), '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), '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', '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, '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_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, '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, '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, 'creation_time' => $data['creationtime'] ?? $data['creation_time'] ?? Arr::get($data, 'orders.creationtime') ?? null,
+4
View File
@@ -3,6 +3,7 @@
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@@ -12,6 +13,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->redirectGuestsTo(fn (Request $request) => route('sso.connect', [
'redirect' => $request->fullUrl(),
]));
$middleware->web(append: [ $middleware->web(append: [
\App\Http\Middleware\SetActingAccount::class, \App\Http\Middleware\SetActingAccount::class,
]); ]);
@@ -60,6 +60,48 @@
</div> </div>
</a> </a>
@endforeach @endforeach
@foreach ($legacyOrders ?? [] as $order)
<a href="{{ route('hosting.legacy.show', $order) }}"
class="group block rounded-xl border border-slate-200 bg-white p-5 hover:border-indigo-300 hover:shadow-md transition-all duration-200">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-100">
<svg class="h-6 w-6 text-amber-700" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<h3 class="font-semibold text-slate-900 group-hover:text-indigo-600 transition">
{{ $order->domain_name ?: 'Legacy hosting' }}
</h3>
<div class="flex items-center gap-3 mt-1">
<span class="text-sm text-slate-500">{{ $order->cpanel_username ?: 'Legacy account' }}</span>
<span class="text-sm text-slate-400">·</span>
<span class="text-sm text-slate-500">{{ $order->plan_name ?? 'Legacy hosting' }}</span>
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
@if($order->status === 'active') bg-emerald-100 text-emerald-700
@elseif($order->status === 'suspended') bg-red-100 text-red-700
@else bg-slate-100 text-slate-700 @endif">
{{ ucfirst($order->status) }}
</span>
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800">Legacy</span>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<div class="text-right hidden sm:block">
@if($order->expires_at)
<p class="text-xs text-slate-400">Expires {{ $order->expires_at->format('M j, Y') }}</p>
@endif
</div>
<svg class="h-5 w-5 text-slate-400 group-hover:text-indigo-600 group-hover:translate-x-1 transition-all" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
</svg>
</div>
</div>
</a>
@endforeach
</div> </div>
<div class="mt-8 rounded-xl border border-dashed border-slate-300 bg-slate-50 p-6 text-center"> <div class="mt-8 rounded-xl border border-dashed border-slate-300 bg-slate-50 p-6 text-center">
+23 -2
View File
@@ -29,11 +29,11 @@
<div class="mt-6 rounded-2xl border border-slate-200 bg-white"> <div class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3.5"> <div class="flex items-center justify-between border-b border-slate-100 px-5 py-3.5">
<h2 class="text-sm font-semibold text-slate-900">Hosting accounts</h2> <h2 class="text-sm font-semibold text-slate-900">Hosting accounts</h2>
@if ($accounts->count() > 5) @if ($accounts->count() + ($recentLegacyOrders ?? collect())->count() > 5)
<a href="{{ route('hosting.accounts.index') }}" class="text-xs font-medium text-indigo-600 hover:underline">View all</a> <a href="{{ route('hosting.accounts.index') }}" class="text-xs font-medium text-indigo-600 hover:underline">View all</a>
@endif @endif
</div> </div>
@if ($recent->isEmpty()) @if ($recent->isEmpty() && ($recentLegacyOrders ?? collect())->isEmpty())
<div class="px-5 py-12 text-center"> <div class="px-5 py-12 text-center">
<p class="text-sm font-semibold text-slate-700">No hosting accounts yet</p> <p class="text-sm font-semibold text-slate-700">No hosting accounts yet</p>
<p class="mx-auto mt-1 max-w-sm text-xs text-slate-400">Choose a plan to get started single domain, multi domain, or WordPress hosting.</p> <p class="mx-auto mt-1 max-w-sm text-xs text-slate-400">Choose a plan to get started single domain, multi domain, or WordPress hosting.</p>
@@ -75,6 +75,27 @@
])>{{ $account->status }}</span> ])>{{ $account->status }}</span>
</a> </a>
@endforeach @endforeach
@foreach ($recentLegacyOrders ?? [] as $order)
<a href="{{ route('hosting.legacy.show', $order) }}" class="flex items-center justify-between px-5 py-3.5 transition hover:bg-slate-50">
<div class="min-w-0">
<p class="truncate text-sm font-medium text-slate-900">{{ $order->domain_name ?: 'Legacy hosting' }}</p>
<p class="mt-0.5 text-xs text-slate-400">
{{ $order->cpanel_username ?: 'Legacy account' }}
· {{ $order->plan_name ?? 'Legacy hosting' }}
· Legacy
@if ($order->expires_at)
· Expires {{ $order->expires_at->format('d M Y') }}
@endif
</p>
</div>
<span @class([
'shrink-0 rounded-full px-2.5 py-1 text-[11px] font-medium capitalize',
'bg-emerald-50 text-emerald-700' => $order->status === 'active',
'bg-red-50 text-red-700' => $order->status === 'suspended',
'bg-slate-100 text-slate-600' => ! in_array($order->status, ['active', 'suspended'], true),
])>{{ $order->status }}</span>
</a>
@endforeach
</div> </div>
@endif @endif
</div> </div>
+1
View File
@@ -56,5 +56,6 @@
]) ])
@include('partials.afia') @include('partials.afia')
@endauth @endauth
@include('partials.sso-keepalive')
</body> </body>
</html> </html>
@@ -55,5 +55,6 @@
]) ])
@include('partials.afia') @include('partials.afia')
@endauth @endauth
@include('partials.sso-keepalive')
</body> </body>
</html> </html>
@@ -0,0 +1,4 @@
@if (auth()->check())
{{-- Same-site iframe keeps the shared auth.ladill.com session warm while using this app. --}}
<iframe src="https://{{ config('app.auth_domain') }}/sso/ping" hidden width="0" height="0" title=""></iframe>
@endif