clusev/routes/web.php

93 lines
3.9 KiB
PHP

<?php
use App\Http\Middleware\EnsureSecurityOnboarded;
use App\Http\Middleware\SetLocale;
use App\Livewire\Audit;
use App\Livewire\Auth;
use App\Livewire\Dashboard;
use App\Livewire\Files;
use App\Livewire\Servers;
use App\Livewire\Services;
use App\Livewire\Settings;
use App\Livewire\System;
use App\Livewire\Versions;
use App\Services\DeploymentService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth as AuthFacade;
use Illuminate\Support\Facades\Route;
// Language switch (public — also usable from the login page). Persists to the signed-in
// user and the session; only known locales (R16) are accepted, then we return back.
Route::get('/locale/{locale}', function (Request $request, string $locale) {
if (in_array($locale, SetLocale::SUPPORTED, true)) {
$request->session()->put('locale', $locale);
if ($user = $request->user()) {
$user->forceFill(['locale' => $locale])->save();
}
}
return redirect()->back();
})->name('locale.set');
// Caddy on-demand TLS gate: Caddy calls this before issuing a certificate for a host.
// Approve ONLY the configured panel domain, so Caddy can never be tricked into
// requesting certs for arbitrary domains pointed at this IP (rate-limit abuse).
Route::get('/_caddy/ask', function (Request $request, DeploymentService $deployment) {
$asked = strtolower(trim((string) $request->query('domain')));
$domain = $deployment->domain();
abort_unless($domain !== null && $asked !== '' && $asked === $domain, 403);
return response('ok', 200);
})->name('caddy.ask');
Route::middleware('guest')->group(function () {
Route::get('/login', Auth\Login::class)->name('login');
Route::get('/forgot-password', Auth\ForgotPassword::class)->name('password.request');
Route::get('/reset-password/{token}', Auth\ResetPassword::class)->name('password.reset');
Route::get('/two-factor-challenge', Auth\TwoFactorChallenge::class)->name('two-factor.challenge');
});
Route::middleware('auth')->group(function () {
// Security onboarding — reachable before the panel is unlocked.
Route::get('/password', Auth\PasswordChange::class)->name('password.change');
Route::get('/two-factor-setup', Auth\TwoFactorSetup::class)->name('two-factor.setup');
// 2FA backup codes — shown right after enrolling (before the onboarding gate) and
// manageable later from Settings.
Route::get('/two-factor/recovery-codes', Auth\RecoveryCodes::class)->name('two-factor.recovery');
Route::get('/two-factor/recovery-codes/download', function () {
$codes = AuthFacade::user()->recoveryCodes();
return response()->streamDownload(
fn () => print (implode("\n", $codes)."\n"),
'clusev-recovery-codes.txt',
['Content-Type' => 'text/plain; charset=UTF-8'],
);
})->name('two-factor.recovery.download');
Route::post('/logout', function () {
AuthFacade::logout();
session()->invalidate();
session()->regenerateToken();
return redirect()->route('login');
})->name('logout');
// The panel — requires completed onboarding (password rotated + 2FA enrolled).
Route::middleware(EnsureSecurityOnboarded::class)->group(function () {
Route::get('/', Dashboard::class)->name('dashboard');
Route::get('/servers', Servers\Index::class)->name('servers.index');
Route::get('/servers/{server}', Servers\Show::class)->name('servers.show');
Route::get('/services', Services\Index::class)->name('services.index');
Route::get('/files', Files\Index::class)->name('files.index');
Route::get('/audit', Audit\Index::class)->name('audit.index');
Route::get('/settings', Settings\Index::class)->name('settings');
Route::get('/versions', Versions\Index::class)->name('versions');
Route::get('/system', System\Index::class)->name('system');
});
});