Skip to content

Segurança, Autenticação & Impersonação (RBAC)

Segurança, Autenticação & Impersonação (RBAC & Security)

Section titled “Segurança, Autenticação & Impersonação (RBAC & Security)”

Quando /api/auth/me responde 401, a restauração inicia o chunk da tela desconectada e aquece /api/public/config em paralelo. A resposta informa apenas se existe um cookie HttpOnly de refresh (available/absent), nunca seu valor: ausência confirmada elimina um POST condenado a outro 401, enquanto header ausente de um Worker antigo mantém o fallback compatível. Se a credencial existe, /api/auth/refresh continua sendo validado e rotacionado normalmente; se for recusado, o loader compartilhado com React.lazy já está pronto. O formulário aguarda somente seu próprio chunk: falha ou lentidão da configuração pública não bloqueia a interface, e a leitura do controller reutiliza a política HTTP de cinco minutos do endpoint. Assim o login social perde uma etapa serial em inicializações anônimas frias sem confiar em marcador local nem enfraquecer a restauração autenticada.

No Android, o botão Google permanece acionável enquanto a inicialização antecipada do provider termina. O primeiro toque aguarda a mesma Promise serializada, em vez de ser descartado e exigir um segundo toque; o lock síncrono continua impedindo duas Activities concorrentes. Perfil, configurações do chat e navegação usam useLogoutAction: a trava é adquirida antes até mesmo de uma confirmação opcional, todas as superfícies exibem o mesmo loading acessível e nenhuma anuncia sucesso antes da revogação. A operação de contexto também é idempotente, e o menu principal mantém uma ação de saída acessível mesmo recolhido.

A exclusão de conta trava antes de abrir a confirmação, impedindo dois diálogos ou dois POSTs no mesmo frame, vincula a conclusão à identidade/lifecycle atual e limita o POST a 15 segundos. Cancelamento libera uma nova tentativa, falha mantém a ação disponível e sucesso permanece ocupado até o logout.

Em Profile → Security → Linked Accounts, Android e iOS vinculam Google pelo mesmo provider nativo usado no login; web/PWA/desktop preservam o botão Google Identity Services. A configuração pública necessária é lida do boot com teto de 12 segundos, credenciais da sessão e cancelamento ao desmontar ou trocar de conta; a configuração anterior é apagada antes da nova leitura, impedindo que uma resposta tardia habilite o provider para outra identidade. A primeira ativação nativa aguarda a inicialização serializada, callbacks possuem lock e owner ID, e uma resposta iniciada por outra conta é descartada antes de alcançar POST /api/auth/link-google. Desvincular adquire a trava antes da confirmação, mantém loading até a resposta real e só confirma sucesso para a identidade que iniciou a operação.

Ativar e remover passkeys no Perfil compartilha lock síncrono e epoch da conta. Uma conclusão de Face ID, Touch ID, impressão digital ou Windows Hello iniciada antes de logout/impersonação não altera a identidade seguinte. Remover biometria só apaga a dica local e anuncia sucesso depois que /api/auth/webauthn/remove responde com sucesso; rede ou servidor indisponível preservam a dica e informam que a credencial continua ativa.

A troca administrativa de identidade possui lock síncrono antes do primeiro await e epoch vinculado ao usuário atual. O seletor emite apenas feedback de seleção ao iniciar; sucesso ocorre somente depois que o backend autoriza, o novo cookie é confirmado e a limpeza privada termina. Falha restaura o seletor, produz háptico de erro e mensagem recuperável, enquanto uma conclusão pertencente a uma tela ou identidade antiga é descartada. Em sucesso, o carregador permanece ativo até a navegação efetiva, impedindo que a interface administrativa reapareça com a sessão já trocada.

A camada de segurança do Prime Crown garante controle estrito de acessos aos dados operacionais e financeiros através de tokens JWT em Cookies HttpOnly, validação de permissões RBAC no Hono e suporte a Impersonação de Usuários para Suporte.

O login Google possui duas entradas equivalentes. No navegador/PWA, Google Identity Services entrega o ID token ou usa o redirect server-side; no Android Capacitor, o provider nativo abre o seletor de contas do sistema usando os scopes OIDC padrão já incluídos pelo plugin (openid, e-mail e perfil), sem options.scopes ou alteração especial da MainActivity. Ambos enviam o ID token ao mesmo backend, que valida a audience contra o OAuth Web Client ID antes de criar a sessão HttpOnly. Antes de recarregar, o cliente confirma que esse cookie já pode ser lido por /api/auth/me; a janela de confirmação cobre a latência adicional do WebView e uma falha não é tratada como sucesso, evitando que o primeiro toque volte silenciosamente ao login. Na variante web, container e wrapper do GSI reservam exatamente 48 px e contêm overflow; o iframe recebe 44 px desde a inserção e fica centralizado, em vez de crescer de 22 px depois do primeiro frame. A montagem anônima/personalizada não desloca mais o card, sem alterar o conteúdo cross-origin ou a marca do Google. O cliente OAuth Android serve para autorizar o binário instalado e deve combinar o package uk.co.primecrowncleaning.app com o SHA-1 do certificado da instalação; ele não substitui a validação server-side.

