Auth (Registro e Login)

3. Auth (Registro e Login)#

POST/v1/auth/register#

Cria uma nova empresa (tenant) com o usuário owner.

Auth: Nenhuma

Request:

json
{
  "company_name": "Minha Empresa",
  "owner_email": "dono@empresa.com",
  "owner_name": "Joao Silva",
  "password": "MinhaSenh@123"
}
Campo Tipo Obrigatório Descricao
company_name string sim Nome da empresa
owner_email string sim Email do owner
owner_name string sim Nome do owner
password string sim Senha (mínimo 12 caracteres)

Resposta 201:

json
{
  "company_id": 1,
  "token": "ctc_xxxx...xxxx",
  "api_token": "ctc_xxxx...xxxx",
  "token_id": 1,
  "last4": "xxxx",
  "slug": "minha-empresa",
  "email_verification_required": true,
  "message": "company registered successfully"
}
Campo Tipo Descricao
company_id integer ID numerico da empresa criada (ex: 1, 38, 42 — NÃO é string)
token string API token bootstrap ctc_... — mesmo valor que api_token
api_token string Alias de token (mesmo valor, mantido por retrocompatibilidade)
token_id integer ID interno do token no banco
last4 string Últimos 4 caracteres do token (para exibição segura)
slug string Slug URL-safe derivado do company_name
email_verification_required boolean true quando o owner deve confirmar o email com o código 6 dígitos enviado por Resend (sempre que o registro usou senha). Integrações que não renderizam UI podem ignorar — POST /v1/instances e /v1/tokens e /v1/webhooks retornam 403 EMAIL_NOT_VERIFIED até que POST /v1/auth/verify-email seja chamado.
message string Mensagem de confirmação

⚠️ company_id é INTEGER, não string. JSON retorna "company_id": 38 (número sem aspas). Tipagens de DTO que usem string para este campo falharão silenciosamente no json.Unmarshal / JSON.parse com erro de tipo. Use int, int64, ou number.

O token retornado no registro e um API token bootstrap (ctc_...), não um JWT de sessão do browser. Para chamadas subsequentes, envie via X-API-Key: ctc_... (ver seção Autenticação acima). Para iniciar a sessão web, chame POST /v1/auth/login.

Erros:

Status Código Descricao
400 INVALID_JSON Corpo não e JSON válido
400 MISSING_FIELD Campo obrigatório ausente (company_name, owner_email, owner_name, password)
400 BAD_REQUEST company_name com caracteres inválidos, owner_email inválido, ou senha com menos de 12 caracteres
409 CONFLICT Nome/slug da empresa já existe (company name already taken)
409 EMAIL_ALREADY_REGISTERED Email já usado por outra conta
500 REGISTRATION_FAILED Falha ao provisionar plano, empresa, usuário, token ou tenant DB

Integrações automatizadas (como o Catcher) que provisionam multiplas empresas com o mesmo email humano devem usar plus-addressing (user+suffix@domain.com) ou emails únicos por tenant para evitar EMAIL_ALREADY_REGISTERED.


POST/v1/auth/quick-register#

Cria uma empresa (tenant) a partir de apenas um email. Gera automaticamente nome da empresa, nome do owner, senha e API key, e marca o email como verificado na criação (sem o código de 6 digitos) — receber o email com as credenciais já prova a posse do endereço. Desenhado para onboarding dirigido por agente LLM.

Auth: Nenhuma

Request:

json
{
  "email": "dono@empresa.com"
}
Campo Tipo Obrigatório Descricao
email string sim Email do owner (deve ser válido)

Resposta 201:

json
{
  "company_id": 42,
  "api_key": "ctc_xxxx...xxxx",
  "email": "dono@empresa.com",
  "password": "a1b2c3d4e5f6g7h8",
  "base_url": "https://api.catcher.one",
  "message": "account created — credentials sent to your email"
}
Campo Tipo Descricao
company_id integer ID numerico da empresa criada
api_key string API token bootstrap ctc_... — usar em X-API-Key
email string Email cadastrado
password string Senha gerada automaticamente (16 chars hex) — também enviada por email
base_url string Base URL da API
message string Mensagem de confirmação

Erros:

Status Código Quando
400 INVALID_JSON Corpo não e JSON válido
400 BAD_REQUEST Email ausente ou inválido
409 EMAIL_ALREADY_REGISTERED Email já usado por outra conta
500 INTERNAL_ERROR Falha ao provisionar plano, empresa, token ou tenant DB

POST/v1/auth/login#

Autêntica um usuário e retorna JWT.

Auth: Nenhuma

Request:

