Auth (Registro e Login)

2. 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": "bza_xxxx...xxxx",
  "api_token": "bza_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 bza_... — 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 (bza_...), não um JWT de sessão do browser. Para chamadas subsequentes, envie via X-API-Key: bza_... (ver seção Autenticação acima). Para iniciar a sessão web, chame POST /v1/auth/login.

Erros:

Status Código Body Descricao
400 VALIDATION_ERROR {"error":"password must be at least 12 characters","error_code":"VALIDATION_ERROR"} Campos faltando ou senha curta
409 EMAIL_ALREADY_REGISTERED {"error":"email already registered","error_code":"EMAIL_ALREADY_REGISTERED"} Email já usado por outra empresa
409 SLUG_TAKEN {"error":"company name already taken","error_code":"SLUG_TAKEN"} Slug do company_name já existe

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/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",
  "is_superadmin": false
}

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:

  • 401 - Credenciais inválidas
  • 403 - Empresa suspensa
  • 429 - Conta temporariamente bloqueada por excesso de tentativas

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:

  • 401 - Refresh token ausente, inválido ou expirado
  • 403 - Falha de CSRF quando houver cookie de refresh

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/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",
  "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 contínua 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 contínua 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