Os rótulos auxiliares e divisores da autenticação preservam contraste AA sobre o card branco. O documento raiz também publica uma descrição estável para navegadores e indexadores.

Em viewports horizontais com até 560 px de altura, o mesmo card compartilhado adota cabeçalho em linha, espaços menores e corpo compacto. Assim Google, Apple quando disponível, biometria e a entrada por e-mail/telefone permanecem reconhecíveis na primeira tela de celulares rotacionados, split-screen e janelas desktop baixas. Formulários expandidos continuam dentro da região rolável e preservam safe areas e recuperação quando o teclado reduz o viewport.

robots.txt e llms.txt são servidos como texto antes do fallback da SPA, mantêm API, chat e guia interno fora de crawling e apontam somente para informações realmente públicas. Web Vitals observados antes da restauração da sessão ficam temporariamente em memória; são enviados apenas depois que AuthContext confirma uma identidade, e são descartados no login anônimo para não produzir telemetria 401.

No iPhone, a presença de Google Sign-In e de cadastro público torna arriscado depender da exceção empresarial da regra 4.8 da App Store. functions/lib/appleIdentity.ts valida o ID token contra o JWKS público da Apple e exige assinatura RS256, issuer oficial, audience configurada, validade temporal e subject. POST /api/auth/apple localiza primeiro pelo sub, aceita vínculo/criação inicial somente com e-mail verificado (inclusive relay privado), reutiliza funcionários/clientes existentes e emite a mesma sessão HttpOnly. O provider solicita troca correta do authorization code; quando as credenciais Apple estão configuradas, functions/lib/appleOAuth.ts troca o código no servidor e o refresh token é cifrado em D1 para futura revogação. Capability, entitlement e provider nativo estão incluídos; login e cadastro exibem o botão Apple apenas quando getRuntimePlatform() retorna ios, mantendo Android e PWA no fluxo próprio.

Passkeys WebAuthn descobríveis mantêm a chave privada no autenticador seguro do dispositivo e persistem somente a chave pública no backend. O botão de login aparece desde o primeiro render em runtimes WebAuthn e nos shells nativos com provider empacotado, evitando que o cartão cresça depois da detecção e gere layout shift. A capacidade não fica limitada ao autenticador de plataforma: passkeys sincronizadas e chaves de segurança externas também permanecem válidas no desktop. Em aparelhos novos, uma autenticação sem e-mail permite que Google Password Manager ou o autenticador do sistema apresente as passkeys sincronizadas e que o backend resolva a conta pelo credential ID. As chamadas de opções, verificação e remoção usam o transporte limitado de 15 segundos; o prompt do autenticador fica deliberadamente fora desse prazo, para que o usuário possa concluir Face ID, Touch ID, impressão digital ou chave externa sem uma corrida artificial. O cliente guarda somente marcadores opacos de UX; a migração primecrown_passkey_markers_v3 converte e apaga as antigas listas e dicas de e-mail em texto claro. A dica Google é limitada ao sessionStorage. password_hash é omitido nos endpoints de autenticação e bloqueado defensivamente pelo mapper compartilhado antes de qualquer resposta JSON.

Enquanto o prompt biométrico está ativo, a tela substitui as demais ações de autenticação por um único indicador de progresso. Isso impede cliques concorrentes e restaura todas as opções automaticamente após cancelamento ou falha.

Os controles textuais da autenticação também preservam alvo de toque mínimo de 44 px: expansão de email/senha, cadastro manual, recuperação, criação de conta e retorno ao login. touch-manipulation mantém resposta direta sem alterar a hierarquia visual leve desses links, compartilhando a mesma ergonomia entre navegador, PWA, Android e iOS.

Login e cadastro anunciam os estados assíncronos de Google e biometria como status ocupados e educados. O botão Apple preserva seu nome acessível, publica aria-busy e permanece desabilitado enquanto o provider responde; todos os spinners ficam decorativos para VoiceOver/TalkBack.