json
{
  "email": "dono@empresa.com",
  "password": "MinhaSenh@123"
}

Resposta 200:

json
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "company_id": 1,
  "user_id": 1,
  "role": "owner",
  "email": "dono@empresa.com",
  "name": "Joao Silva",
  "is_superadmin": false,
  "email_verified": true
}

Cookies de sessão enviados no login bem-sucedido:

  • biazap_refresh_token: HttpOnly, usado para rotação de sessão
  • biazap_csrf_token: usado no header X-CSRF-Token em requests mutaveis quando o cookie de refresh estiver presente

Protecoes adicionais:

  • lockout por conta após 5 falhas em 15 minutos
  • JWT HS256 com jti e revogacao em logout
  • refresh token com rotação a cada uso

Erros:

Status Código Quando
400 INVALID_JSON Corpo não e JSON válido
400 MISSING_FIELD email ou password ausente
401 INVALID_CREDENTIALS Email ou senha incorretos
429 ACCOUNT_LOCKED Conta bloqueada após 5 falhas em 15 minutos
503 SERVICE_UNAVAILABLE Serviço de autenticação indisponível (Redis)

GET/v1/auth/oauth/{provider}/start#

Inicia o login/cadastro social via OAuth 2.0. provider aceita google ou github.

Auth: Nenhuma

Query params:

Campo Tipo Obrigatório Descricao
mode string não login ou signup. Default: login

Resposta 302: redireciona para o provedor com state, redirect_uri, client_id, response_type=code e escopos:

  • Google: openid email profile
  • GitHub: read:user user:email

Erros JSON:

  • 400 OAUTH_PROVIDER_INVALID - provider diferente de google ou github
  • 503 OAUTH_PROVIDER_DISABLED - provider sem OAUTH_<PROVIDER>_ENABLED=true, CLIENT_ID ou CLIENT_SECRET
  • 503 SERVICE_UNAVAILABLE - Redis indisponível para armazenar o state

Quando o request e uma navegação de browser (Accept: text/html), esses erros redirecionam para /login?oauth_error=<ERROR_CODE> em vez de devolver JSON.


GET/v1/auth/oauth/{provider}/callback#

Callback configurado no Google/GitHub. O backend válida state, troca code por access token, busca o perfil e exige email verificado.

Auth: Nenhuma

Fluxo:

  • Identidade OAuth existente: emite a mesma sessão do POST /v1/auth/login e redireciona para /oauth/callback?provider=....
  • Email verificado já cadastrado: vincula automaticamente o provider ao usuário existente, emite sessão e redireciona para /oauth/callback?provider=....
  • Email novo: cria um token pendente curto no Redis e redireciona para /oauth/complete?provider=...&token=....

Cookies de sessão: quando o callback conclui login de usuário existente, envia biazap_refresh_token e biazap_csrf_token com as mesmas regras do login por senha.

Erros via redirect: falhas redirecionam para /login?oauth_error=<ERROR_CODE>&provider=<provider>.

Código Quando ocorre
OAUTH_STATE_INVALID state ausente, expirado ou de outro provider
OAUTH_EMAIL_UNVERIFIED Provider não retornou email verificado
OAUTH_UPSTREAM_FAILED Falha no token exchange ou profile fetch
OAUTH_PROVIDER_INVALID Provider desconhecido
OAUTH_PROVIDER_DISABLED Provider desabilitado ou sem credenciais

POST/v1/auth/oauth/complete-signup#

Finaliza cadastro social novo pedindo apenas o nome da empresa. Cria empresa, tenant DB, usuário owner e vinculo OAuth; em seguida emite sessão browser.

Auth: Nenhuma

Request:

json
{
  "token": "pending_oauth_token",
  "company_name": "Minha Empresa"
}

Resposta 200: mesmo payload de POST /v1/auth/login, mais cookies biazap_refresh_token e biazap_csrf_token.

Erros:

  • 400 OAUTH_PENDING_INVALID - token pendente inválido ou expirado
  • 400 BAD_REQUEST - company_name inválido
  • 409 EMAIL_ALREADY_REGISTERED - email foi cadastrado antes da conclusao
  • 409 CONFLICT - nome/slug da empresa já existe
  • 500 REGISTRATION_FAILED - falha ao provisionar empresa, usuário, token ou tenant

Variaveis de ambiente OAuth:

