name('overview'); Route::get('/customers', Admin\Customers::class)->name('customers'); Route::get('/instances', Admin\Instances::class)->name('instances'); Route::get('/hosts', Admin\Hosts::class)->name('hosts'); Route::get('/hosts/create', Admin\HostCreate::class)->name('hosts.create'); Route::get('/hosts/{host}', Admin\HostDetail::class)->name('hosts.show'); Route::get('/datacenters', Admin\Datacenters::class)->name('datacenters'); Route::get('/plans', Admin\Plans::class)->name('plans'); Route::get('/plans/{uuid}', Admin\PlanVersions::class)->name('plans.versions'); // POST so the identity change is CSRF-protected (a GET could be forced cross-site). Route::post('/impersonate/{customer}', [ImpersonationController::class, 'start'])->name('impersonate'); Route::get('/provisioning', Admin\Provisioning::class)->name('provisioning'); Route::get('/maintenance', Admin\Maintenance::class)->name('maintenance'); Route::get('/vpn', Admin\Vpn::class)->name('vpn'); Route::get('/revenue', Admin\Revenue::class)->name('revenue'); Route::get('/mail', Admin\Mail::class)->name('mail'); Route::get('/secrets', Admin\Secrets::class)->name('secrets'); Route::get('/settings', Admin\Settings::class)->name('settings'); // POST so Laravel's CSRF middleware protects it, like every other state change. Route::post('/logout', function () { Auth::guard('operator')->logout(); // NOT session()->invalidate(): where the console and the portal share a // host (shared/fallback mode — this VM's own mode), both guards keep // their login key in the SAME session. invalidate() is flush() + // migrate(true) — it discards every attribute in the session, not just // the operator's, so it would silently sign a customer out of the portal // too if the same browser happened to carry both. regenerate() issues a // new session id (no fixation risk) without touching the other // attributes, so the 'web' guard's own login key — already removed for // 'operator' by the logout() call above — survives untouched. session()->regenerate(); session()->regenerateToken(); return redirect()->route('admin.login'); })->name('logout'); /* * A deployment restarts the very application that is watching it. * * The settings card polls itself with wire:poll, which is fine until the run * reaches the restart: the requests fail, and what comes back afterwards is a * new build being asked questions by the old page's JavaScript. So the card * never learned that the update had finished and an operator had to reload to * find out — reported exactly that way. * * This is the smallest thing that can answer "is it done, and is it still the * same build" across that gap: no Livewire, no component state, no assets. A * failed request here is expected (it IS the restart) and the poller keeps * trying rather than giving up. */ Route::get('/update/state', function (\App\Services\Deployment\UpdateChannel $channel) { $state = $channel->state(); return response()->json([ 'running' => $state['running'], 'commit' => $state['commit'], 'behind' => $state['behind'], 'last_finished_at' => $state['last_finished_at']?->toIso8601String(), ])->header('Cache-Control', 'no-store'); })->name('update.state');