Falhas nos fluxos de login, cadastro, recuperação, redefinição e troca obrigatória são regiões alert; a confirmação de recuperação é um status educado. A troca obrigatória usa o loading compartilhado de CFButton, e os botões de revelar senha publicam nome contextual e aria-pressed.

Falhas inesperadas de login, cadastro, sessão, impersonação, recuperação/troca de senha, Google OAuth e WebAuthn passam por authError.ts. O diagnóstico é sanitizado e classificado em system_logs; nenhuma resposta ou escrita de console recebe a exceção bruta. Mensagens funcionais — credencial inválida, conta existente, token inválido/expirado, autorização e orientação biométrica — preservam status e texto próprios. Assim o mesmo contrato atende navegador, PWA, desktop, Android e iOS sem empobrecer a correção do formulário.

As respostas JSON de identidade passam por parseApiJson mesmo quando o fluxo usa fetch direto em vez de CloudClient. Isso inclui restauração/refresh, senha, Google, Apple, telefone, magic link, impersonação, vínculo, troca e redefinição de senha, passkeys e exclusão pelo Perfil. O tratamento funcional de 4xx e seus fallbacks permanece igual, mas campos de instante SQLite são fixados em UTC antes de alcançar estado React; Safari/iOS, Android/Chromium, PWA e desktop não podem interpretar a mesma sessão ou auditoria em horários diferentes.

Login por senha ou biometria, cadastro, recuperação, magic link, troca obrigatória e redefinição de senha possuem trava síncrona contra toque duplo, bloqueiam campos e navegação durante a requisição e ignoram respostas que chegam depois da troca de tela, troca de usuário ou desmontagem. Senha, Google, Apple, cadastro, telefone, solicitação/verificação de link, recuperação, redefinição, impersonação, vínculo Google e troca de senha usam o transporte limitado compartilhado: mutações encerram em 15 segundos, e os formulários públicos abortam também ao trocar de tela ou desmontar. Depois que um provider ou credencial emite cookie, a confirmação por /api/auth/me possui teto total de oito segundos e tentativas individuais de dois segundos; uma leitura presa não bloqueia o retry seguinte nem deixa a WebView em loading indefinido. O token recebido por link é mantido apenas na memória e removido imediatamente da barra de endereço e do histórico visível. Em dispositivos nativos, início, sucesso e falha também produzem feedback tátil sem antecipar uma confirmação do servidor; cancelar a biometria continua sendo uma saída silenciosa.

O sanitizador reconhece também nomes compostos de segredo em texto, incluindo client_secret, refresh_token, chaves privadas, de assinatura e de acesso. A proteção vale para erros OAuth, logs dos Workers e telemetria do cliente, além das chaves sensíveis já tratadas em objetos aninhados.

As builds de produção instalam uma fronteira única para os 17 métodos de console que aceitam dados antes dos handlers globais. Browser, PWA e os WebViews Android/iOS preservam somente o primeiro rótulo operacional redigido e números/booleanos; objetos, exceções, payloads de provedores e textos auxiliares são omitidos. E-mail, telefone, UUID e query/hash encontrados no rótulo também são removidos. O console completo permanece disponível apenas no desenvolvimento local, e erros relevantes continuam chegando ao painel administrativo pela telemetria sanitizada.

Logout é uma fronteira de dispositivo, não apenas de cookie. A interface encerra a sessão local imediatamente, cancela e apaga consultas autenticadas e desmonta os dados antes das tarefas de rede, para o comando responder sem atraso também no WebView nativo. No Android/iOS, a leitura do token já persistido recebe um limite curto e a revogação de cookie começa sem aguardar a limpeza do provider; Firebase, Preferences ou unregister nunca ficam entre o toque e /api/auth/logout. O endpoint remove o token conhecido com escopo do usuário e revoga a sessão na mesma operação. Depois o cliente limpa fila offline, histórico de alertas, snapshots de boot e dica Google; Android/iOS também encerram em segundo plano a sessão mantida pelo provider Google. Revogação remota executa a limpeza local mesmo quando a exclusão server-side já não pode ser autorizada, e impersonação apaga conteúdo privado antes de carregar a identidade de destino. As etapas externas são limitadas por tempo e um failsafe de navegação garante que uma ponte nativa travada nunca obrigue o usuário a forçar o fechamento do app. O evento de retomada só revalida sessões já autenticadas: o retorno do seletor nativo Google não pode ser confundido com revogação enquanto a troca do token ainda está terminando.

