134 lines
6.1 KiB
PHP
134 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Providers;
|
|
|
|
use App\Provisioning\PipelineRegistry;
|
|
use App\Services\Dns\HetznerDnsClient;
|
|
use App\Services\Dns\HttpHetznerDnsClient;
|
|
use App\Services\Monitoring\HttpMonitoringClient;
|
|
use App\Services\Monitoring\MonitoringClient;
|
|
use App\Services\Proxmox\HttpProxmoxClient;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
use App\Services\Stripe\HttpStripeClient;
|
|
use App\Services\Stripe\StripeClient;
|
|
use App\Services\Ssh\PhpseclibRemoteShell;
|
|
use App\Services\Ssh\RemoteShell;
|
|
use App\Services\Traefik\SshTraefikWriter;
|
|
use App\Services\Traefik\TraefikWriter;
|
|
use App\Services\Wireguard\LocalWireguardHub;
|
|
use App\Services\Wireguard\WireguardHub;
|
|
use App\Http\Middleware\EnsureAdmin;
|
|
use App\Http\Middleware\EnsureCustomerActive;
|
|
use App\Http\Middleware\RestrictAdminHost;
|
|
use App\Mail\MaintenanceCancelledMail;
|
|
use App\Models\MaintenanceNotification;
|
|
use App\Services\Maintenance\MaintenanceNotifier;
|
|
use Illuminate\Mail\Events\MessageSending;
|
|
use Illuminate\Mail\Events\MessageSent;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\ServiceProvider;
|
|
use Livewire\Livewire;
|
|
|
|
class AppServiceProvider extends ServiceProvider
|
|
{
|
|
/**
|
|
* Register any application services.
|
|
*/
|
|
public function register(): void
|
|
{
|
|
$this->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
|
|
{
|
|
// 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,
|
|
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),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|