Seções

Integração

Contas, API keys e cofre

Usuários, convites, chaves de acesso e como as credenciais são guardadas.


Cada pessoa tem uma conta, as próprias API keys e um cofre com os segredos que os crawlers usam em seu nome: certificado A1, senha do certificado, TOTP, usuário e senha de tribunal. O painel (ui/, Next.js) e a API (/auth, /api-keys, /credenciais, /admin) cobrem tudo; os jobs usam as credenciais pelo campo credencial_id.

Primeiro acesso

uv run python -m core.contas chave          # gera CREDENCIAIS_CHAVE_MESTRA → .env
uv run python -m core.jobs migrar            # schemas jobs e contas (0002)
uv run python -m core.contas admin EMAIL "Nome"   # primeiro admin; senha pedida sem eco

Depois disso, novos usuários entram só por convite: o admin gera o link no painel (Administração → Convites) ou com python -m core.contas convite EMAIL. O link vale 72 h e uma única vez. Não existe cadastro público.

Acesso

QuemComoOnde
Pessoa no painele-mail + senha → cookie de sessão HttpOnly (12 h)ui/
IntegraçãoAuthorization: Bearer crw_… ou X-API-Key: crw_…API direto ou /api do painel
  • Senha: argon2id, mínimo de 12 caracteres misturando letras e números/símbolos. Cinco erros seguidos bloqueiam a conta por 15 minutos; a mensagem de erro é a mesma para e-mail inexistente, senha errada e conta bloqueada.
  • API key: aleatória (256 bits), guardada só como SHA-256; aparece uma única vez. Pode ter prazo e é revogável. O painel mostra só o prefixo.
  • Com cookie, todo método que altera estado exige X-Requested-With: crawlers-ui. Sem CORS na API, um site de terceiros não consegue mandar esse header: isso fecha CSRF.
  • O token fixo antigo (api/security.py) não vale para estas rotas nem para /jobs.
  • Cada usuário só vê os próprios jobs, credenciais e chaves. Admin vê todos os jobs (/jobs?todos=true), administra usuários e convites e lê a auditoria; não lê credenciais de ninguém.

O cofre

Credenciais são somente escrita: nenhuma rota devolve segredo. A API e o painel mostram só metadados — rótulo, sistemas, tribunal, login mascarado (987******00), se há senha/A1/TOTP, e do certificado: titular sem CPF, documento mascarado, emissor, validade e impressão digital.

Uma credencial vale para uma lista de sistemas (pje, eproc, saj...); lista vazia = todos os sistemas, o normal para um certificado A1. Na API, o campo sistemas aceita valores repetidos ou separados por vírgula, e todos volta a liberar para todos. Login e senha costumam ser de um sistema só.

TOTP. Fica dentro da credencial do certificado, cifrado, um por sistema:

  • PJe: o mesmo TOTP vale para todos os tribunais; não se informa tribunal.
  • eProc: cada tribunal tem o seu; totp_tribunal é obrigatório (ex.: TJSC).

Para incluir ou trocar, envie totp com totp_sistema (e totp_tribunal no eProc). Para remover um, remover=totp:pje ou remover=totp:eproc:TJSC; remover=totp apaga todos. A regra está em SISTEMAS_TOTP_POR_TRIBUNAL (core/contas/servico.py).

No envio:

  • o .pfx é aberto com a senha informada; senha errada é recusada na hora (certificados antigos RC2/3DES passam pelo openssl, com a senha pelo stdin);
  • o TOTP é validado como base32 e, se vier o código atual do app, conferido;
  • atualização parcial: só o que veio muda; remover=totp,pfx apaga partes.

Criptografia (core/contas/cripto.py): envelope AES-256-GCM. Cada credencial tem a própria chave de dados, cifrada pela chave mestra (CREDENCIAIS_CHAVE_MESTRA). O id da credencial e o do dono entram como dado autenticado: copiar o blob para outra linha ou outro usuário não decifra. O banco sozinho não abre nada.

Uso pelos jobs

Uma tarefa declara credencial="obrigatoria" ou "opcional" e abre dentro do worker:

@tarefa("pje.algo", credencial="obrigatoria")
def executar(ctx, params):
    with ctx.credencial() as cred:
        PjeAuthenticator(
            cert_pfx_path=str(cred.pfx_path),
            cert_passphrase=cred.pfx_senha,
            cert_totp_secret=cred.totp,
        )

O .pfx existe em disco só dentro do with: diretório privado (modo 700, em /dev/shm quando há), arquivo 600, sobrescrito e apagado na saída. O job só aceita credencial do próprio dono, e cada abertura é auditada (credencial.aberta). credenciais.verificar confere uma credencial sem tocar em tribunal.

curl -X POST https://API/jobs -H "Authorization: Bearer crw_..." \
  -H "Content-Type: application/json" \
  -d '{"tarefa": "credenciais.verificar", "credencial_id": "<uuid>"}'

Rotação da chave mestra

  1. Gere uma chave nova; ela vai em CREDENCIAIS_CHAVE_MESTRA e a antiga em CREDENCIAIS_CHAVES_ANTIGAS.
  2. uv run python -m core.contas rotacionar re-embrulha as chaves de dados (o conteúdo cifrado não muda).
  3. Tire a antiga do .env.

Perder a chave mestra = perder todas as credenciais: guarde uma cópia fora do servidor.

Auditoria

contas.auditoria registra login (ok/falha/recusado), convites, API keys, criação/edição/remoção de credencial e cada abertura por job, com IP. Cada usuário vê a própria em "Minha conta"; o admin vê tudo.

Painel

cd ui && pnpm install && API_URL=http://localhost:8000 pnpm dev   # desenvolvimento
docker compose -f docker-compose.api.yml up -d                    # api + ui (Traefik)

O painel chama a API pela própria origem (/api/* é repassado para API_URL), então o cookie é first-party e a API não precisa de CORS. Em desenvolvimento local por http, use CONTAS_COOKIE_SEGURO=false na API.