diff --git a/src/components/portal/machine-deactivation-overlay.tsx b/src/components/portal/machine-deactivation-overlay.tsx new file mode 100644 index 0000000..5bb6d12 --- /dev/null +++ b/src/components/portal/machine-deactivation-overlay.tsx @@ -0,0 +1,73 @@ +"use client" + +import { ShieldAlert, Mail, RefreshCw } from "lucide-react" +import { Button } from "@/components/ui/button" + +type MachineDeactivationOverlayProps = { + companyName?: string | null + onRetry?: () => void + isRetrying?: boolean +} + +/** + * Overlay de bloqueio exibido quando a máquina é desativada. + * Cobre toda a tela impedindo qualquer interação até que a máquina seja reativada. + */ +export function MachineDeactivationOverlay({ + companyName, + onRetry, + isRetrying, +}: MachineDeactivationOverlayProps) { + return ( +
+
+
+ + Acesso bloqueado + +

Dispositivo desativado

+

+ Este dispositivo foi desativado temporariamente pelos administradores. O acesso ao portal + e o envio de informações ficam indisponíveis até a reativação. +

+ {companyName ? ( + + {companyName} + + ) : null} +
+ +
+
+

Como regularizar

+
    +
  • Entre em contato com o suporte e solicite a reativação.
  • +
  • Informe o nome do computador e seus dados de contato.
  • +
+
+ +
+ + Falar com o suporte + + {onRetry ? ( + + ) : null} +
+
+
+
+ ) +} diff --git a/src/components/portal/portal-shell.tsx b/src/components/portal/portal-shell.tsx index 17abd38..6712fc6 100644 --- a/src/components/portal/portal-shell.tsx +++ b/src/components/portal/portal-shell.tsx @@ -1,6 +1,6 @@ "use client" -import { type ReactNode, useMemo, useState } from "react" +import { type ReactNode, useMemo, useState, useCallback } from "react" import Image from "next/image" import Link from "next/link" import { usePathname, useRouter } from "next/navigation" @@ -12,6 +12,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" import { useAuth, signOut } from "@/lib/auth-client" +import { useMachineStateMonitor } from "@/hooks/use-machine-state-monitor" +import { MachineDeactivationOverlay } from "./machine-deactivation-overlay" interface PortalShellProps { children: ReactNode @@ -25,10 +27,50 @@ const navItems = [ export function PortalShell({ children }: PortalShellProps) { const pathname = usePathname() const router = useRouter() - const { session, machineContext, machineContextError, machineContextLoading } = useAuth() + const { session, machineContext, machineContextError, machineContextLoading, refreshMachineContext } = useAuth() const [isSigningOut, setIsSigningOut] = useState(false) + const [showDeactivationOverlay, setShowDeactivationOverlay] = useState(false) + const [isRetryingActivation, setIsRetryingActivation] = useState(false) const isMachineSession = session?.user.role === "machine" || Boolean(machineContext) + + // Monitor de estado da máquina em tempo real via Convex + const handleMachineDeactivated = useCallback(() => { + console.log("[PortalShell] Máquina foi desativada - exibindo overlay de bloqueio") + setShowDeactivationOverlay(true) + }, []) + + const handleTokenRevoked = useCallback(() => { + console.log("[PortalShell] Token foi revogado - redirecionando para login") + toast.error("Este dispositivo foi resetado. Faça login novamente.") + router.replace("/login") + }, [router]) + + const { isActive: machineIsActive } = useMachineStateMonitor({ + machineId: machineContext?.machineId, + onDeactivated: handleMachineDeactivated, + onTokenRevoked: handleTokenRevoked, + enabled: isMachineSession && !!machineContext?.machineId, + }) + + // Verifica também o estado vindo do contexto (polling) como fallback + const effectivelyDeactivated = showDeactivationOverlay || machineContext?.isActive === false || !machineIsActive + + // Função para verificar novamente o estado + const handleRetryActivation = useCallback(async () => { + setIsRetryingActivation(true) + try { + await refreshMachineContext?.() + // Se ainda está ativo após refresh, esconde overlay + if (machineContext?.isActive !== false && machineIsActive) { + setShowDeactivationOverlay(false) + } + } catch (error) { + console.error("Erro ao verificar estado:", error) + } finally { + setIsRetryingActivation(false) + } + }, [refreshMachineContext, machineContext?.isActive, machineIsActive]) const personaValue = machineContext?.persona ?? session?.user.machinePersona ?? null const collaboratorName = machineContext?.assignedUserName?.trim() ?? "" const collaboratorEmail = machineContext?.assignedUserEmail?.trim() ?? "" @@ -74,6 +116,13 @@ export function PortalShell({ children }: PortalShellProps) { return (
+ {/* Overlay de bloqueio quando máquina é desativada */} + {isMachineSession && effectivelyDeactivated && ( + + )}
diff --git a/src/hooks/use-machine-state-monitor.ts b/src/hooks/use-machine-state-monitor.ts new file mode 100644 index 0000000..6a6df48 --- /dev/null +++ b/src/hooks/use-machine-state-monitor.ts @@ -0,0 +1,117 @@ +/** + * Hook para monitorar o estado da máquina em tempo real via Convex subscription. + * + * Usado no portal para detectar instantaneamente quando uma máquina é: + * - Desativada (isActive = false) + * - Resetada (tokens revogados) + * + * Diferente do polling de 15s do AuthProvider, isso é verdadeiramente real-time. + */ + +import { useEffect, useRef, useCallback } from "react" +import { useQuery } from "convex/react" +import { api } from "@/convex/_generated/api" +import type { Id } from "@/convex/_generated/dataModel" + +type UseMachineStateMonitorOptions = { + machineId: string | null | undefined + onDeactivated?: () => void + onTokenRevoked?: () => void + enabled?: boolean +} + +type MachineStateResult = { + isActive: boolean + hasValidToken: boolean + isLoading: boolean + found: boolean +} + +export function useMachineStateMonitor({ + machineId, + onDeactivated, + onTokenRevoked, + enabled = true, +}: UseMachineStateMonitorOptions): MachineStateResult { + // Refs para rastrear estado anterior e evitar chamadas duplicadas + const previousIsActive = useRef(null) + const previousHasValidToken = useRef(null) + const initialLoadDone = useRef(false) + + // Subscription Convex - só ativa se tiver machineId válido + const machineState = useQuery( + api.machines.getMachineState, + enabled && machineId ? { machineId: machineId as Id<"machines"> } : "skip" + ) + + // Callbacks estáveis + const handleDeactivated = useCallback(() => { + onDeactivated?.() + }, [onDeactivated]) + + const handleTokenRevoked = useCallback(() => { + onTokenRevoked?.() + }, [onTokenRevoked]) + + useEffect(() => { + if (!machineState) return + + // Na primeira carga, verifica estado inicial E armazena valores + if (!initialLoadDone.current) { + console.log("[useMachineStateMonitor] Carga inicial", { + isActive: machineState.isActive, + hasValidToken: machineState.hasValidToken, + found: machineState.found, + }) + + // Se já estiver desativado na carga inicial, chama callback + if (machineState.isActive === false) { + console.log("[useMachineStateMonitor] Máquina já estava desativada") + handleDeactivated() + } + + // Se token já estiver inválido na carga inicial, chama callback + if (machineState.hasValidToken === false) { + console.log("[useMachineStateMonitor] Token já estava revogado") + handleTokenRevoked() + } + + previousIsActive.current = machineState.isActive + previousHasValidToken.current = machineState.hasValidToken + initialLoadDone.current = true + return + } + + // Detecta mudança de ativo para inativo + if (previousIsActive.current === true && machineState.isActive === false) { + console.log("[useMachineStateMonitor] Máquina foi desativada") + handleDeactivated() + } + + // Detecta mudança de token válido para inválido + if (previousHasValidToken.current === true && machineState.hasValidToken === false) { + console.log("[useMachineStateMonitor] Token foi revogado (reset)") + handleTokenRevoked() + } + + // Atualiza refs + previousIsActive.current = machineState.isActive + previousHasValidToken.current = machineState.hasValidToken + }, [machineState, handleDeactivated, handleTokenRevoked]) + + // Reset refs quando machineId muda + useEffect(() => { + if (!machineId) { + previousIsActive.current = null + previousHasValidToken.current = null + initialLoadDone.current = false + } + }, [machineId]) + + return { + isActive: machineState?.isActive ?? true, + hasValidToken: machineState?.hasValidToken ?? true, + isLoading: machineState === undefined, + found: machineState?.found ?? false, + } +} diff --git a/src/lib/auth-client.tsx b/src/lib/auth-client.tsx index e661020..33c108a 100644 --- a/src/lib/auth-client.tsx +++ b/src/lib/auth-client.tsx @@ -68,6 +68,7 @@ type AuthContextValue = { machineContext: MachineContext | null machineContextLoading: boolean machineContextError: MachineContextError | null + refreshMachineContext: (() => Promise) | null } const AuthContext = createContext({ @@ -81,6 +82,7 @@ const AuthContext = createContext({ machineContext: null, machineContextLoading: false, machineContextError: null, + refreshMachineContext: null, }) export function useAuth() { @@ -345,6 +347,35 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const effectiveConvexUserId = baseRole === "machine" ? (machineContext?.assignedUserId ?? null) : convexUserId + // Função para forçar atualização do contexto de máquina + const refreshMachineContext = useCallback(async () => { + try { + const response = await fetch("/api/machines/session", { credentials: "include" }) + if (!response.ok) { + console.warn("[refreshMachineContext] Falha ao buscar sessão:", response.status) + return + } + const data = (await response.json()) as { machine?: MachineContext & { id: string } } + const mc = data?.machine + if (mc && typeof mc === "object") { + setMachineContext({ + machineId: mc.id, + tenantId: mc.tenantId, + persona: mc.persona ?? null, + assignedUserId: mc.assignedUserId ?? null, + assignedUserEmail: mc.assignedUserEmail ?? null, + assignedUserName: mc.assignedUserName ?? null, + assignedUserRole: mc.assignedUserRole ?? null, + companyId: mc.companyId ?? null, + isActive: mc.isActive ?? true, + }) + setMachineContextError(null) + } + } catch (error) { + console.error("[refreshMachineContext] Erro:", error) + } + }, []) + const value = useMemo( () => ({ session: session ?? null, @@ -357,8 +388,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { machineContext, machineContextLoading, machineContextError, + refreshMachineContext, }), - [session, isPending, effectiveConvexUserId, normalizedRole, machineContext, machineContextLoading, machineContextError] + [session, isPending, effectiveConvexUserId, normalizedRole, machineContext, machineContextLoading, machineContextError, refreshMachineContext] ) return {children}