group(function () { Route::post('/login', [AuthController::class, 'login']); Route::post('/register', [AuthController::class, 'register']); // /2fa/verify публичный — у user'а ещё нет полноценной session-auth, только // pending_user_id в session. Verify завершает login после проверки TOTP. Route::post('/2fa/verify', [AuthController::class, 'verifyTwoFactor']); // /2fa/recovery-use — публичный (нет полноценной session-auth до verify). Route::post('/2fa/recovery-use', [AuthController::class, 'useRecoveryCode']); // /forgot — публичный (anti-enumeration unified-ответ + rate-limit). Route::post('/forgot', [AuthController::class, 'forgotPassword']); // /reset-password — публичный (deep-link из email с token+email+password). Route::post('/reset-password', [AuthController::class, 'resetPassword']); Route::middleware('auth:sanctum')->group(function () { Route::get('/me', [AuthController::class, 'me']); Route::post('/logout', [AuthController::class, 'logout']); }); }); // In-app уведомления (P0 этап 2b). Все endpoint'ы под Sanctum SPA auth — // уведомления USER-personal, читать/писать может только сам user. Route::middleware('auth:sanctum')->prefix('/api/notifications')->group(function () { Route::get('/', [InAppNotificationController::class, 'index']); Route::patch('/{id}/read', [InAppNotificationController::class, 'markRead'])->where('id', '[0-9]+'); Route::post('/mark-all-read', [InAppNotificationController::class, 'markAllRead']); Route::delete('/{id}', [InAppNotificationController::class, 'destroy'])->where('id', '[0-9]+'); }); // SaaS-admin impersonation flow (Ю-1). На MVP без middleware (saas-admin auth // не реализован), production: middleware('auth:saas-admin') + role('compliance' if needed). Route::prefix('/api/admin/impersonation')->group(function () { Route::get('/active', [ImpersonationController::class, 'active']); Route::get('/recent', [ImpersonationController::class, 'recent']); Route::post('/init', [ImpersonationController::class, 'init']); Route::post('/verify', [ImpersonationController::class, 'verify']); Route::post('/end', [ImpersonationController::class, 'end']); }); // SaaS-admin → Тенанты: lookup для AdminTenantsView. Без auth (saas-admin SSO ⏸ Б-1). Route::get('/api/admin/tenants', [AdminTenantsController::class, 'index']); // SaaS-admin → Биллинг: aggregates пополнений/списаний за текущий месяц. Route::get('/api/admin/billing', [AdminBillingController::class, 'index']); // SaaS-admin → Инциденты: чтение incidents_log для AdminIncidentsView. Route::get('/api/admin/incidents', [AdminIncidentsController::class, 'index']); // SaaS-admin → Система: edit-flow для system_settings + audit-log (4-eyes-pattern). // На MVP без auth-middleware (admin_user_id параметром); production: middleware('auth:saas-admin'). Route::prefix('/api/admin/system-settings')->group(function () { Route::get('/', [AdminSystemSettingsController::class, 'index']); Route::put('/{key}', [AdminSystemSettingsController::class, 'update'])->where('key', '[a-z0-9_\.]+'); }); // Сделки — manual create через UI (NewDealDialog). На prod: middleware // 'auth:sanctum' + 'tenant', tenant_id берётся из user'а. На MVP — параметром. Route::get('/api/deals', [DealController::class, 'index']); Route::get('/api/deals/{id}', [DealController::class, 'show'])->where('id', '[0-9]+'); Route::post('/api/deals', [DealController::class, 'store']); Route::post('/api/deals/export', [DealController::class, 'export']); Route::post('/api/deals/transition', [DealController::class, 'transition']); Route::patch('/api/deals/{id}', [DealController::class, 'update'])->where('id', '[0-9]+'); Route::delete('/api/deals', [DealController::class, 'destroy']); Route::post('/api/deals/restore', [DealController::class, 'restore']); // Lookup endpoints — заполняют v-select'ы (NewDealDialog, smart-filters). Route::get('/api/managers', [ManagerController::class, 'index']); Route::get('/api/projects', [ProjectController::class, 'index']); Route::get('/api/lead-statuses', [LeadStatusController::class, 'index']); // Receive endpoint для входящих webhook'ов (narrative §5.5). // Auth — по `tenants.webhook_token` в URL (без middleware, проверка внутри controller). // На prod: + HMAC-валидация X-Webhook-Signature + per-token rate-limit. Route::post('/api/webhook/{token}', [WebhookReceiveController::class, 'receive']) ->where('token', '[A-Za-z0-9\-_]+'); // 2FA setup wizard — все эндпоинты под auth:sanctum (только для уже залогиненных). Route::prefix('/api/2fa')->middleware('auth:sanctum')->group(function () { Route::post('/init', [TwoFactorSetupController::class, 'init']); Route::post('/confirm', [TwoFactorSetupController::class, 'confirm']); Route::post('/disable', [TwoFactorSetupController::class, 'disable']); Route::post('/regenerate-recovery-codes', [TwoFactorSetupController::class, 'regenerateRecoveryCodes']); }); // SPA-страницы: каждый путь отдаёт Vue-shell (один Blade-template `welcome`). // Vue Router (createWebHistory) разруливает /login, /register и т.п. на фронте. // // Регистрируем явно, а не catch-all `/{any?}` — иначе тесты, регистрирующие // runtime routes через `Route::get(...)` в beforeEach (например _test/...), // будут перехвачены catch-all'ом и вернут welcome view вместо expected response. Route::view('/', 'welcome'); Route::view('/login', 'welcome'); Route::view('/register', 'welcome'); Route::view('/forgot', 'welcome'); Route::view('/reset', 'welcome'); // SPA-router рендерит ResetPasswordView для /reset/{token} Route::view('/2fa', 'welcome'); Route::view('/recovery', 'welcome'); Route::view('/recovery-use', 'welcome'); Route::view('/dashboard', 'welcome'); Route::view('/deals', 'welcome'); Route::view('/kanban', 'welcome'); Route::view('/billing', 'welcome'); Route::view('/settings', 'welcome'); Route::view('/reports', 'welcome'); Route::view('/403', 'welcome'); Route::view('/500', 'welcome'); // Fallback для всех неизвестных путей — Vue Router catch-all отрисует 404. // Срабатывает ПОСЛЕ всех явных route'ов выше и runtime-route'ов от Pest // beforeEach (они регистрируются в момент теста, до запроса). Route::fallback(fn () => view('welcome'));