91 lines
4.4 KiB
PHP
91 lines
4.4 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\ImpersonationController;
|
|
use App\Livewire\Admin;
|
|
use App\Services\Deployment\UpdateChannel;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
/*
|
|
| The operator console.
|
|
|
|
|
| Its own file, and registered BEFORE routes/web.php, because once the console
|
|
| has a hostname to itself it lives at the ROOT of that host — where `/` and
|
|
| `/settings` also exist in the customer portal, and the first matching route
|
|
| wins.
|
|
|
|
|
| Where it is mounted is AdminArea's decision, not this file's: `/` on the
|
|
| console hostname where the console has one to itself, `/admin` on any host
|
|
| otherwise. Route NAMES are `admin.*` in both cases, so every route('admin.…')
|
|
| in the views is unaffected by which mode an installation is in.
|
|
|
|
|
| RestrictAdminHost is listed here as well as prepended to the `web` group:
|
|
| Livewire records it on the component snapshot and re-applies it to
|
|
| /livewire/update, so a console action cannot be driven through another host.
|
|
*/
|
|
Route::get('/', Admin\Overview::class)->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');
|
|
// Its own route so RequireOperatorTwoFactor can exempt ENROLMENT without
|
|
// exempting the rest of admin.settings — see that middleware for why.
|
|
Route::get('/two-factor-setup', Admin\TwoFactorSetup::class)->name('two-factor-setup');
|
|
|
|
// 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 (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');
|