$alternate) { Route::domain($alternate) ->middleware(['admin.host', 'auth', 'admin']) ->prefix(AdminArea::prefix()) ->name("admin.via{$index}.") ->group(base_path('routes/admin.php')); } Route::domain($canonical) ->middleware(['admin.host', 'auth', 'admin']) ->prefix(AdminArea::prefix()) ->name('admin.') ->group(base_path('routes/admin.php')); } else { // Shared with the portal: no hostname binding, and the /admin prefix keeps // the two apart by path. Route::middleware(['admin.host', 'auth', 'admin']) ->prefix(AdminArea::prefix()) ->name('admin.') ->group(base_path('routes/admin.php')); } // Old console addresses, once the console has moved off /admin. Permanent, // because it really has moved, and only on the console's own host so this // never hands a stranger a redirect that confirms a console exists. if (AdminArea::isExclusive()) { Route::domain(AdminArea::host()) ->get('/admin/{rest?}', fn (?string $rest = null) => redirect('/'.($rest ?? ''), 301)) ->where('rest', '.*') ->name('admin.legacy'); } // The service status page. Its own address: it used to sit under /legal beside // the imprint and the terms, and nothing about the current health of the // platform is a legal document. // // Bound to its own hostname when one is configured, and every other host // redirects there rather than 404ing — a status page is the one address people // keep in a bookmark and reach for when something is already wrong. $statusHost = (string) config('admin_access.status_host'); if ($statusHost !== '') { // At the ROOT of its own hostname. "status.clupilot.com/status" says the // same word twice and reads like a mistake, because it is one. // // Registered BEFORE the landing page below: `/` exists on both, Laravel // takes the first match, and a host-agnostic route registered earlier wins // over a domain-bound one registered later. Route::domain($statusHost)->get('/', StatusController::class)->name('status'); // The old path on that host keeps working rather than 404ing. Route::domain($statusHost)->get('/status', fn () => redirect()->to('/', 301)); Route::get('/status', fn () => redirect()->away('https://'.$statusHost, 301))->name('status.elsewhere'); } else { Route::get('/status', StatusController::class)->name('status'); } Route::get('/', LandingController::class)->name('home'); // Generated, not a static file: while the site is hidden this has to say so, // and a crawler that gets a 404 here simply crawls anyway. Route::get('/robots.txt', function () { $body = App\Support\Settings::bool('site.public', true) ? "User-agent: *\nAllow: /\n" : "User-agent: *\nDisallow: /\n"; return response($body, 200, ['Content-Type' => 'text/plain']); })->name('robots'); // Stripe webhook — paid order → customer provisioning run (CSRF-exempt, signed). Route::post('/webhooks/stripe', StripeWebhookController::class)->name('webhooks.stripe'); // Public legal pages (placeholders — replace with real content before launch). Route::prefix('legal')->name('legal.')->group(function () { Route::get('/impressum', fn () => view('legal', ['title' => 'Impressum']))->name('impressum'); Route::get('/datenschutz', fn () => view('legal', ['title' => 'Datenschutz']))->name('datenschutz'); Route::get('/agb', fn () => view('legal', ['title' => 'AGB']))->name('agb'); // Kept so existing links and bookmarks do not 404 — permanently, because // the page has genuinely moved. Through route() rather than a literal // path: once the page has its own hostname, a relative redirect would land // on the host the visitor is already on, where it no longer answers. Route::get('/status', fn () => redirect()->to(route('status'), 301))->name('status'); }); // Guest auth pages — full-page class-based Livewire components (R1/R2). Fortify // handles the POST actions (login.store, two-factor.login.store) with views off. Route::middleware('guest')->group(function () { Route::get('/login', Login::class)->name('login'); Route::get('/register', \App\Livewire\Auth\Register::class)->name('register'); // Registration POST goes through Fortify's controller but with our own // registration-scoped throttle (Fortify's built-in route has no limiter). Route::post('/register', [\Laravel\Fortify\Http\Controllers\RegisteredUserController::class, 'store']) ->middleware('throttle:registration') ->name('register.store'); Route::get('/two-factor-challenge', TwoFactorChallenge::class)->name('two-factor.login'); }); // Customer portal — each sidebar tab is a full-page class-based Livewire // component (R1/R2); paths are English (R13). Route::middleware(['auth', 'customer.active'])->group(function () { Route::get('/dashboard', Dashboard::class)->name('dashboard'); Route::get('/cloud', Cloud::class)->name('cloud'); Route::get('/users', Users::class)->name('users'); Route::get('/backups', Backups::class)->name('backups'); Route::get('/invoices', Invoices::class)->name('invoices'); Route::get('/billing', Billing::class)->name('billing'); Route::get('/settings', \App\Livewire\Settings::class)->name('settings'); Route::get('/support', Support::class)->name('support'); // Return from an admin impersonation session (accessible as the customer user). // POST so Laravel's CSRF middleware protects the identity change. Route::post('/impersonate/leave', [ImpersonationController::class, 'leave'])->name('impersonate.leave'); });