Antes de remover a identidade React, o cliente grava uma barreira opaca pc_logout_pending_v1, sem ID, e-mail ou token. Como JavaScript não consegue apagar diretamente um cookie HttpOnly quando está offline, uma nova inicialização consulta essa barreira antes de /api/auth/me: tenta novamente o logout idempotente e permanece na tela de acesso se a rede ainda estiver ausente. A barreira só é removida por revogação confirmada ou por um novo login confirmado, portanto reiniciar Android/iOS, recarregar a PWA ou fechar o desktop não ressuscita a sessão que o usuário já encerrou.

Erros de campo em CFInput combinam aria-invalid, aria-describedby e uma região alert, mantendo o texto associado ao controle e anunciando validação dinâmica. Os disclosures de senha e cadastro manual usam aria-expanded e aria-controls, além de esconder a seta decorativa.

Exclusão de conta é iniciada na área destrutiva separada de Profile → About por POST /api/auth/account-deletion. A confirmação explica que a rota autenticada grava/atualiza uma única linha PENDING em account_deletion_requests, define deletion_due_at para 30 dias, revoga todos os refresh tokens, eleva access_revoked_at, aplica bloqueio temporário e limpa os cookies do dispositivo atual. Se houver vínculo Apple, a rota também revoga o refresh token pelo endpoint oficial e registra o resultado; a conclusão administrativa é bloqueada enquanto essa revogação estiver pendente, falhar ou exigir nova autorização. Após a desativação, todos os administradores recebem e-mail com o prazo e o caminho Settings → Account Deletions; admin_notification_status audita o envio sem transformar uma indisponibilidade do Resend em falha da solicitação. A fila exclusiva de ADMIN consulta e resolve solicitações por /api/auth/admin/account-deletions; concluir ou rejeitar exige confirmação explícita da revisão de retenção, registra horário e administrador responsável e não executa uma remoção genérica potencialmente destrutiva. A fila adquire lock antes da confirmação, bloqueia toda ação concorrente, limita mutações a 15 segundos e trata leituras como latest wins, abortando a anterior e descartando resposta antiga. Revogação de acesso da equipe usa o mesmo teto. Após a resolução, o usuário recebe confirmação pelo Resend; notification_status audita esse segundo envio. Antes de marcar COMPLETED, o operador deve anonimizar/remover os dados sem base legal e preservar somente registros fiscais, trabalhistas, antifraude ou de disputa pelo prazo aplicável. A página pública mantém e-mail como fallback para quem perdeu acesso, nunca como etapa obrigatória dentro do app.

No shell iOS, Google e Apple compartilham um único lock de autenticação social. Enquanto um provider está aberto, o outro botão fica desabilitado e o hook recusa uma segunda Activity mesmo se um toque escapar da interface; callbacks dos dois sheets não podem cruzar sessão, loading ou troca de token. Em instalação nova, o primeiro toque Google aguarda a mesma leitura pública deduplicada do Web Client ID e inicializa o provider com o valor resolvido, sem depender de um segundo render ou segundo toque. Essa leitura usa o transporte limitado compartilhado, encerra em 12 segundos e é abortada ao desmontar o login; rede degradada não pode prender indefinidamente a primeira ativação, e a Promise é liberada para uma tentativa posterior.

A restauração inicial mantém um único AbortController e timeout ativos durante /me, refresh e a segunda leitura, cancelando tudo no teardown. Revalidações de app-resume são latest wins, abortam a anterior e confirmam o ID do usuário no limite da revogação; um 401 atrasado de outra sessão não encerra a conta atual. Revogação confirmada remove imediatamente o usuário da UI antes da limpeza de push/storage e da navegação final.


🔒 1. Fluxo de Autenticação & Impersonação

