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:
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user