Convite de usuário — API
Cria, inspeciona, aceita, reenvia e revoga um convite por e-mail que atribui um ou mais perfis (roles) a um usuário — novo ou já existente — em uma unidade organizacional. É o ponto de entrada para criar conta por convite (em vez de cadastro direto por Gerenciar usuário ou Usuário por tenant). Um worker de fundo expira automaticamente convites pendentes vencidos.
Funcionamento
- Criar (
POST /invitations): exige que o ator tenha, na unidade do convite,user:writeerole:manage. Recusa se já existe um convite pendente para o mesmo e-mail normalizado (unicidade garantida também por índice parcial no banco). Determina o modo do convite comparando o e-mail com o diretório de usuários ativos:new_account(não existe perfil ativo com esse e-mail — o aceite vai criar a conta) ouprofile_grant(já existe um usuário ativo com esse e-mail — o aceite só atribui os perfis, sem pedir senha). Envia um e-mail com um token de uso único. - Inspecionar (
GET /invitations/accept/:token): rota pública, com rate limit; devolve modo, status e o e-mail mascarado (a***@dominio.com) — nunca o e-mail completo. Um convite pendente cujo prazo já passou é reportado comoexpiredmesmo antes do worker de expiração rodar. - Aceitar (
POST /invitations/accept/:token): rota pública, com rate limit mais restrito.- Convite inexistente, revogado ou expirado → recusado.
- Convite já aceito → idempotente: devolve o mesmo
invitationId/userIdcomalready_accepted: true, sem duplicar nada. - Convite
pending: se o modo énew_account,display_nameepasswordsão obrigatórios e uma conta nova é criada (com e-mail já marcado como verificado, já que veio de convite); se éprofile_grant, esses dois campos não são necessários. Em ambos os casos, cadarole_iddo convite é atribuído ao usuário (na unidade do convite, ou no tenant, se o convite não tinha unidade).
- Reenviar (
POST /invitations/:invitationId/resend): só convites pendentes e não vencidos; respeita um cooldown configurável desde o último reenvio/criação — reenviar antes do cooldown terminar é recusado. Gera um novo token (o anterior deixa de valer) e uma nova data de expiração. - Revogar (
DELETE /invitations/:invitationId): só convites pendentes; muda o status pararevokede gravarevokedAt. - Reenviar e revogar são escopados às unidades onde o ator tem as duas permissões
(
user:writeerole:manage); um administrador de plataforma vê/gerencia qualquer convite. - Worker de expiração (
IdentityInvitationExpirationWorker, a cada 60s): move todo convitependingcujo prazo já passou paraexpired, com evento de auditoria — mesmo que ninguém tenha consultado ou tentado aceitá-lo.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/invitations | Cria um convite |
| GET | /v1/invitations/accept/:token | Inspeciona um convite antes de aceitar (público) |
| POST | /v1/invitations/accept/:token | Aceita o convite e materializa a conta/perfis (público) |
| POST | /v1/invitations/:invitationId/resend | Reenvia um convite pendente |
| DELETE | /v1/invitations/:invitationId | Revoga um convite pendente |
Versão: v1
Swagger: Identity — Invitations · Rota (Dev): http://localhost:3000/v1/invitations
Fluxo completo, do convite ao aceite:
Permissões
| Rota | Guards | Permissão / escopo exigido |
|---|---|---|
POST /invitations | IdentityOAuthAccessTokenGuard, AuthorizationGuard | user:write e role:manage, escopo unit (unit_id do corpo) |
GET/POST /invitations/accept/:token | nenhum (público) | posse do token do convite |
POST /invitations/:invitationId/resend | IdentityOAuthAccessTokenGuard | user:write e role:manage em alguma unidade que alcance o convite (ou platform-admin) |
DELETE /invitations/:invitationId | IdentityOAuthAccessTokenGuard | idem |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim (criar/reenviar/revogar) | Bearer <access_token> |
Content-Type | Sim (POST) | application/json |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
token | string | Sim (GET/POST accept) | token de convite recebido por e-mail (1–512 caracteres) |
invitationId | uuid | Sim (resend/DELETE) | identificador do convite |
Body
POST /invitations — IdentityInvitationCreateRequest:
json{"email": "novo.usuario@exemplo.com","role_ids": ["8e061c1d-b2fa-4f55-92ee-54208e152f90"],"unit_id": "0192f3c6-44e8-7a32-a6ef-dad4679fe83b"}
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
email | string | Sim | formato de e-mail |
role_ids | uuid[] | Sim | ao menos 1 item; cada perfil deve existir e estar ativo |
unit_id | uuid | Sim | unidade onde o(s) perfil(is) serão atribuídos |
POST /invitations/accept/:token — IdentityInvitationAcceptRequest:
json{ "display_name": "Novo Usuário", "password": "SenhaForte123!", "client_id": "web-portal" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
display_name | string | Sim se mode: new_account | 1–255 caracteres |
password | string | Sim se mode: new_account | 8–128 caracteres |
client_id | string | Sim | 1–128 caracteres |
Response
200 — criação (IdentityInvitationResponse):
json{ "id": "8e061c1d-...-52f90", "expires_at": "2026-08-27T12:00:00.000Z", "mode": "new_account", "status": "pending" }
200 — inspeção (sem id, com masked_email):
json{ "masked_email": "n***@exemplo.com", "mode": "new_account", "status": "pending", "expires_at": "2026-08-27T12:00:00.000Z" }
200 — aceite (IdentityInvitationAcceptanceResponse):
json{ "invitation_id": "8e061c1d-...-52f90", "user_id": "8f2a...-uuid", "already_accepted": false }
200 — reenvio/revogação (IdentityInvitationAcknowledgementResponse):
json{ "message": "Invitation resent." }
Erros
| Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
ForbiddenAction | IDENTITY_INVITATION_FORBIDDEN | 403 | ator sem user:write e role:manage combinados na unidade exigida |
IdentityInvitationUnavailableError | IDENTITY_INVITATION_UNAVAILABLE | 400 | e-mail já com convite pendente; perfil inexistente/inativo; token inexistente/revogado/expirado; aceite sem senha/nome quando exigido; reenvio fora da janela (não pendente, vencido ou em cooldown); revogação de convite não pendente |
| — (validação de payload) | BAD_REQUEST | 400 | corpo inválido (e-mail malformado, role_ids vazio etc.) |
| — | RATE_LIMIT_EXCEEDED | 429 | limite de requisições excedido em GET/POST accept |
| — | IDENTITY_INVALID_OAUTH_ACCESS_TOKEN | 401 | token ausente/inválido nas rotas autenticadas |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Só um convite pendente por e-mail | criar um segundo convite para o mesmo e-mail enquanto o primeiro está pendente é recusado |
| RN-02 | Modo é decidido na criação, pelo estado do e-mail naquele instante | new_account se não há usuário ativo com esse e-mail; profile_grant caso contrário |
| RN-03 | Aceitar um convite já aceito é idempotente | não recria conta nem duplica atribuição de perfil; devolve o mesmo resultado da primeira vez |
| RN-04 | E-mail nunca aparece completo antes do aceite | GET/inspeção só devolve e-mail mascarado |
| RN-05 | Conta criada por convite já nasce com e-mail verificado | dispensa o fluxo de verificação de e-mail do primeiro acesso |
| RN-06 | Reenvio respeita um cooldown | reenviar antes do intervalo configurado terminar é recusado, mesmo que o convite continue pendente |
| RN-07 | Expiração é ativa, não só "lida sob demanda" | um worker de fundo marca expired os convites vencidos independentemente de qualquer consulta |
| RN-08 | Revogação só se aplica a convite pendente | convite já aceito, expirado ou revogado não pode ser revogado de novo |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD | minimização na exposição pública | e-mail sempre mascarado antes do aceite; token de uso único, não reaproveitável |
| HIPAA/ANVISA (indireto) | rastreabilidade de concessão de acesso | toda criação/aceite/reenvio/revogação/expiração gera evento de auditoria com o ator (ou identity-scheduler para expiração automática) |
Variáveis de ambiente
| Variável | Uso | Obrigatória |
|---|---|---|
Config identity.invitation.tokenTtlInHours | validade do convite (criação e reenvio) | Sim |
Config identity.invitation.resendCooldownInMinutes | intervalo mínimo entre reenvios | Sim |
Config identity.invitation.acceptUrl | base da URL enviada no e-mail de convite | Sim |
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | Aceitar um convite já aceito é idempotente; criar/reenviar/revogar não são |
| Rate limit | GET accept/:token: 20/60s · POST accept/:token: 10/60s |
| Auditoria | Sim — identity.invitation.created, .accepted, .resent, .revoked, .expired |
Relacionado
- 🖥️ Tela: Aceitar convite por e-mail — cobre
GET/POST /v1/invitations/accept/:token. Criar, reenviar e revogar convite não têm tela nesta branch. - 📂 Módulo: Usuário
- 👥 Usuário por tenant — via de criar usuário sem passar por convite
- 🔑 Login — a conta criada aqui autentica pelo fluxo padrão de login