Skip to content

Timeout de Inatividade — Documentação Técnica

1. Visão Geral

O sistema implementa logout automático por inatividade em dois contextos com thresholds diferentes:

ContextoTimeoutWarningMotivo
Sistema Admin (olp_auth)3 horas2h55minEstações compartilhadas em escolas
Mural Olímpico (olp_mural)1 hora59minSessões efêmeras, dispositivos compartilhados (celulares de família)

2. Constantes e Thresholds

Sistema Admin

typescript
const INACTIVITY_LIMIT_MS   = 3 * 60 * 60 * 1000;  // 3 horas → logout
const WARNING_BEFORE_MS     = 5 * 60 * 1000;        // 5 min antes → aviso (2h55m)
const THROTTLE_MS           = 60 * 1000;             // 60s entre gravações no localStorage
const CHECK_INTERVAL_MS     = 60 * 1000;             // 60s entre verificações do timer
const STORAGE_KEY           = 'olp_last_activity';   // Chave no localStorage

Portal Aluno/Responsável

typescript
const TIMEOUT_MS            = 3_600_000;             // 1 hora → logout
const WARNING_MS            = 3_540_000;             // 59min → aviso
const THROTTLE_MS           = 30_000;                // 30s entre gravações (sessões curtas)
const CHECK_INTERVAL_MS     = 30_000;                // 30s entre verificações
const LS_KEY                = 'olp_mural_last_activity';

Por que valores diferentes?

AspectoAdminPortal
Timeout3h — reuniões, intervalos longos1h — sessões rápidas de consulta
Throttle60s — sessões longas30s — sessões curtas, resolução maior
Check60s30s — logout mais preciso

3. Fluxo Técnico

┌─────────────────────────────────────────────────────────────┐
│                    NAVEGADOR (aba OLP)                       │
│                                                             │
│  Eventos DOM capturados:                                    │
│  mousemove, click, keydown, scroll, touchstart              │
│       │                                                     │
│       ▼                                                     │
│  ┌──────────────────┐                                       │
│  │  Throttle (Ns)   │  ← Ignora eventos se <N segundos     │
│  │  useRef(lastSave)│    desde a última gravação            │
│  └────────┬─────────┘                                       │
│           │ (passou N segundos)                             │
│           ▼                                                 │
│  ┌──────────────────────────┐                               │
│  │ localStorage.setItem(    │  ← Apenas um número (epoch)   │
│  │   'olp_[portal_]last_...'│    Nenhum dado sensível       │
│  │   Date.now().toString()  │                               │
│  │ )                        │                               │
│  └──────────────────────────┘                               │
│                                                             │
│  ┌──────────────────────────┐                               │
│  │ setInterval (Ns)         │  ← Lê localStorage            │
│  │                          │                               │
│  │ elapsed = now - lastAct  │                               │
│  │                          │                               │
│  │ if elapsed ≥ TIMEOUT:    │──→ LOGOUT (invoca logout)     │
│  │ elif elapsed ≥ WARNING:  │──→ showWarning = true         │
│  │ else:                    │──→ (nada)                     │
│  └──────────────────────────┘                               │
│                                                             │
│  ┌──────────────────────────┐                               │
│  │ Dialog de aviso          │  ← Aparece quando             │
│  │                          │    showWarning = true          │
│  │ [Estou aqui!]            │──→ Reseta timer (setItem now)  │
│  │                          │    showWarning = false          │
│  └──────────────────────────┘                               │
└─────────────────────────────────────────────────────────────┘

Detalhes do Throttle

O throttle é implementado com useRef (não setTimeout/debounce):

typescript
const lastSaveRef = useRef(0);

const handleActivity = useCallback(() => {
  const now = Date.now();
  if (now - lastSaveRef.current < THROTTLE_MS) return; // Ignora
  lastSaveRef.current = now;
  localStorage.setItem(STORAGE_KEY, now.toString());
}, []);

Impacto em performance:

  • Zero chamadas HTTP (apenas localStorage)
  • Zero re-renders React (useRef, não useState)
  • CPU: ~0.001% (4-5 listeners passivos + 1 interval)

4. Segurança do localStorage

Dado Armazenado

Admin:   olp_last_activity         = "1709312400000"
Mural:   olp_mural_last_activity   = "1709312400000"

Não há dado sensível. O valor é apenas um timestamp.

Persistência Proposital

O localStorage (e não sessionStorage) é usado para que o timer sobreviva a reloads (F5).

Cleanup

O localStorage é limpo em:

  1. Logout voluntário: logoutPortal('logout') / handleLogout removem a chave
  2. Logout por inatividade: logoutPortal('inatividade') remove a chave

5. UX dos Avisos

Admin — "Tem alguém aí?" 👀

AspectoImplementação
Emoji👀
Título"Tem alguém aí?"
Mensagem"Você está ausente há quase 3 horas..."
Botão"Estou aqui! Continuar 🙋"
Componentesrc/components/inactivity-warning-dialog.tsx

Portal — "Sessão expirando..." ⏳

AspectoImplementação
Emoji
Título"Sessão expirando..."
Mensagem"Você está inativo há quase 1 hora..."
Botão"Estou aqui! Continuar 🙋"
Componentesrc/components/portal-inactivity-warning-dialog.tsx

Ambos: ESC bloqueado, não fecha clicando fora, obriga clique no botão.


6. Backend: Motivo do Logout

O logout_portal aceita parâmetro motivo:

typescript
await logoutPortal('inatividade'); // ou 'logout'

O backend registra no log:

json
{
  "acao": "portal.logout",
  "detalhes": {
    "tipo": "logout",
    "motivo": "inatividade",
    "origem": "portal_publico"
  }
}

7. Arquivos Envolvidos

Sistema Admin

ArquivoResponsabilidade
src/hooks/useInactivityTimeout.tsHook principal (3h)
src/components/inactivity-warning-dialog.tsxDialog "Tem alguém aí?"
src/App.tsxIntegra hook + dialog
src/contexts/auth-context.tsxCleanup do localStorage

Portal Aluno/Responsável

ArquivoResponsabilidade
src/hooks/usePortalInactivityTimeout.tsHook principal (1h)
src/components/portal-inactivity-warning-dialog.tsxDialog "Sessão expirando..."
src/pages/portal/PortalEscolaPage.tsxIntegra hook + dialog
src/hooks/usePortalEscola.tslogoutPortal(motivo) + cleanup LS
supabase/functions/portal-escola/index.tsAceita motivo no logout_portal

8. Cross-Tab — Esclarecimento Arquitetural

O sistema NÃO implementa sincronização cross-tab dedicada. O localStorage compartilhado entre abas do mesmo domínio é um efeito colateral inerente da API.


9. Cenários de Teste Manual

Admin

CenárioResultado Esperado
Usuário fica 2h55m sem mexerDialog "Tem alguém aí?" aparece
Usuário clica "Estou aqui!"Timer reseta, dialog fecha
Usuário ignora o dialog por 5minLogout automático, redirect para /

Portal

CenárioResultado Esperado
Aluno/responsável fica 59min sem mexerDialog "Sessão expirando..." aparece
Clica "Estou aqui!"Timer reseta, dialog fecha
Ignora dialog por 1minLogout automático, toast "Sessão encerrada por inatividade"
F5 após 50minTimer não reseta (persiste via localStorage)
Logout voluntárioolp_mural_last_activity removido do localStorage
Log no backendacao: "portal.logout", detalhes.motivo: "inatividade"