Vai al contenuto

Chat Integration

Obiettivo

SafeOps agisce solo come host applicativo e identity/context provider del microservizio chat esterno.

SafeOps: - identifica l'utente loggato - risolve il tenant effettivo - emette il JWT host lato server - passa il contesto business al microservizio chat - monta il widget in punti UI mirati

SafeOps non fa: - persistenza chat locale - websocket chat - unread locali - logica conversazioni

Architettura

Componenti introdotti: - app/blueprints/chat_portal.py - app/services/chat_handoff.py - app/services/chat_context.py

Config: - CHAT_INTEGRATION_ENABLED - CHAT_ENABLED_TENANTS - CHAT_APP_ID - CHAT_SERVICE_BASE_URL - CHAT_WIDGET_BASE_URL - CHAT_HANDOFF_JWT_SECRET - CHAT_HANDOFF_JWT_ALGORITHM - CHAT_HANDOFF_JWT_AUDIENCE - CHAT_HANDOFF_JWT_ISSUER

Flusso

  1. utente autenticato apre una schermata SafeOps abilitata
  2. il frontend server-rendered monta un wrapper minimo del widget
  3. il wrapper chiama POST /api/chat/session/handoff
  4. SafeOps risolve tenant_id = effective_tenant_key()
  5. SafeOps costruisce il JWT host lato server
  6. SafeOps restituisce:
  7. tenant_id
  8. app_id
  9. chat_base_url
  10. widget_base_url
  11. host_jwt
  12. context
  13. il widget riceve bootstrap via postMessage

Contratto backend

Endpoint: - POST /api/chat/session/handoff

Input tipico:

{
  "entity_type": "cliente",
  "entity_id": "123",
  "label": "Cliente ACME",
  "url": "/cliente/show/123",
  "metadata": {
    "module": "anagrafiche"
  }
}

Output:

{
  "ok": true,
  "tenant_id": "tenant-alpha",
  "app_id": "safeops-web",
  "chat_base_url": "http://chat-service/api/v1",
  "widget_base_url": "http://chat-widget",
  "host_jwt": "<jwt>",
  "context": {
    "entity_type": "cliente",
    "entity_id": "123",
    "label": "Cliente ACME",
    "url": "/cliente/show/123",
    "metadata": {
      "module": "anagrafiche",
      "entity_type": "cliente"
    },
    "room_key": "tenant-alpha/safeops-web/cliente/123"
  }
}

Regole multi-tenant

Identità esterna sempre definita da: - tenant_id - app_id - external_user_id

Mapping SafeOps: - tenant_id = effective_tenant_key() - app_id = safeops-web - external_user_id = current_user.id

Regola non negoziabile: - nessun fallback a default - se effective_tenant_key() non risolve un tenant reale, il token chat non viene emesso

Claim JWT host

Claim emessi: - tenant_id - app_id - external_user_id - username - display_name - email - roles - scopes - context opzionale

L'algoritmo di default è HS256.

Mapping contesti

Supporto attuale lato host: - cliente - documento - ticket - intervento - ras - dvr

Regola room: - tenant_id / app_id / entity_type / entity_id

Quindi: - tenant-a / safeops-web / cliente / 123 - tenant-b / safeops-web / cliente / 123

sono due room diverse.

Mapping ruoli -> scopes

Mapping pragmatico lato SafeOps:

Base utente autenticato: - chat:read - chat:write - chat:context:open

Ruoli operativi con allegati: - chat:attach

Tenant admin / platform admin: - chat:moderate - chat:admin

Il microservizio chat resta il punto finale di enforcement.

Feature flags

Flag principale: - CHAT_INTEGRATION_ENABLED

Rollout per tenant: - CHAT_ENABLED_TENANTS

Comportamento: - flag off: endpoint handoff rifiutato - tenant fuori allowlist: endpoint handoff rifiutato

Rollout

Fase 1

  • backend handoff
  • mount pilota solo su Cliente
  • nessuna sostituzione della chat ticket legacy

Fase 2

  • aggiunta Ticket
  • convivenza con chat ticket legacy
  • il widget esterno viene montato come pannello contestuale aggiuntivo nella vista ticket, senza sostituire il thread legacy

Fase 3

  • aggiunta Documento
  • aggiunta Intervento
  • aggiunta RAS
  • aggiunta DVR
  • stato attuale: widget montato su Documento, Intervento, RAS e DVR
  • RAS e DVR usano il mount nella pagina dynamic/compila, con entity_type coerente al doc_kind

UI

Approccio adottato: - server-rendered - wrapper JS minimale - nessun frontend monolitico nuovo

Il frontend: - non conosce il secret - chiama l'endpoint handoff di SafeOps - riceve host_jwt e config dal backend - passa il bootstrap al widget via postMessage

Convivenza con la chat ticket legacy

Legacy che resta attivo: - chat ticket locale in app/modules/tickets/views.py

Nuova integrazione: - esterna - pilotata da feature flag - oggi montata su Cliente, Ticket, Documento, Intervento, RAS e DVR - su Ticket convive esplicitamente con la chat locale esistente

Strategia futura: - mantenere entrambe durante il rollout - spostare progressivamente i punti UI da legacy a microservizio - non toccare la persistenza ticket legacy finché non c'è decisione esplicita di migrazione

Limiti attuali

  • mount UI su Cliente, Ticket, Documento, Intervento, RAS e DVR
  • nessun unread locale
  • nessuna moderazione locale
  • nessuna persistenza messaggi in SafeOps
  • nessun supporto widget avanzato oltre al bootstrap host