// Shell do console: rail navy + navegação + contexto de ponto + painel de comando. const DSa = window.WiFi_eeb21e; const { CharlesLogoMark: LogoA, Badge: BadgeA } = DSa; const { Icone: IconeA, CmdPainel: CmdPainelA, PontoMenu: PontoMenuA } = window; const GESTAO_TELAS = [ { id: 'visao', rot: 'Visão geral', ic: 'grid' }, { id: 'inventario', rot: 'Dispositivos', ic: 'lista' }, { id: 'listabranca', rot: 'Lista branca', ic: 'escudo' }, { id: 'ssids', rot: 'SSIDs & bridges', ic: 'wifi' }, { id: 'telas', rot: 'Telas do portal', ic: 'tela' }, { id: 'registros', rot: 'E-mails capturados', ic: 'mail' }, { id: 'iot', rot: 'IoT', ic: 'cpu' }]; function GestaoApp() { // Sessão real: o cookie HttpOnly (gestao_sessao, path=/api) é a fonte de verdade. // O JS nunca lê o cookie — pergunta ao servidor por GET /api/auth/eu. const [sessao, setSessao] = React.useState(null); // {email, perfil} | null const [carregando, setCarregando] = React.useState(true); const [erroLink, setErroLink] = React.useState(null); // 'expirado'|'substituido'|null React.useEffect(() => { let vivo = true; (async () => { // 1) token do e-mail (?acesso=). O POST troca o token por uma sessão. A troca é IDEMPOTENTE // no servidor dentro do TTL (15 min): filtros de e-mail (Microsoft Safe Links etc.) que // detonam o link num sandbox — executando o JS e até clicando — não queimam o token; o // humano troca depois e entra. Por isso podemos trocar já no load, sem fricção. const params = new URLSearchParams(window.location.search); const token = params.get('acesso'); if (token) { try { const r = await fetch(`/api/auth/acesso/${encodeURIComponent(token)}`, { method: 'POST' }); limparUrl(); if (r.ok) { const d = await r.json(); if (vivo) { setSessao(d); setCarregando(false); } return; } // erro: talvez o cookie já exista (self-heal via /eu) antes de mostrar a tela de erro const err = await r.json().catch(() => ({})); const eu = await fetch('/api/auth/eu'); if (eu.ok) { const d = await eu.json(); if (vivo) { setSessao(d); setCarregando(false); } return; } if (vivo) { setErroLink(err.erro || 'expirado'); } } catch (e) { limparUrl(); if (vivo) setErroLink('expirado'); } } // 2) sem token (ou após erro): já tem sessão pelo cookie? try { const r = await fetch('/api/auth/eu', { headers: { 'Accept': 'application/json' } }); if (vivo && !sessao) setSessao(r.ok ? await r.json() : null); } catch (e) { if (vivo) setSessao(null); } if (vivo) setCarregando(false); })(); return () => { vivo = false; }; }, []); const sair = async () => { try { await fetch('/api/auth/sair', { method: 'POST' }); } catch (e) {} setSessao(null); setErroLink(null); }; const [tela, setTela] = React.useState('visao'); const [sel, setSel] = React.useState(['habibi']); const [cmd, setCmd] = React.useState(null); const go = (t, s) => { if (s) setSel([s]); setTela(t); }; const COMP = { visao: window.TelaVisaoGeral, inventario: window.TelaInventario, listabranca: window.TelaListaBranca, ssids: window.TelaSsids, telas: window.TelaTelasPortal, registros: window.TelaRegistros, iot: window.TelaIot }; const Corpo = COMP[tela]; const rotulo = GESTAO_TELAS.find(t => t.id === tela).rot; if (carregando) return