Section titled “🔒 1. Fluxo de Autenticação & Impersonação”
sequenceDiagram
    autonumber
    actor Admin as Administrador / Gerente
    actor User as Cliente / Funcionária
    participant Edge as Edge Auth (/api/auth/*)
    participant D1 as Cloudflare D1 (auth_accounts)
    participant App as Dashboard SPA

    Note over User, App: Login Padrão (Email/Senha, Google ou Apple)
    User->>Edge: POST /api/auth/login { email, password }
    Edge->>D1: Verifica hash PBKDF2 / BCRYPT em auth_accounts
    Edge-->>User: Define Cookie HttpOnly `token` (JWT assinado com JWT_SECRET)

    Note over Admin, App: Impersonação para Suporte Técnico
    Admin->>Edge: POST /api/auth/impersonate { targetUserId }
    Edge->>Edge: Valida se o solicitante possui role ADMIN
    Edge-->>Admin: Retorna novo JWT assinado com a identidade do cliente/funcionário
    Admin->>App: Navega na aplicação exatamente na visão do usuário impersonado

🛡️ 2. Níveis de Permissão (Roles RBAC)

Section titled “🛡️ 2. Níveis de Permissão (Roles RBAC)”

O sistema possui quatro papéis de autorização. LEAD não é um novo valor de role: é um tipo de referência de uma conta CLIENT.

Role Escopo de Acesso Permissões
ADMIN Acesso Total Acesso irrestrito a todos os dados, finanças, configurações de sistema, auditorias, logs e impersonação.
MANAGER Operacional Geral Gestão de agendamentos, clientes, equipes, suporte por chat e alocação de limpezas. Sem acesso a impersonação crítica.
CLIENT / EMPLOYEE Restrito (Self Service) Visualizam apenas seus próprios agendamentos, mensagens de chat e extrato/faturas pessoais.

Identidade referenciada e cadastro por telefone

Section titled “Identidade referenciada e cadastro por telefone”

auth_accounts.reference_type pode ser LEAD, CLIENT ou EMPLOYEE; reference_id só pode ser interpretado junto com esse discriminador. Uma conta com role = CLIENT e reference_type = LEAD recebe o dashboard comercial e pode usar Request Service, mas não acessa dados de um cliente contratado. Quando uma cotação é confirmada, o backend muda a referência para CLIENT no mesmo fluxo que cria o atendimento.

Senha, Google e Apple convergem no mesmo gate para pessoas ainda desconhecidas. O primeiro passo emite um JWT de cadastro pendente com validade de dez minutos e não escreve entidades. POST /api/auth/phone/start valida esse token, aplica limite persistente de três envios por hora e envia um código pelo WhatsApp. POST /api/auth/phone/verify aceita somente seis dígitos, limita tentativas, consome a verificação e só então cria/vincula conta e lead/cliente. Posse do telefone resolve identidade existente, mas nunca transfere um número que já seja login de outra conta.

PhoneVerificationStep trava a ação antes do primeiro await, associa erros ao campo ativo e impede voltar/trocar número enquanto a operação está em voo. O teclado usa tel para o número e numeric + one-time-code para o OTP, preservando autofill, VoiceOver/TalkBack e comportamento nativo nas três plataformas.

A matriz canRoleAccessView em src/constants/navigation.ts é aplicada ao menu, parâmetros ?view=, mensagens de navegação do service worker, Universal Links, custom schemes e ao próprio AppRoutes. Tentativas de abrir uma área incompatível com o papel voltam ao Dashboard antes de processar channelId ou reqId. A restauração também é reexecutada quando muda o ID ou o papel da sessão: usa a tela mais recente por referência, sem transformar cada navegação em dependência do efeito, e remove imediatamente uma rota administrativa após redução de privilégio. Essa barreira de interface complementa — e não substitui — o escopo obrigatório aplicado por cada endpoint do backend.

O gate npm run check executa npm audit --audit-level=moderate sobre a árvore travada antes do build. Isso cobre tanto dependências de execução sensíveis — como Hono e DOMPurify — quanto a cadeia local de Workers usada para validar o mesmo backend publicado. Um advisory moderado ou alto precisa ser resolvido e toda a suíte repetida antes do push.

O estado local de logout mantém o carregador global desativado: assim que a identidade vira null, a tela de login aparece no próximo commit, enquanto remoção do push e revogação server-side continuam com o cookie ainda válido. Isso evita que rede degradada transforme a saída em vários segundos de Connecting Engine no Android, iOS, PWA ou desktop.

A inicialização do login social nativo é deduplicada por configuração e serializada quando o Web Client ID ou o conjunto Android/iOS muda. Alternar entre Login e Cadastro não inicia configurações concorrentes do plugin, e a própria ação Google/Apple aguarda a configuração atual dentro do gesto antes de abrir o provider. Falhas liberam o cache para nova tentativa, evitando o primeiro toque sem efeito durante retomada ou configuração tardia da WebView.

O hook compartilhado de push só fica habilitado com usuário autenticado e fora de impersonação. A tela de login não lê permissões nem registra o dispositivo na montagem ou retomada web/nativa; uma chamada de Subscribe sem sessão retorna indisponibilidade explícita. Cada operação captura a identidade e a geração do lifecycle: logout, troca de conta e desmontagem descartam permissões, resultados de registro e feedback tardios. O Web Push propaga cancelamento às leituras/escritas HTTP e revalida após esperas do navegador, impedindo que uma assinatura obtida pela sessão anterior seja salva pela próxima. Cancelar HTTP não desfaz uma gravação já aceita pelo servidor. A desativação explícita também pertence à sessão: callbacks retidos após logout são ignorados; conclusão nativa tardia não sobrescreve a permissão da conta seguinte, e uma busca web antiga não consulta nem remove a assinatura do novo usuário.