Unify SSO and surface legacy ResellerClub hosting orders on the dashboard.
Deploy Ladill Hosting / deploy (push) Successful in 26s
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user