Files
ladill-hosting/app/Http/Controllers/Hosting/AccountController.php
T
isaaccladandCursor e251a4cf60
Deploy Ladill Hosting / deploy (push) Failing after 17s
Initial Ladill Hosting app with Gitea deploy pipeline.
Shared web hosting extracted from the platform monolith, with CI deploy
to /var/www/ladill-hosting matching Bird/Domains/Email.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 16:24:20 +00:00

277 lines
9.5 KiB
PHP

<?php
namespace App\Http\Controllers\Hosting;
use App\Http\Controllers\Controller;
use App\Models\EmailSetting;
use App\Models\User;
use App\Services\Billing\BillingClient;
use App\Services\Identity\IdentityClient;
use App\Services\Mailbox\MailboxClient;
use App\Services\Payment\WalletPaymentService;
use App\Support\MailboxPricing;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
/**
* Account — Wallet, Billing, Settings. Money is read from the one platform
* UserWallet via the Billing API (tagged 'email'); funding is platform-level, so
* "Add funds" deep-links to the account portal. Settings are email-local prefs.
*/
class AccountController extends Controller
{
public function __construct(
private WalletPaymentService $wallet,
private BillingClient $billing,
private MailboxClient $mailboxes,
private IdentityClient $identity,
) {}
private function topupUrl(): string
{
return 'https://'.config('app.account_domain').'/wallet';
}
/** Wallet: balance + email spend summary + add-funds link. */
public function wallet(): View
{
$account = ladill_account();
[$balanceMinor, $ledger] = $this->billingSnapshot($account?->public_id);
return view('hosting.account.wallet', [
'balanceMinor' => $balanceMinor,
'spentMinor' => (int) ($ledger['spent_minor'] ?? 0),
'creditedMinor' => (int) ($ledger['credited_minor'] ?? 0),
'topupUrl' => $this->topupUrl(),
]);
}
/** Billing: wallet snapshot + recurring paid-mailbox subscriptions. */
public function billing(): View
{
$account = ladill_account();
[$balanceMinor, $ledger] = $this->billingSnapshot($account?->public_id);
$paidMailboxes = [];
$monthlyTotalMinor = 0;
try {
foreach ($this->mailboxes->forUser((string) $account?->public_id) as $m) {
if (! ($m['is_paid'] ?? false)) {
continue;
}
$m['price_minor'] = MailboxPricing::priceMinorFor((int) ($m['quota_mb'] ?? 0));
$m['quota_label'] = MailboxPricing::label((int) ($m['quota_mb'] ?? 0));
$monthlyTotalMinor += $m['price_minor'];
$paidMailboxes[] = $m;
}
} catch (\Throwable $e) {
report($e);
}
return view('hosting.account.billing', [
'balanceMinor' => $balanceMinor,
'spentMinor' => (int) ($ledger['spent_minor'] ?? 0),
'creditedMinor' => (int) ($ledger['credited_minor'] ?? 0),
'paidMailboxes' => $paidMailboxes,
'monthlyTotalMinor' => $monthlyTotalMinor,
'topupUrl' => $this->topupUrl(),
]);
}
public function settings(): View
{
$account = ladill_account();
$settings = EmailSetting::firstOrNew(['user_id' => $account?->id]);
['linkStatus' => $linkStatus, 'mailboxOptions' => $mailboxOptions, 'showMailboxLinkUi' => $showMailboxLinkUi] = $this->mailboxLinkContext($account);
return view('hosting.account.settings', [
'account' => $account,
'settings' => $settings,
'linkStatus' => $linkStatus,
'mailboxOptions' => $mailboxOptions,
'showMailboxLinkUi' => $showMailboxLinkUi,
]);
}
public function linkMailbox(Request $request): RedirectResponse
{
$account = ladill_account();
$data = $request->validate([
'mailbox_address' => ['required', 'email', 'max:255'],
]);
try {
$result = $this->identity->linkMailbox(
(string) $account->public_id,
$data['mailbox_address'],
);
} catch (\Illuminate\Http\Client\RequestException $e) {
$message = (string) $e->response?->json('message', '');
if ($message !== '') {
return redirect()->route('account.settings')->with('error', $message);
}
report($e);
return redirect()->route('account.settings')->with('error', 'Could not link mailbox. Try again in a moment, or contact support if this keeps happening.');
} catch (\Throwable $e) {
report($e);
return redirect()->route('account.settings')->with('error', 'Could not link mailbox. Try again in a moment, or contact support if this keeps happening.');
}
$request->session()->forget('mailbox_link_banner_dismissed');
$connectUrl = (string) ($result['webmail_connect_url'] ?? '');
if ($connectUrl === '') {
$connectUrl = rtrim((string) config('services.ladill_webmail.url', 'https://mail.ladill.com'), '/').'/sso/connect';
}
return redirect()->away($connectUrl);
}
public function unlinkMailbox(Request $request): RedirectResponse
{
$account = ladill_account();
try {
$this->identity->unlinkMailbox((string) $account->public_id);
} catch (\Illuminate\Http\Client\RequestException $e) {
$message = (string) $e->response?->json('message', '');
if ($message !== '') {
return redirect()->route('account.settings')->with('error', $message);
}
report($e);
return redirect()->route('account.settings')->with('error', 'Could not unlink mailbox. Try again in a moment.');
} catch (\Throwable $e) {
report($e);
return redirect()->route('account.settings')->with('error', 'Could not unlink mailbox. Try again in a moment.');
}
$request->session()->forget('mailbox_link_banner_dismissed');
return redirect()->route('account.settings')->with('success', 'Mailbox unlinked from your Ladill account.');
}
/** @return array{linkStatus: array<string, mixed>, mailboxOptions: array<int, array<string, mixed>>, showMailboxLinkUi: bool} */
private function mailboxLinkContext(?User $account): array
{
$linkStatus = ['show_reminder' => false];
$mailboxOptions = [];
if (! $account?->public_id) {
return [
'linkStatus' => $linkStatus,
'mailboxOptions' => $mailboxOptions,
'showMailboxLinkUi' => false,
];
}
try {
$mailboxOptions = $this->mailboxes->forUser((string) $account->public_id);
} catch (\Throwable $e) {
report($e);
}
try {
$linkStatus = $this->identity->mailboxLinkStatus((string) $account->public_id);
} catch (\Throwable $e) {
report($e);
$linkStatus = $this->localMailboxLinkStatus($account, $mailboxOptions);
}
$showMailboxLinkUi = (bool) ($linkStatus['linked_mailbox'] ?? null)
|| ($linkStatus['show_reminder'] ?? false)
|| $this->localNeedsMailboxLink($account, $mailboxOptions, $linkStatus);
return [
'linkStatus' => $linkStatus,
'mailboxOptions' => $mailboxOptions,
'showMailboxLinkUi' => $showMailboxLinkUi,
];
}
/** @param array<int, array<string, mixed>> $mailboxOptions */
private function localNeedsMailboxLink(User $account, array $mailboxOptions, array $linkStatus): bool
{
if ($linkStatus['linked_mailbox'] ?? null) {
return false;
}
$email = strtolower(trim($account->email ?? ''));
if ($email === '') {
return false;
}
foreach ($mailboxOptions as $mailbox) {
if (strtolower((string) ($mailbox['address'] ?? '')) === $email) {
return false;
}
}
return count($mailboxOptions) > 0;
}
/** @param array<int, array<string, mixed>> $mailboxOptions */
/** @return array<string, mixed> */
private function localMailboxLinkStatus(User $account, array $mailboxOptions): array
{
$needsLink = $this->localNeedsMailboxLink($account, $mailboxOptions, []);
return [
'show_reminder' => $needsLink,
'stage' => count($mailboxOptions) > 0 ? 'needs_link' : 'needs_mailbox',
'linked_mailbox' => null,
'account_email' => $account->email,
];
}
public function updateSettings(Request $request): RedirectResponse
{
$account = ladill_account();
$data = $request->validate([
'default_quota_mb' => ['nullable', 'integer', 'min:256', 'max:153600'],
'notify_email' => ['nullable', 'email', 'max:255'],
'product_updates' => ['nullable', 'boolean'],
]);
EmailSetting::updateOrCreate(
['user_id' => $account->id],
[
'default_quota_mb' => $data['default_quota_mb'] ?? null,
'notify_email' => $data['notify_email'] ?? null,
'product_updates' => $request->boolean('product_updates'),
],
);
return redirect()->route('account.settings')->with('success', 'Settings saved.');
}
/**
* @return array{0:int,1:array<string,mixed>} [balanceMinor, ledger]
*/
private function billingSnapshot(?string $publicId): array
{
if (! $publicId) {
return [0, []];
}
try {
return [
$this->wallet->balanceMinor(ladill_account()),
$this->billing->serviceLedger($publicId, (string) config('billing.service', 'email')),
];
} catch (\Throwable $e) {
report($e);
return [0, []];
}
}
}