fase 5
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
|
||||
/**
|
||||
* Hook helper per accedere all'utente corrente e ai permessi.
|
||||
*/
|
||||
export function useAuth() {
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
||||
const isLoading = useAuthStore((s) => s.isLoading)
|
||||
const logout = useAuthStore((s) => s.logout)
|
||||
|
||||
const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'
|
||||
const isSuperAdmin = user?.role === 'super_admin'
|
||||
const canSend = user?.role !== 'readonly'
|
||||
const canManage = isAdmin
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
isAdmin,
|
||||
isSuperAdmin,
|
||||
canSend,
|
||||
canManage,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Hook useWebSocket – connessione WebSocket al backend PecFlow.
|
||||
*
|
||||
* Il backend usa FastAPI WebSocket nativo (non Socket.io).
|
||||
* Endpoint: /api/v1/ws/{tenant_id}?token=<access_token>
|
||||
*
|
||||
* Gestisce:
|
||||
* - Connessione automatica all'autenticazione
|
||||
* - Riconnessione con backoff esponenziale
|
||||
* - Dispatch degli eventi all'inbox/mailbox store
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
import { useInboxStore } from '@/store/inbox.store'
|
||||
import { useMailboxStore } from '@/store/mailbox.store'
|
||||
import type { MessageResponse, WsEvent } from '@/types/api.types'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
const MAX_RECONNECT_DELAY = 30000
|
||||
const BASE_RECONNECT_DELAY = 1000
|
||||
|
||||
export function useWebSocket() {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const reconnectAttemptsRef = useRef(0)
|
||||
const isUnmountedRef = useRef(false)
|
||||
|
||||
const { user, isAuthenticated } = useAuthStore()
|
||||
const prependMessage = useInboxStore((s) => s.prependMessage)
|
||||
const updateMailboxStatus = useMailboxStore((s) => s.updateMailboxStatus)
|
||||
|
||||
const handleEvent = useCallback(
|
||||
(event: WsEvent) => {
|
||||
switch (event.type) {
|
||||
case 'mailbox:new_message': {
|
||||
const message = event.payload as unknown as MessageResponse
|
||||
prependMessage(message)
|
||||
if (!message.is_read) {
|
||||
const from = message.from_address || 'Mittente sconosciuto'
|
||||
const subject = message.subject || '(nessun oggetto)'
|
||||
toast.success(`📨 Nuova PEC da ${from}: ${subject}`, {
|
||||
duration: 5000,
|
||||
id: `new-msg-${message.id}`,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'mailbox:status_changed': {
|
||||
const { mailbox_id, status, error_msg } = event.payload as {
|
||||
mailbox_id: string
|
||||
status: string
|
||||
error_msg?: string
|
||||
}
|
||||
updateMailboxStatus(mailbox_id, status, error_msg)
|
||||
if (status === 'error') {
|
||||
toast.error(`⚠️ Errore sincronizzazione casella`, { duration: 8000 })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'send_job:status_changed': {
|
||||
const { job_id: _jobId, status, mailbox_id: _mid } = event.payload as {
|
||||
job_id: string
|
||||
status: string
|
||||
mailbox_id: string
|
||||
}
|
||||
if (status === 'sent') {
|
||||
toast.success('✅ PEC inviata con successo', { duration: 4000 })
|
||||
} else if (status === 'failed') {
|
||||
toast.error('❌ Invio PEC fallito definitivamente', { duration: 8000 })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'send_job:anomaly': {
|
||||
toast.error(
|
||||
'⚠️ Anomalia invio PEC: nessuna ricevuta di accettazione entro 24h',
|
||||
{ duration: 10000 },
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[prependMessage, updateMailboxStatus],
|
||||
)
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!isAuthenticated || !user) return
|
||||
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (!token) return
|
||||
|
||||
// Costruisce l'URL WebSocket
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
const wsUrl = `${protocol}//${host}/api/v1/ws/${user.tenant_id}?token=${token}`
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(wsUrl)
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WS] Connessione stabilita')
|
||||
reconnectAttemptsRef.current = 0
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as WsEvent
|
||||
handleEvent(data)
|
||||
} catch {
|
||||
console.warn('[WS] Messaggio non valido:', event.data)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WS] Errore:', error)
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('[WS] Connessione chiusa', event.code, event.reason)
|
||||
wsRef.current = null
|
||||
|
||||
if (isUnmountedRef.current || !isAuthenticated) return
|
||||
|
||||
// Backoff esponenziale per la riconnessione
|
||||
const delay = Math.min(
|
||||
BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttemptsRef.current),
|
||||
MAX_RECONNECT_DELAY,
|
||||
)
|
||||
reconnectAttemptsRef.current += 1
|
||||
|
||||
console.log(`[WS] Riconnessione in ${delay}ms (tentativo ${reconnectAttemptsRef.current})`)
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
if (!isUnmountedRef.current) connect()
|
||||
}, delay)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WS] Errore durante la connessione:', error)
|
||||
}
|
||||
}, [isAuthenticated, user, handleEvent])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close(1000, 'Logout')
|
||||
wsRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
isUnmountedRef.current = false
|
||||
|
||||
if (isAuthenticated && user) {
|
||||
connect()
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
|
||||
return () => {
|
||||
isUnmountedRef.current = true
|
||||
disconnect()
|
||||
}
|
||||
}, [isAuthenticated, user?.id, connect, disconnect])
|
||||
|
||||
return { isConnected: wsRef.current?.readyState === WebSocket.OPEN }
|
||||
}
|
||||
Reference in New Issue
Block a user