intermediate
portal-cidade
magic-link
HMAC

Portal da Cidade e Magic-Link Auth

O portal /city/{slug} no qual contatos municipais fazem login via magic link enviado por e-mail -- limitado a geometria da jurisdicao, somente leitura, com cookie de sessao assinado por HMAC de 24h.

Equipe Levy FleetsMay 18, 202610 min read

Portal da Cidade e Magic-Link Auth

Oficiais de compliance municipal nao querem outra senha. O portal da cidade do Levy em /city/{slug} usa auth por magic link baseado em e-mail -- digite seu e-mail, clique no link, voce esta dentro. A sessao e assinada por HMAC, dura 24 horas e e limitada a uma unica jurisdicao.

Quem pode fazer login

Apenas e-mails listados em city_contacts com portal_access = true podem se autenticar. Adicionar um contato municipal acontece no dashboard do operador, nao no portal em si -- nao ha auto-cadastro.

Estrutura de URL

PaginaURLProposito
Login/city/{slug}Inserir e-mail, solicitar magic link
Callback de auth/api/city/{slug}/auth/callback?token=<tok>Consumir magic link, definir cookie de sessao, redirecionar para dashboard
Dashboard/city/{slug} (pos-auth)Scoreboard de compliance + links para outras visoes
Mapa de frota/city/{slug}/fleetVeiculos dentro da bbox da jurisdicao
Heatmap de viagens/city/{slug}/tripsDensidade de viagens em buckets de tempo
Corredores de estacionamento/city/{slug}/parking-corralsSnapshots de utilizacao
Relatorio de compliance/city/{slug}/compliance-report?period=monthly&date=2026-05Baixar CSV/PDF

O slug corresponde a mds_jurisdictions.slug. Um slug = uma jurisdicao = uma bbox.

1

Contato insere seu e-mail

Em /city/{slug}, o formulario faz POST para POST /api/city/{slug}/auth/magic-link com { email }. A rota sempre retorna HTTP 200 -- nao revelamos se o e-mail existe em city_contacts (evita enumeracao).

2

Token gerado e enviado por e-mail

Se o e-mail corresponde a uma linha city_contacts com portal_access = true para este slug, a rota gera um token de uso unico, armazena seu hash sha256 + expires_at = now() + 15 minutos na linha do contato e envia o link por e-mail.

3

Contato clica no link

O link vai para /api/city/{slug}/auth/callback?token=<tok>. A rota faz hash do token, busca a linha city_contacts correspondente, verifica expires_at, marca o token como consumido e emite um cookie de sessao.

4

Cookie de sessao definido

Set-Cookie: levy_city_session=<hmac>; Path=/city/<slug>; HttpOnly; Secure; SameSite=Lax; Max-Age=86400. O payload do cookie codifica city_contact_id, jurisdiction_id, issued_at. Assinado por HMAC com um segredo do lado do servidor.

5

Redirecionamento para /city/{slug}

Agora mostra o dashboard pos-auth. Cada requisicao subsequente a /api/city/{slug}/* verifica o HMAC e corta a resposta para a bbox.

O que o contato ve

O dashboard mostra:

  • Scoreboard de compliance -- pass/fail por linha permit_conditions, rolling de 30 dias
  • Snapshot de hoje -- contagem de frota implantada, viagens, reclamacoes abertas
  • Politicas ativas -- as regras ingeridas de seu proprio feed de politica (sanity check)
  • Links -- para mapa de frota, heatmap de viagens, corredores de estacionamento, relatorio mensal

Tudo abaixo do snapshot e somente leitura. O portal nao deixa a cidade editar dados do operador -- eles editam seu proprio feed de politica e nos reingerimos.

Corte por bbox

Toda rota da API do portal da cidade e cortada para a bbox da jurisdicao antes de retornar dados. O corte e aplicado no lado do servidor, nao na camada UI:

// Dentro de cada rota /api/city/{slug}/*
const session = await verifyCitySession(request);
const jurisdiction = await getJurisdiction(session.jurisdiction_id);
const bbox = jurisdiction.bbox; // [minLng, minLat, maxLng, maxLat]

const vehicles = await sb
  .from('vehicles')
  .select('id, last_lat, last_lng, status, vehicle_number')
  .gte('last_lng', bbox[0])
  .lte('last_lng', bbox[2])
  .gte('last_lat', bbox[1])
  .lte('last_lat', bbox[3]);

Um contato municipal nunca vera um veiculo fora de sua fronteira, mesmo que exista na mesma subconta.

Para o heatmap de viagens, o corte e realizado no ponto de inicio da viagem -- uma viagem que comecou dentro da jurisdicao e terminou fora ainda aparece no heatmap. E o que cidades esperam para analises de origem de viagem.

Expiracao de sessao + refresh

Sessoes duram 24 horas. Nao ha fluxo de refresh -- o contato solicita um novo magic link quando a sessao expira. Isso e deliberado: um analista municipal perdendo acesso por algumas horas e passando pelo movimento do magic link novamente e aceitavel; sessoes longas no portal da cidade nao sao.

O segredo HMAC pode ser rotacionado configurando um novo valor na env config e redeployando. Sessoes assinadas com o segredo antigo falham imediatamente a verificacao HMAC e o contato recebe um prompt de re-login limpo.

Multiplos contatos por jurisdicao

Uma jurisdicao pode ter qualquer numero de linhas city_contacts. Padrao comum:

PapelE-mail
Oficial principal de compliancesarah@city.gov
Revisor de licencapat@city.gov
Analista Populus / Ride Report (consultor)analyst@populus.ai

Todos os tres veem os mesmos dados, todos os tres recebem e-mails de digest em sua propria cadencia. Sessoes sao independentes -- um contato fazendo logout nao afeta os outros.

Revogando acesso

Para revogar acesso de um contato:

  1. Abra /dashboard/compliance/{jurisdiction-id} -> Contatos municipais.
  2. Defina portal_access = false na linha, ou delete-a totalmente.
  3. Cookies de sessao ativos nesse contato falharao a verificacao do lado do servidor na proxima requisicao -- o cookie ainda e HMAC valido, mas a linha do contato nao permite mais acesso ao portal.

Magic links ja emitidos para esse contato tambem sao invalidados -- auth/callback rejeita tokens para contatos onde portal_access = false.

Localizacao

O portal e renderizado na preferencia de idioma do contato (city_contacts.locale, padrao en). E-mails de digest tambem usam a mesma preferencia. Lancamento suporta en e es; locales adicionais voltam para ingles.

Solucao de problemas

  • Contato diz "nao recebi o e-mail" -- verifique spam, depois confirme que o e-mail corresponde a city_contacts.email exatamente (insensivel a caixa). Reenvie inserindo o e-mail no formulario do portal novamente.
  • Link diz "expirado" -- magic links vivem 15 minutos. O contato deve solicitar um novo.
  • Link diz "invalido" -- o token ja foi consumido, a linha do contato foi deletada ou portal_access foi definido como false. Investigue no dashboard do operador.
  • Cookie de sessao nao esta sendo definido -- o contato esta em um navegador que bloqueia cookies de terceiros e /city/{slug} esta sendo carregado dentro de um iframe em algum lugar. Evite embeds de iframe.

Proximos passos