126 lines
5.7 KiB
PHP
126 lines
5.7 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\ImpersonationController;
|
|
use App\Http\Controllers\LandingController;
|
|
use App\Http\Controllers\StatusController;
|
|
use App\Http\Controllers\StripeWebhookController;
|
|
use App\Livewire\Admin;
|
|
use App\Livewire\Billing;
|
|
use App\Livewire\Auth\Login;
|
|
use App\Livewire\Auth\TwoFactorChallenge;
|
|
use App\Livewire\Backups;
|
|
use App\Livewire\Cloud;
|
|
use App\Livewire\Dashboard;
|
|
use App\Livewire\Invoices;
|
|
use App\Livewire\Support;
|
|
use App\Livewire\Users;
|
|
use App\Support\AdminArea;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
/*
|
|
| The operator console, registered BEFORE anything else in this file.
|
|
|
|
|
| Once the console has a hostname to itself it sits at the ROOT of that host —
|
|
| where `/`, `/settings` and `/customers` also exist further down for the
|
|
| public site and the customer portal. Laravel takes the first matching route,
|
|
| so the order of these two blocks is the separation.
|
|
|
|
|
| Where it mounts is AdminArea's call: `/` on the console hostname where it has
|
|
| one to itself, `/admin` on any host otherwise. The names stay `admin.*` in
|
|
| both modes, so nothing that generates a URL has to know which mode it is in.
|
|
*/
|
|
if (AdminArea::isExclusive()) {
|
|
// Once per accepted hostname, and the canonical one LAST.
|
|
//
|
|
// ADMIN_HOSTS may list alternates — a bare IP, a second name — and those
|
|
// are the recovery paths someone locked out reaches for. Binding only the
|
|
// first would make the console 404 through every one of them. Registering
|
|
// the canonical host last means route() generates URLs for it, while all of
|
|
// them still match.
|
|
$hosts = AdminArea::hosts();
|
|
foreach (array_reverse($hosts) as $host) {
|
|
Route::domain($host)
|
|
->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');
|
|
}
|
|
|
|
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');
|
|
|
|
// 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.
|
|
Route::get('/status', StatusController::class)->name('status');
|
|
|
|
// 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.
|
|
Route::permanentRedirect('/status', '/status')->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');
|
|
});
|