2026-05-08 09:37:16 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
|
|
|
2026-05-09 19:18:38 +03:00
|
|
|
|
// 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 не действует — синтаксис стабилен под форматтером.
|
|
|
|
|
|
//
|
2026-05-08 19:41:35 +03:00
|
|
|
|
// 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 () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::post('/login', 'App\Http\Controllers\Api\AuthController@login');
|
|
|
|
|
|
Route::post('/register', 'App\Http\Controllers\Api\AuthController@register');
|
2026-05-08 20:14:33 +03:00
|
|
|
|
// /2fa/verify публичный — у user'а ещё нет полноценной session-auth, только
|
|
|
|
|
|
// pending_user_id в session. Verify завершает login после проверки TOTP.
|
2026-05-09 20:14:33 +03:00
|
|
|
|
//
|
|
|
|
|
|
// 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');
|
2026-05-09 03:43:58 +03:00
|
|
|
|
// /2fa/recovery-use — публичный (нет полноценной session-auth до verify).
|
2026-05-09 20:14:33 +03:00
|
|
|
|
Route::post('/2fa/recovery-use', 'App\Http\Controllers\Api\TwoFactorController@useRecoveryCode');
|
2026-05-08 21:10:28 +03:00
|
|
|
|
// /forgot — публичный (anti-enumeration unified-ответ + rate-limit).
|
2026-05-09 20:14:33 +03:00
|
|
|
|
Route::post('/forgot', 'App\Http\Controllers\Api\PasswordResetController@forgotPassword');
|
2026-05-09 03:36:27 +03:00
|
|
|
|
// /reset-password — публичный (deep-link из email с token+email+password).
|
2026-05-09 20:14:33 +03:00
|
|
|
|
Route::post('/reset-password', 'App\Http\Controllers\Api\PasswordResetController@resetPassword');
|
2026-05-08 19:41:35 +03:00
|
|
|
|
Route::middleware('auth:sanctum')->group(function () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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');
|
2026-05-08 19:41:35 +03:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 11:27:57 +03:00
|
|
|
|
// In-app уведомления (P0 этап 2b). Все endpoint'ы под Sanctum SPA auth —
|
|
|
|
|
|
// уведомления USER-personal, читать/писать может только сам user.
|
2026-05-09 18:22:30 +03:00
|
|
|
|
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/notifications')->group(function () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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]+');
|
2026-05-09 11:27:57 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 12:30:38 +03:00
|
|
|
|
// Reminders (P0 этап 4) — schema v8.10 §17.5. Auth обязательный.
|
2026-05-09 18:22:30 +03:00
|
|
|
|
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/reminders')->group(function () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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]+');
|
2026-05-09 12:30:38 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 13:44:09 +03:00
|
|
|
|
// Reports backend. Schema §13.5 report_jobs. Auth обязательный.
|
|
|
|
|
|
// Этапы 1+2 (CRUD + provider/formatter) + этап 3 (retry/cancel/delete +
|
|
|
|
|
|
// retention cron `reports:cleanup-expired`).
|
2026-05-09 18:22:30 +03:00
|
|
|
|
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/reports/jobs')->group(function () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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]+');
|
2026-05-09 13:34:03 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 04:24:02 +03:00
|
|
|
|
// 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 () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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');
|
2026-05-09 04:24:02 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 14:32:24 +03:00
|
|
|
|
// SaaS-admin → Тенанты: lookup + детали для AdminTenantsView/AdminTenantDetailView.
|
|
|
|
|
|
// Без auth (saas-admin SSO ⏸ Б-1).
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::get('/api/admin/tenants', 'App\Http\Controllers\Api\AdminTenantsController@index');
|
|
|
|
|
|
Route::get('/api/admin/tenants/{subdomain}', 'App\Http\Controllers\Api\AdminTenantsController@show')
|
2026-05-09 14:32:24 +03:00
|
|
|
|
->where('subdomain', '[a-z0-9_-]+');
|
2026-05-09 09:19:53 +03:00
|
|
|
|
|
2026-05-09 09:28:49 +03:00
|
|
|
|
// SaaS-admin → Биллинг: aggregates пополнений/списаний за текущий месяц.
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::get('/api/admin/billing', 'App\Http\Controllers\Api\AdminBillingController@index');
|
2026-05-09 09:28:49 +03:00
|
|
|
|
|
2026-05-09 09:38:34 +03:00
|
|
|
|
// SaaS-admin → Инциденты: чтение incidents_log для AdminIncidentsView.
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::get('/api/admin/incidents', 'App\Http\Controllers\Api\AdminIncidentsController@index');
|
2026-05-09 09:38:34 +03:00
|
|
|
|
|
2026-05-09 05:33:21 +03:00
|
|
|
|
// 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 () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::get('/', 'App\Http\Controllers\Api\AdminSystemSettingsController@index');
|
|
|
|
|
|
Route::put('/{key}', 'App\Http\Controllers\Api\AdminSystemSettingsController@update')->where('key', '[a-z0-9_\.]+');
|
2026-05-09 05:33:21 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-11 11:18:01 +03:00
|
|
|
|
// Plan 4: SaaS-admin pricing-tiers editor.
|
|
|
|
|
|
// CRUD для 7-ступенчатого тарифа. effective_from auto-computed = 1-е число
|
|
|
|
|
|
// следующего месяца (МСК). Audit-trail в saas_admin_audit_log.
|
|
|
|
|
|
Route::prefix('/api/admin/pricing-tiers')->group(function () {
|
|
|
|
|
|
Route::get('/', 'App\Http\Controllers\Api\AdminPricingTiersController@index');
|
|
|
|
|
|
Route::post('/', 'App\Http\Controllers\Api\AdminPricingTiersController@store');
|
|
|
|
|
|
Route::delete('/scheduled/{effective_from}',
|
|
|
|
|
|
'App\Http\Controllers\Api\AdminPricingTiersController@deleteScheduled')
|
|
|
|
|
|
->where('effective_from', '\d{4}-\d{2}-\d{2}');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-11 11:28:03 +03:00
|
|
|
|
// Plan 4 Task 10: SaaS-admin supplier prices editor.
|
|
|
|
|
|
// CRUD для B1/B2/B3 закупочных цен. Audit-trail в saas_admin_audit_log.
|
|
|
|
|
|
Route::get('/api/admin/suppliers', 'App\Http\Controllers\Api\AdminSuppliersController@index');
|
|
|
|
|
|
Route::patch('/api/admin/suppliers/{id}', 'App\Http\Controllers\Api\AdminSuppliersController@update')
|
|
|
|
|
|
->where('id', '[0-9]+');
|
|
|
|
|
|
|
2026-05-11 11:51:13 +03:00
|
|
|
|
// Plan 4 Task 11: tenant charges ledger (read-only + CSV export).
|
|
|
|
|
|
// RLS изоляция через SetTenantContext (auth:sanctum + tenant) — текущий tenant
|
|
|
|
|
|
// видит только свои lead_charges. Pagination 20/page, фильтры period/source.
|
|
|
|
|
|
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/billing/charges')->group(function () {
|
|
|
|
|
|
Route::get('/', 'App\Http\Controllers\Api\TenantChargesController@index');
|
|
|
|
|
|
Route::post('/export', 'App\Http\Controllers\Api\TenantChargesController@export');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 06:43:21 +03:00
|
|
|
|
// Сделки — manual create через UI (NewDealDialog). На prod: middleware
|
|
|
|
|
|
// 'auth:sanctum' + 'tenant', tenant_id берётся из user'а. На MVP — параметром.
|
2026-05-09 20:08:20 +03:00
|
|
|
|
//
|
|
|
|
|
|
// Sprint 3 Phase A (audit O-refactor-01): single-resource CRUD остаётся в
|
|
|
|
|
|
// DealController, bulk-операции (transition/destroy/restore) — в
|
|
|
|
|
|
// DealBulkActionController, export — в DealExportController. URL и shape
|
|
|
|
|
|
// payload'ов сохранены, только controller@method обновлён.
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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');
|
2026-05-09 20:08:20 +03:00
|
|
|
|
Route::post('/api/deals/export', 'App\Http\Controllers\Api\DealExportController@export');
|
|
|
|
|
|
Route::post('/api/deals/transition', 'App\Http\Controllers\Api\DealBulkActionController@transition');
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::patch('/api/deals/{id}', 'App\Http\Controllers\Api\DealController@update')->where('id', '[0-9]+');
|
2026-05-09 20:08:20 +03:00
|
|
|
|
Route::delete('/api/deals', 'App\Http\Controllers\Api\DealBulkActionController@destroy');
|
|
|
|
|
|
Route::post('/api/deals/restore', 'App\Http\Controllers\Api\DealBulkActionController@restore');
|
2026-05-09 06:43:21 +03:00
|
|
|
|
|
2026-05-09 06:58:49 +03:00
|
|
|
|
// Lookup endpoints — заполняют v-select'ы (NewDealDialog, smart-filters).
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::get('/api/managers', 'App\Http\Controllers\Api\ManagerController@index');
|
|
|
|
|
|
Route::get('/api/lead-statuses', 'App\Http\Controllers\Api\LeadStatusController@index');
|
2026-05-09 06:58:49 +03:00
|
|
|
|
|
2026-05-11 18:08:01 +03:00
|
|
|
|
// Plan 5 Task 2: Projects CRUD — расширенный API с auth:sanctum + RLS.
|
|
|
|
|
|
// Заменяет старый GET /api/projects?tenant_id={id} (без auth, MVP-версия).
|
|
|
|
|
|
// ⚠️ NewDealDialog использовал старый endpoint (tenant_id param, без auth) —
|
|
|
|
|
|
// после этой замены получит 401. Defer fix до Task 7 (frontend phase).
|
|
|
|
|
|
Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/projects')->group(function () {
|
|
|
|
|
|
Route::get('/', 'App\Http\Controllers\Api\ProjectController@index')->name('projects.index');
|
2026-05-11 18:29:54 +03:00
|
|
|
|
Route::post('/', 'App\Http\Controllers\Api\ProjectController@store')->name('projects.store');
|
2026-05-11 19:06:07 +03:00
|
|
|
|
// /bulk MUST be declared before /{id} parameterized routes so the literal
|
|
|
|
|
|
// segment matches before the regex placeholder is even considered.
|
|
|
|
|
|
Route::post('/bulk', 'App\Http\Controllers\Api\ProjectController@bulk')->name('projects.bulk');
|
2026-05-11 18:08:01 +03:00
|
|
|
|
Route::get('/{id}', 'App\Http\Controllers\Api\ProjectController@show')->name('projects.show')->where('id', '[0-9]+');
|
2026-05-11 19:00:39 +03:00
|
|
|
|
Route::patch('/{id}', 'App\Http\Controllers\Api\ProjectController@update')->name('projects.update')->where('id', '[0-9]+');
|
2026-05-11 19:06:07 +03:00
|
|
|
|
Route::delete('/{id}', 'App\Http\Controllers\Api\ProjectController@destroy')->name('projects.destroy')->where('id', '[0-9]+');
|
|
|
|
|
|
Route::post('/{id}/sync', 'App\Http\Controllers\Api\ProjectController@sync')->name('projects.sync')->where('id', '[0-9]+');
|
|
|
|
|
|
Route::patch('/{id}/toggle-active', 'App\Http\Controllers\Api\ProjectController@toggleActive')->name('projects.toggle')->where('id', '[0-9]+');
|
2026-05-11 18:08:01 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 05:33:21 +03:00
|
|
|
|
// Receive endpoint для входящих webhook'ов (narrative §5.5).
|
|
|
|
|
|
// Auth — по `tenants.webhook_token` в URL (без middleware, проверка внутри controller).
|
|
|
|
|
|
// На prod: + HMAC-валидация X-Webhook-Signature + per-token rate-limit.
|
2026-05-09 19:18:38 +03:00
|
|
|
|
Route::post('/api/webhook/{token}', 'App\Http\Controllers\Api\WebhookReceiveController@receive')
|
2026-05-09 05:33:21 +03:00
|
|
|
|
->where('token', '[A-Za-z0-9\-_]+');
|
|
|
|
|
|
|
2026-05-10 19:39:43 +03:00
|
|
|
|
// 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_\-]+');
|
|
|
|
|
|
|
2026-05-09 04:03:02 +03:00
|
|
|
|
// 2FA setup wizard — все эндпоинты под auth:sanctum (только для уже залогиненных).
|
|
|
|
|
|
Route::prefix('/api/2fa')->middleware('auth:sanctum')->group(function () {
|
2026-05-09 19:18:38 +03:00
|
|
|
|
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');
|
2026-05-09 04:03:02 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 19:41:35 +03:00
|
|
|
|
// SPA-страницы: каждый путь отдаёт Vue-shell (один Blade-template `welcome`).
|
2026-05-08 16:59:00 +03:00
|
|
|
|
// 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');
|
2026-05-09 03:36:27 +03:00
|
|
|
|
Route::view('/reset', 'welcome'); // SPA-router рендерит ResetPasswordView для /reset/{token}
|
2026-05-08 16:59:00 +03:00
|
|
|
|
Route::view('/2fa', 'welcome');
|
|
|
|
|
|
Route::view('/recovery', 'welcome');
|
2026-05-09 03:43:58 +03:00
|
|
|
|
Route::view('/recovery-use', 'welcome');
|
2026-05-08 17:21:19 +03:00
|
|
|
|
Route::view('/dashboard', 'welcome');
|
2026-05-08 17:45:25 +03:00
|
|
|
|
Route::view('/deals', 'welcome');
|
2026-05-08 17:56:59 +03:00
|
|
|
|
Route::view('/kanban', 'welcome');
|
2026-05-12 20:25:19 +03:00
|
|
|
|
Route::view('/projects', 'welcome');
|
2026-05-08 18:39:49 +03:00
|
|
|
|
Route::view('/billing', 'welcome');
|
2026-05-08 18:51:41 +03:00
|
|
|
|
Route::view('/settings', 'welcome');
|
2026-05-08 19:01:10 +03:00
|
|
|
|
Route::view('/reports', 'welcome');
|
2026-05-12 20:25:19 +03:00
|
|
|
|
Route::view('/reminders', 'welcome');
|
|
|
|
|
|
Route::view('/admin', 'welcome');
|
|
|
|
|
|
Route::view('/admin/tenants', 'welcome');
|
|
|
|
|
|
Route::view('/admin/billing', 'welcome');
|
|
|
|
|
|
Route::view('/admin/incidents', 'welcome');
|
|
|
|
|
|
Route::view('/admin/system', 'welcome');
|
|
|
|
|
|
Route::view('/admin/pricing-tiers', 'welcome');
|
|
|
|
|
|
Route::view('/admin/supplier-prices', 'welcome');
|
|
|
|
|
|
Route::view('/admin/impersonation', 'welcome');
|
2026-05-08 19:11:09 +03:00
|
|
|
|
Route::view('/403', 'welcome');
|
|
|
|
|
|
Route::view('/500', 'welcome');
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback для всех неизвестных путей — Vue Router catch-all отрисует 404.
|
|
|
|
|
|
// Срабатывает ПОСЛЕ всех явных route'ов выше и runtime-route'ов от Pest
|
|
|
|
|
|
// beforeEach (они регистрируются в момент теста, до запроса).
|
|
|
|
|
|
Route::fallback(fn () => view('welcome'));
|