Variavel Descricao
OAUTH_FRONTEND_REDIRECT_URL Base do Console para redirects (https://app.catcher.one)
OAUTH_GOOGLE_ENABLED / OAUTH_GITHUB_ENABLED Habilita explicitamente cada provider (true/false)
OAUTH_GOOGLE_CLIENT_ID / OAUTH_GOOGLE_CLIENT_SECRET Credenciais Google
OAUTH_GOOGLE_REDIRECT_URL Callback cadastrado no Google (https://api.../v1/auth/oauth/google/callback)
OAUTH_GITHUB_CLIENT_ID / OAUTH_GITHUB_CLIENT_SECRET Credenciais GitHub
OAUTH_GITHUB_REDIRECT_URL Callback cadastrado no GitHub (https://api.../v1/auth/oauth/github/callback)

*_AUTH_URL, *_TOKEN_URL, *_USER_INFO_URL e OAUTH_GITHUB_EMAILS_URL existem para testes/overrides; em produção os defaults oficiais são usados.


POST/v1/auth/refresh#

Rotaciona o refresh token e devolve um novo access token JWT.

Auth: Cookie biazap_refresh_token

Headers recomendados:

http
X-CSRF-Token: <valor_do_cookie_biazap_csrf_token>

Resposta 200: mesmo payload de POST /v1/auth/login.

Erros:

Status Código Quando
401 REFRESH_FAILED Refresh token ausente, inválido, expirado, ou usuário não existe
403 CSRF_INVALID Header X-CSRF-Token ausente ou divergente do cookie (rejeitado no middleware quando ha cookie de refresh)
500 INTERNAL_ERROR Falha ao reemitir a sessão

POST/v1/auth/logout#

Revoga o JWT atual, inválida o refresh token e limpa os cookies de sessão.

Auth: JWT em Authorization: Bearer <token>

Headers recomendados:

http
X-CSRF-Token: <valor_do_cookie_biazap_csrf_token>

Resposta 204: Sem corpo.


POST/v1/auth/forgot-password#

Solicita a recuperacao de senha. Sempre devolve 202 independentemente de o email existir (anti-enumeracao). Quando o email pertence a um usuário, gera um token de uso único, persiste apenas o hash SHA256 dele e envia um link /reset-password?token=... por email. O token bruto nunca e persistido.

Auth: Nenhuma

Request:

json
{
  "email": "dono@empresa.com"
}
Campo Tipo Obrigatório Descricao
email string sim Email da conta

Resposta 202:

json
{ "message": "if an account exists for that email, a reset link was sent" }

A resposta e sempre 202, mesmo para um email inexistente — não ha como diferenciar pelo retorno se a conta existe. O link de reset expira conforme PASSWORD_RESET_TTL (default 30 minutos) e e de uso único.

Erros:

Status Código Quando
400 INVALID_JSON Corpo não e JSON válido
400 MISSING_FIELD email ausente
500 INTERNAL_ERROR Falha ao gerar ou persistir o token de reset

POST/v1/auth/reset-password#

Consome o token de uso único recebido por email e define uma nova senha. O token e marcado como usado imediatamente (replay impossível). Em sucesso, revoga todas as sessões ativas (refresh tokens) e bumpa password_changed_at, invalidando qualquer JWT pendente.

Auth: Nenhuma

Request:

json
{
  "token": "abc123...def",
  "password": "MinhaNovaSenh@123"
}
Campo Tipo Obrigatório Descricao
token string sim Token bruto recebido no link de email
password string sim Nova senha (mínimo 12 caracteres)

Resposta 200:

json
{ "message": "password updated — you can now log in" }

Erros:

Status Código Quando
400 INVALID_JSON Corpo não e JSON válido
400 MISSING_FIELD token ou password ausente
400 BAD_REQUEST password com menos de 12 caracteres
400 RESET_TOKEN_INVALID Token desconhecido, já usado, ou usuário não existe
400 RESET_TOKEN_EXPIRED Token expirou — solicite um novo
500 INTERNAL_ERROR Falha ao validar token ou atualizar senha

POST/v1/auth/verify-email#

Válida o código de 6 dígitos enviado por email no momento do registro (ou por resend). Ao sucesso, marca users.email_verified_at = NOW(), limpa a state de verificação e reemite a sessão (mesmos cookies de POST /v1/auth/login). A resposta é idêntica ao login, mais o campo email_verified: true.

Auth: JWT em Authorization: Bearer <token> (+ CSRF header quando via browser)

Request:

json
{ "code": "428193" }

Resposta 200: Idêntico ao login.

json
{
  "token": "<JWT>",
  "company_id": 42,
  "user_id": 118,
  "role": "owner",
  "email": "dono@empresa.com",
  "name": "Joao Silva",
  "is_superadmin": false,
  "email_verified": true
}

Erros:

Status Código Quando
400 EMAIL_VERIFICATION_CODE_INVALID Código com formato errado, ou não bate com o persistido. Contador email_verification_attempts é incrementado.
400 EMAIL_VERIFICATION_CODE_EXPIRED Código expirou (TTL de 30 minutos). Peça resend e tente de novo.
429 EMAIL_VERIFICATION_MAX_ATTEMPTS 5 tentativas erradas consecutivas — a state é limpa, obrigando resend.
409 EMAIL_ALREADY_VERIFIED Usuário já verificado (ex: outro tab). Cliente pode seguir direto.

POST/v1/auth/verify-email/resend#

Gera um novo código (invalidando qualquer anterior) e dispara o email via Resend. Respeita cooldown de 60 segundos entre requisições.

Auth: JWT em Authorization: Bearer <token>

Resposta 202:

json
{
  "message": "verification code sent",
  "cooldown_secs": 60,
  "expires_in": 1800
}

Erros:

Status Código Quando
429 EMAIL_VERIFICATION_RESEND_COOLDOWN Enviou faz menos de 60 segundos. Retry-After header diz quantos segundos faltam.
409 EMAIL_ALREADY_VERIFIED Nada a reenviar.

PATCH/v1/auth/me#

Edita o nome do usuário autenticado e/ou o nome da empresa. Ambos os campos são opcionais (sem nenhum = no-op 200). company_name só é honrado para role=owner.

Auth: JWT em Authorization: Bearer <token>

Request:

json
{
  "name": "Renata Schelbauer",
  "company_name": "Catcher LTDA"
}

Resposta 200:

json
{
  "user":    { "id": 12, "email": "renata@catcher.one", "name": "Renata Schelbauer", "role": "owner" },
  "company": { "id": 9,  "name": "Catcher LTDA", "slug": "catcher-original-slug" }
}

Slug não muda. Mesmo se você renomear a empresa, o slug permanece o original (compatibilidade com integrações externas).

Erros:

Status Código Quando
400 BAD_REQUEST Campo vazio (após trim) ou maior que 255 caracteres
403 OWNER_ONLY Usuário não-owner tentou setar company_name

PATCH/v1/auth/me/password#

Troca a senha do usuário autenticado. Exige a senha atual + nova com mínimo 8 caracteres. Em sucesso, revoga todas as sessões ativas (refresh tokens) e bumpa password_changed_at (que inválida o JWT atual via claim pwd_changed_at). O JWT atual continua válido até expirar (~15min); no próximo refresh o usuário é forçado a re-logar.

Auth: JWT em Authorization: Bearer <token>

Request:

json
{
  "current_password": "MinhaSenhaAtual",
  "new_password": "MinhaNovaSenha"
}

Resposta 200:

json
{ "message": "password updated", "requires_relogin": true }

Erros:

Status Código Quando
400 PASSWORD_TOO_SHORT new_password com menos de 8 caracteres
400 MISSING_FIELD current_password vazio
401 INVALID_CURRENT_PASSWORD Senha atual incorreta

POST/v1/auth/me/email/start#

Solicita troca de email. Exige o novo email + a senha atual. Envia código de 6 dígitos para o novo email + notificação de aviso para o email antigo. Token persiste em Redis por 30min; uma segunda chamada antes da confirmação sobrescreve a primeira (last-write-wins).

Auth: JWT em Authorization: Bearer <token>

Request:

json
{
  "new_email": "renata.new@example.com",
  "current_password": "MinhaSenhaAtual"
}

Resposta 202:

json
{ "message": "verification code sent to the new email", "expires_in": 1800 }

Erros:

Status Código Quando
400 BAD_REQUEST Email inválido ou igual ao email atual
400 MISSING_FIELD new_email ou current_password vazio
401 INVALID_CURRENT_PASSWORD Senha atual incorreta
409 EMAIL_ALREADY_TAKEN new_email já está em uso por outra conta

POST/v1/auth/me/email/confirm#

Confirma o código de 6 dígitos recebido no novo email. Em sucesso, troca users.email no DB e revoga todos os refresh tokens. Cliente deve descartar a sessão e redirecionar para /login.

Auth: JWT em Authorization: Bearer <token>

Request:

json
{ "code": "123456" }

Resposta 200:

json
{ "message": "email updated — please log in again", "requires_relogin": true }

Erros:

Status Código Quando
400 EMAIL_CHANGE_CODE_INVALID Código não bate. Token continua válido até 5 tentativas.
404 EMAIL_CHANGE_NOT_FOUND Sem start prévio ou TTL expirou
429 EMAIL_CHANGE_LOCKED 5 tentativas erradas; token foi destruído. Reinicie com start