Vínculo do usuário com serviço externo — API
Gerencia o vínculo entre um usuário e uma integração externa nomeada (instant, laudite ou
iara) — um token de serviço opaco, ativo/inativo, com expiração opcional — e expõe um endpoint
interno, autenticado por segredo compartilhado, para que o próprio serviço externo resolva
esse token de volta para um userId da Identity.
Funcionamento
- Listar (
GET /users/:userId/external-services): o próprio usuário sempre pode ver seus vínculos; para ver os de um terceiro, o ator precisa ser administrador de plataforma. - Registrar/atualizar (
PUT /users/:userId/external-services/:serviceName): cria o vínculo se não existir, ou atualiza os campos enviados sobre o vínculo existente (campos omitidos mantêm o valor atual —undefinedpreserva,nulllimpa). Oservice_tokenem claro nunca é persistido nem devolvido: só o hash SHA-256 é gravado, e a resposta expõe apenashas_token: true/false. serviceNamesó aceita os valores do catálogo fixo (instant,laudite,iara); qualquer outro valor é rejeitado antes de tocar o banco.- Resolver (
GET /internal/v1/external-services/:serviceName/resolve): rota sem guard de OAuth — a autenticação é o headerX-Service-Tokencomparado (por hash) contra o vínculo ativo daquele serviço. Um vínculo inativo, expirado, ou de um serviço diferente, sempre resulta em "não encontrado" (nunca distingue o motivo).
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| GET | /v1/users/:userId/external-services | Lista os vínculos de um usuário |
| PUT | /v1/users/:userId/external-services/:serviceName | Cria ou atualiza um vínculo |
| GET | /internal/v1/external-services/:serviceName/resolve | Resolve um token de serviço para um userId (uso S2S) |
Versão: v1 (as duas primeiras) · VERSION_NEUTRAL com v1 fixo no path (a última)
Swagger: Identity — User administration and profile e Identity — Internal external-service resolution · Rota (Dev): http://localhost:3000/v1/users/:userId/external-services
Permissões
| Rota | Guards | Regra de acesso |
|---|---|---|
GET /users/:userId/external-services | IdentityOAuthAccessTokenGuard | próprio usuário, ou administrador de plataforma para terceiro |
PUT /users/:userId/external-services/:serviceName | idem | idem |
GET /internal/v1/external-services/:serviceName/resolve | nenhum guard OAuth | header X-Service-Token válido para o serviço nomeado |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim (rotas /users/...) | Bearer <access_token> |
X-Service-Token | Sim (rota /internal/...) | token opaco do serviço externo (comparado por hash) |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
userId | uuid | Sim (GET/PUT de /users) | Usuário dono do vínculo |
serviceName | instant | laudite | iara | Sim | Nome da integração |
Body
PUT /users/:userId/external-services/:serviceName — IdentityUserExternalServiceRequest:
json{ "is_active": true, "service_token": "opaque-service-token", "expires_at": null, "extra_config": { "environment": "production" } }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
is_active | boolean | Sim | — |
service_token | string | null | Não | 1–500 caracteres; null limpa o token; omitido mantém o atual |
expires_at | string (ISO 8601) | null | Não | null remove expiração; omitido mantém a atual |
extra_config | objeto JSON | null | Não | objeto plano, sem ciclos, serializado até 16.384 caracteres |
Response
200 — IdentityUserExternalServiceResponse:
json{"id": "8e061c1d-...-52f90", "service_name": "laudite", "is_active": true,"has_token": true, "activated_at": "2026-08-26T12:00:00.000Z", "expires_at": null}
200 — resolução interna:
json{ "userId": "8e061c1d-b2fa-4f55-92ee-54208e152f90" }
Erros
| Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
| ValidationError | IDENTITY_EXTERNAL_SERVICE_INVALID | 400 | serviceName fora do catálogo (instant/laudite/iara) |
| ValidationError | IDENTITY_EXTERNAL_SERVICE_TOKEN_INVALID | 400 | service_token fora de 1–500 caracteres |
| ValidationError | IDENTITY_EXTERNAL_SERVICE_CONFIGURATION_INVALID | 400 | extra_config não é objeto JSON plano ou excede o tamanho serializado |
| ValidationError | IDENTITY_EXTERNAL_SERVICE_EXPIRATION_INVALID | 400 | expires_at não é uma data válida |
ForbiddenAction | IDENTITY_EXTERNAL_SERVICE_FORBIDDEN | 403 | ator não é o dono nem administrador de plataforma |
IdentityUserNotFoundError | IDENTITY_USER_NOT_FOUND | 404 | usuário alvo inativo |
| — (validação de request) | BAD_REQUEST | 400 | header X-Service-Token ausente na rota interna |
IdentityExternalServiceBindingNotFoundError | IDENTITY_EXTERNAL_SERVICE_BINDING_NOT_FOUND | 404 | token não corresponde a um vínculo ativo e válido daquele serviço |
| — | IDENTITY_INVALID_OAUTH_ACCESS_TOKEN | 401 | token ausente, inválido, expirado ou revogado (rotas /users/...) |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Token de serviço nunca é devolvido | resposta expõe só has_token; persistência guarda apenas o hash SHA-256 |
| RN-02 | Campo omitido preserva, null limpa | distinção feita a nível de undefined vs. null no corpo da requisição |
| RN-03 | Catálogo de serviços é fixo no código | instant, laudite, iara — qualquer outro nome é rejeitado antes de qualquer acesso a dado |
| RN-04 | Resolução nunca distingue o motivo da falha | token errado, vínculo inativo, expirado ou usuário inativo resultam todos em 404 |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD/HIPAA | segredo de integração nunca em claro | apenas o hash do token é armazenado; resposta nunca inclui o valor |
Variáveis de ambiente
Nenhuma específica a esta rota identificada no código (o segredo é por vínculo, não por variável de ambiente global).
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | PUT é idempotente para o mesmo payload |
| Auditoria | Sim — identity.user.external_service.updated |
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026. - 📂 Módulo: Usuário