Files
portal/app/routes/web.php
T
Дмитрий 280cfcd6cf feat(routes): register POST /api/webhook/supplier/{secret}
Spec §5.1 supplier-webhook endpoint. SupplierWebhookController tests
переходят с 405 на 8/8 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:39:43 +03:00

171 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use Illuminate\Support\Facades\Route;
// Laravel 13 string-based lazy-loading контроллеров (Sprint 2 Phase A, O-stack-03).
//
// Контроллеры передаются как строки 'FQN@method' вместо
// [Class::class, 'method']. При таком синтаксисе Laravel НЕ автозагружает
// класс при boot'е route'ов — резолвит из строки только при матче маршрута.
// Экономит ~50-100 ms на cold-boot для middleware-пайплайна с 20+ контроллерами.
//
// Почему именно строки, а не FQN-class:
// - Pint default preset (Laravel) применяет fixer fully_qualified_strict_types,
// который сворачивает \App\Http\...\X::class обратно в use-импорт.
// На строки этот fixer не действует — синтаксис стабилен под форматтером.
//
// SPA-API маршруты auth-flow. Размещены в web.php (а не api.php), потому
// что Sanctum SPA mode использует session-cookie auth, а session middleware
// добавляется только к web-группе. См. laravel.com/docs/sanctum#spa-authentication.
Route::prefix('/api/auth')->group(function () {
Route::post('/login', 'App\Http\Controllers\Api\AuthController@login');
Route::post('/register', 'App\Http\Controllers\Api\AuthController@register');
// /2fa/verify публичный — у user'а ещё нет полноценной session-auth, только
// pending_user_id в session. Verify завершает login после проверки TOTP.
//
// Sprint 3 Phase B (audit O-refactor-02): 2FA verify/recovery-use вынесены
// из AuthController в TwoFactorController; forgot/reset — в PasswordResetController.
// URL без изменений.
Route::post('/2fa/verify', 'App\Http\Controllers\Api\TwoFactorController@verifyTwoFactor');
// /2fa/recovery-use — публичный (нет полноценной session-auth до verify).
Route::post('/2fa/recovery-use', 'App\Http\Controllers\Api\TwoFactorController@useRecoveryCode');
// /forgot — публичный (anti-enumeration unified-ответ + rate-limit).
Route::post('/forgot', 'App\Http\Controllers\Api\PasswordResetController@forgotPassword');
// /reset-password — публичный (deep-link из email с token+email+password).
Route::post('/reset-password', 'App\Http\Controllers\Api\PasswordResetController@resetPassword');
Route::middleware('auth:sanctum')->group(function () {
Route::get('/me', 'App\Http\Controllers\Api\AuthController@me');
Route::post('/logout', 'App\Http\Controllers\Api\AuthController@logout');
Route::patch('/me/notification-preferences', 'App\Http\Controllers\Api\AuthController@updateNotificationPreferences');
});
});
// In-app уведомления (P0 этап 2b). Все endpoint'ы под Sanctum SPA auth —
// уведомления USER-personal, читать/писать может только сам user.
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/notifications')->group(function () {
Route::get('/', 'App\Http\Controllers\Api\InAppNotificationController@index');
Route::patch('/{id}/read', 'App\Http\Controllers\Api\InAppNotificationController@markRead')->where('id', '[0-9]+');
Route::post('/mark-all-read', 'App\Http\Controllers\Api\InAppNotificationController@markAllRead');
Route::delete('/{id}', 'App\Http\Controllers\Api\InAppNotificationController@destroy')->where('id', '[0-9]+');
});
// Reminders (P0 этап 4) — schema v8.10 §17.5. Auth обязательный.
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/reminders')->group(function () {
Route::get('/', 'App\Http\Controllers\Api\ReminderController@index');
Route::post('/', 'App\Http\Controllers\Api\ReminderController@store');
Route::patch('/{id}', 'App\Http\Controllers\Api\ReminderController@update')->where('id', '[0-9]+');
Route::post('/{id}/complete', 'App\Http\Controllers\Api\ReminderController@complete')->where('id', '[0-9]+');
Route::delete('/{id}', 'App\Http\Controllers\Api\ReminderController@destroy')->where('id', '[0-9]+');
});
// Reports backend. Schema §13.5 report_jobs. Auth обязательный.
// Этапы 1+2 (CRUD + provider/formatter) + этап 3 (retry/cancel/delete +
// retention cron `reports:cleanup-expired`).
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/reports/jobs')->group(function () {
Route::get('/', 'App\Http\Controllers\Api\ReportJobController@index');
Route::post('/', 'App\Http\Controllers\Api\ReportJobController@store');
Route::get('/{id}', 'App\Http\Controllers\Api\ReportJobController@show')->where('id', '[0-9]+');
Route::post('/{id}/retry', 'App\Http\Controllers\Api\ReportJobController@retry')->where('id', '[0-9]+');
Route::post('/{id}/cancel', 'App\Http\Controllers\Api\ReportJobController@cancel')->where('id', '[0-9]+');
Route::delete('/{id}', 'App\Http\Controllers\Api\ReportJobController@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', 'App\Http\Controllers\Api\ImpersonationController@active');
Route::get('/recent', 'App\Http\Controllers\Api\ImpersonationController@recent');
Route::post('/init', 'App\Http\Controllers\Api\ImpersonationController@init');
Route::post('/verify', 'App\Http\Controllers\Api\ImpersonationController@verify');
Route::post('/end', 'App\Http\Controllers\Api\ImpersonationController@end');
});
// SaaS-admin → Тенанты: lookup + детали для AdminTenantsView/AdminTenantDetailView.
// Без auth (saas-admin SSO ⏸ Б-1).
Route::get('/api/admin/tenants', 'App\Http\Controllers\Api\AdminTenantsController@index');
Route::get('/api/admin/tenants/{subdomain}', 'App\Http\Controllers\Api\AdminTenantsController@show')
->where('subdomain', '[a-z0-9_-]+');
// SaaS-admin → Биллинг: aggregates пополнений/списаний за текущий месяц.
Route::get('/api/admin/billing', 'App\Http\Controllers\Api\AdminBillingController@index');
// SaaS-admin → Инциденты: чтение incidents_log для AdminIncidentsView.
Route::get('/api/admin/incidents', 'App\Http\Controllers\Api\AdminIncidentsController@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('/', 'App\Http\Controllers\Api\AdminSystemSettingsController@index');
Route::put('/{key}', 'App\Http\Controllers\Api\AdminSystemSettingsController@update')->where('key', '[a-z0-9_\.]+');
});
// Сделки — manual create через UI (NewDealDialog). На prod: middleware
// 'auth:sanctum' + 'tenant', tenant_id берётся из user'а. На MVP — параметром.
//
// Sprint 3 Phase A (audit O-refactor-01): single-resource CRUD остаётся в
// DealController, bulk-операции (transition/destroy/restore) — в
// DealBulkActionController, export — в DealExportController. URL и shape
// payload'ов сохранены, только controller@method обновлён.
Route::get('/api/deals', 'App\Http\Controllers\Api\DealController@index');
Route::get('/api/deals/{id}', 'App\Http\Controllers\Api\DealController@show')->where('id', '[0-9]+');
Route::post('/api/deals', 'App\Http\Controllers\Api\DealController@store');
Route::post('/api/deals/export', 'App\Http\Controllers\Api\DealExportController@export');
Route::post('/api/deals/transition', 'App\Http\Controllers\Api\DealBulkActionController@transition');
Route::patch('/api/deals/{id}', 'App\Http\Controllers\Api\DealController@update')->where('id', '[0-9]+');
Route::delete('/api/deals', 'App\Http\Controllers\Api\DealBulkActionController@destroy');
Route::post('/api/deals/restore', 'App\Http\Controllers\Api\DealBulkActionController@restore');
// Lookup endpoints — заполняют v-select'ы (NewDealDialog, smart-filters).
Route::get('/api/managers', 'App\Http\Controllers\Api\ManagerController@index');
Route::get('/api/projects', 'App\Http\Controllers\Api\ProjectController@index');
Route::get('/api/lead-statuses', 'App\Http\Controllers\Api\LeadStatusController@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}', 'App\Http\Controllers\Api\WebhookReceiveController@receive')
->where('token', '[A-Za-z0-9\-_]+');
// Supplier-integration webhook (Plan 2/5, spec §5.1).
// Platform-wide endpoint: единый {secret} в URL для всех лидов от crm.bp-gr.ru.
// Auth: secret (system_settings.supplier_webhook_secret) + IP allowlist
// (system_settings.supplier_ip_allowlist). Не пересекается с legacy /api/webhook/{token}.
Route::post('/api/webhook/supplier/{secret}', 'App\Http\Controllers\Api\SupplierWebhookController@receive')
->where('secret', '[A-Za-z0-9_\-]+');
// 2FA setup wizard — все эндпоинты под auth:sanctum (только для уже залогиненных).
Route::prefix('/api/2fa')->middleware('auth:sanctum')->group(function () {
Route::post('/init', 'App\Http\Controllers\Api\TwoFactorSetupController@init');
Route::post('/confirm', 'App\Http\Controllers\Api\TwoFactorSetupController@confirm');
Route::post('/disable', 'App\Http\Controllers\Api\TwoFactorSetupController@disable');
Route::post('/regenerate-recovery-codes', 'App\Http\Controllers\Api\TwoFactorSetupController@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'));