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
| Quem | Como | Onde |
|---|---|---|
| Pessoa no painel | e-mail + senha → cookie de sessão HttpOnly (12 h) | ui/ |
| Integração | Authorization: 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 peloopenssl, 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,pfxapaga 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
- Gere uma chave nova; ela vai em
CREDENCIAIS_CHAVE_MESTRAe a antiga emCREDENCIAIS_CHAVES_ANTIGAS. uv run python -m core.contas rotacionarre-embrulha as chaves de dados (o conteúdo cifrado não muda).- 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.