[FEAT] Fase 3 backend: portale fornitore OTP/magic-link (SupplierPortalController)

Auth fornitore SEPARATA dagli utenti interni (supplier_users/otp/sessions, mig 034):
- SUPPLIER_JWT_SECRET dedicato, aud=supplier-portal, claim sp_uid/supplier_id/org_id
  (mai user_id); requireSupplierSession() verifica jti in supplier_sessions
  (revocabile), non tocca users/active_sessions.
- OTP 8 cifre SHA-256, 15min, lockout persistente (attempts+locked_until),
  invalidazione OTP precedenti, hash_equals, rate-limit email+IP.
- magic-link 32B hashed single-use (consumo atomico solo su verify).
- request-otp risposta opaca anti-enumerazione.
- OTP via EmailService::sendViaTemplate (/api/emails/send, fuori da email_log).
- Endpoint: requestOtp/verifyOtp (no auth) + me/getQuestionnaire/saveAnswers
  (PATCH autosave)/submitQuestionnaire. Ownership campaign.supplier_id==session (no IDOR).
- Scoring per-vulnerabilita (Art.21.3), snapshot domande immutabile.
- config: SUPPLIER_JWT_SECRET + PATCH in CORS_ALLOWED_METHODS.
- routes: controllerMap + actionMap supplier-portal.

php -l OK su tutti. Tabelle 034 gia' applicate su host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-05-31 17:40:20 +02:00
co-authored by Claude Opus 4.8
parent 261fc4cdd5
commit de09af6d7e
2 changed files with 719 additions and 0 deletions
+13
View File
@@ -110,6 +110,7 @@ $controllerMap = [
'knowledgebase' => 'KnowledgeBaseController', // KB multi-livello (Migration 012-014)
'ai' => 'AiController', // Assistente AI ARIA (askWithRag + fonti certe)
'branding' => 'BrandingController', // White-label firm (Fase 5 / G16)
'supplier-portal' => 'SupplierPortalController', // Portale fornitore Fase 3 (auth OTP/magic-link separata)
];
if (!isset($controllerMap[$controllerName])) {
@@ -301,6 +302,18 @@ $actionMap = [
'DELETE:{id}' => 'delete',
],
// ── SupplierPortalController — portale fornitore (auth OTP/magic-link, SEPARATA da users) ──
'supplier-portal' => [
'POST:requestOtp' => 'requestOtp', // NO AUTH (opaco)
'POST:request-otp' => 'requestOtp',
'POST:verifyOtp' => 'verifyOtp', // NO AUTH (apre sessione)
'POST:verify-otp' => 'verifyOtp',
'GET:me' => 'me', // sessione fornitore
'GET:{id}' => 'getQuestionnaire',
'POST:{id}/submit' => 'submitQuestionnaire',
'PATCH:{id}/answers' => 'saveAnswers', // autosave
],
// ── TrainingController ──────────────────────────
'training' => [
'GET:courses' => 'listCourses',