app->singleton(PipelineRegistry::class, fn () => new PipelineRegistry( config('provisioning.pipelines', []), )); // Real I/O implementations; tests swap in fakes via app()->instance(). $this->app->bind(RemoteShell::class, PhpseclibRemoteShell::class); $this->app->bind(WireguardHub::class, LocalWireguardHub::class); $this->app->bind(ProxmoxClient::class, HttpProxmoxClient::class); $this->app->bind(HetznerDnsClient::class, HttpHetznerDnsClient::class); $this->app->bind(TraefikWriter::class, SshTraefikWriter::class); $this->app->bind(MonitoringClient::class, HttpMonitoringClient::class); $this->app->bind(StripeClient::class, HttpStripeClient::class); } /** * Bootstrap any application services. */ public function boot(): void { // Every timestamp that gets shown to somebody goes through ->local() // first. Storage is UTC and stays UTC; this is the last step before a // human reads it. // // It exists as one macro rather than a convention because the // convention failed: fourteen views formatted times straight out of // the database, and two of them converted to the STORAGE zone — which // reads like "convert to local" and, that zone being UTC, does // nothing. A no-op in the costume of a fix is worse than no call at // all, because it stops the next person looking. // // copy() is not decoration: Illuminate\Support\Carbon is MUTABLE, so // setTimezone() on the instance would rewrite the model attribute as // a side effect of rendering it, and whatever compared or saved that // value afterwards would be an hour or two out. // Registered on both classes with the SAME body on purpose: Carbon // keeps macros in one global table, so a second registration replaces // the first for every Carbon class. Two different bodies means the // last one wins for both — which is how the immutable version, safe // for itself, silently became the mutable one's implementation too. $local = fn () => $this->copy()->setTimezone(config('app.display_timezone')); Carbon::macro('local', $local); CarbonImmutable::macro('local', $local); // Livewire posts every component action to /livewire/update, which a // path-based guard would skip. Marking the host restriction persistent // makes Livewire re-apply it from the component's original route, so an // admin action cannot be driven through a public hostname. // Livewire re-applies only the middleware on this list when an action // posts to /livewire/update. Its own defaults cover `auth` — but not // ours. Without these two, a signed-in NON-operator could drive console // components, and a suspended customer could keep driving the portal: // the page would never load for them, but the page is not where the // actions run. Livewire::addPersistentMiddleware([ RestrictAdminHost::class, RestrictConsoleNetwork::class, EnsureAdmin::class, EnsureCustomerActive::class, ]); // Send-time guard for maintenance mail (X-CP-Notification carries the // ledger id). Deliberately READ-ONLY — it never marks the row sent or // claimed, because returning false makes Laravel treat the send as a // successful cancellation: pre-claiming here would silently DROP a mail // whose transport later fails and retries. It only suppresses a send that // is definitively pointless: the window was cancelled, or a sibling copy // already delivered (sent_at set). sent_at is recorded on real delivery // in MessageSent, so nothing is ever lost. // // Residual: two jobs whose MessageSending both fire before either's // MessageSent (true simultaneous multi-worker send of a stale resend) can // still both deliver. That cannot happen on the current single-worker // log-mail setup; true exactly-once needs a transactional outbox + a // dedicated delivery worker keyed on claimed_at, to be added with real mail. Event::listen(MessageSending::class, function (MessageSending $event) { $header = $event->message->getHeaders()->get('X-CP-Notification'); if ($header === null) { return null; } $notification = MaintenanceNotification::query()->with('window')->find((int) $header->getBodyAsString()); if ($notification === null) { return null; } if ($notification->sent_at !== null) { return false; // already delivered — suppress a duplicate copy } if ($notification->event === 'announcement' && $notification->window?->state === 'cancelled') { return false; // window cancelled meanwhile — do not deliver } return null; }); // Stamp a maintenance-notification ledger row as delivered only once the // mail is actually sent (the X-CP-Notification header carries the id). // Until then sent_at stays null → the row is a retryable marker. If an // announcement delivered for an already-cancelled window (the cancel race), // queue a catch-up cancellation so that customer isn't left mis-informed. Event::listen(MessageSent::class, function (MessageSent $event) { $header = $event->message->getHeaders()->get('X-CP-Notification'); if ($header === null) { return; } $notification = MaintenanceNotification::query()->with(['window', 'customer'])->find((int) $header->getBodyAsString()); if ($notification === null) { return; } $notification->update(['sent_at' => now()]); if ($notification->event === 'announcement' && $notification->window?->state === 'cancelled' && $notification->customer !== null) { app(MaintenanceNotifier::class)->deliver( $notification->window, $notification->customer, 'cancelled', new MaintenanceCancelledMail($notification->window, $notification->customer), ); } }); } }