Profile Server API
Endpoints

Organization Tenants

Vínculo M:N entre cooperativa (organization) e tenant — base do escopo de leitura por tenant.

Define quais tenants alcançam cada cooperativa. É o que faz a trava de acesso funcionar: um usuário só enxerga as cooperativas (e os documentos delas) dos tenants dos quais faz parte. Enquanto uma cooperativa não tiver vínculo com nenhum tenant do usuário, ela permanece invisível para ele.

Desde 2026-09-03 esse vínculo também governa as chamadas app-to-app: um token de serviço só alcança as cooperativas vinculadas ao seu próprio tenant (ver Escopo por organização).

O mesmo vínculo é gerenciado por dois contextos, com regras diferentes:

  • Administrativo (/api/*) — o tenant vem da rota, então um administrador vincula qualquer cooperativa a qualquer tenant. Exclusivo da persona global.
  • App-to-app (/service/*) — o tenant é sempre o do chamador (resolvido do token de serviço pelo ServiceAuthMiddleware) e nunca vem no corpo. Um serviço só vincula/desvincula cooperativas ao próprio tenant. É como um serviço adquire alcance, e por isso essas rotas consultam a base inteira.
MétodoRotaAuthAção (role)SucessoIdempotente
POST/api/tenants/:tenantId/organizationsusuárioorganization-tenants:create201sim
DELETE/api/tenants/:tenantId/organizations/:organizationIdusuárioorganization-tenants:delete204sim
GET/api/organizations/:organizationId/tenantsusuárioorganization-tenants:read200—
GET/api/tenants/:tenantId/organizationsusuáriotenants:read200—
POST/service/organizations/:organizationId/tenantsserviço—201sim
DELETE/service/organizations/:organizationId/tenantsserviço—204sim

Controle de acesso por persona. As rotas /api deste módulo são exclusivas da persona global (decisão de produto de 2026-09-04): o vínculo define o alcance de um tenant, no mesmo nível de Tenants — quem administra um tenant não amplia o próprio alcance. tenant-admin está em TENANT_ADMIN_EXCLUDED_MODULES, e nenhuma outra persona tem o módulo na matriz. Liberação pontual só por role de exceção (organization-tenants:create), e modulo:acao:deny bloqueia. Token sem isso → 403 NOT_ALLOWED. As rotas /service/* não passam por esse controle.

A leitura pelo lado do tenant (GET /api/tenants/:tenantId/organizations) é anterior a este módulo e continua sob tenants:read — ver Tenants.


Vincular cooperativa ao tenant (admin)

POST /api/tenants/:tenantId/organizations

Passa a dar ao tenant alcance sobre a cooperativa informada. Idempotente: se o vínculo já existir, responde 201 do mesmo jeito, sem duplicar.

Params

ParamTipoRegras
tenantIduuidTenant existente (404 TENANT_NOT_FOUND se não existir)

Corpo

CampoTipoObrigatórioRegras
organizationIduuidsimCooperativa existente (404 ORGANIZATION_NOT_FOUND se não existir)
curl -X POST \
  http://localhost:3000/api/tenants/$TENANT_ID/organizations \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "organizationId": "'$ORGANIZATION_ID'" }'

201 Created

{
  "organizationId": "o1b2c3d4-e5f6-4789-a123-456789abcdef",
  "tenantId": "t1b2c3d4-e5f6-4789-a123-456789abcdef"
}

Erros

StatuscodeQuando
400VALIDATION_ERRORorganizationId ausente ou não-uuid
403NOT_ALLOWEDPersona sem organization-tenants:create
404ORGANIZATION_NOT_FOUNDCooperativa não encontrada
404TENANT_NOT_FOUNDTenant não encontrado

Desvincular cooperativa do tenant (admin)

DELETE /api/tenants/:tenantId/organizations/:organizationId

O tenant deixa de alcançar a cooperativa. Idempotente: se não houver vínculo, ainda responde 204.

ParamTipoRegras
tenantIduuidTenant existente (404 TENANT_NOT_FOUND se não existir)
organizationIduuidCooperativa existente (404 ORGANIZATION_NOT_FOUND se não existir)
curl -X DELETE \
  http://localhost:3000/api/tenants/$TENANT_ID/organizations/$ORGANIZATION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"

204 No Content — vínculo removido (ou já inexistente).

Erros

StatuscodeQuando
403NOT_ALLOWEDPersona sem organization-tenants:delete
404ORGANIZATION_NOT_FOUNDCooperativa não encontrada
404TENANT_NOT_FOUNDTenant não encontrado

Desvincular derruba também os escopos por usuário daquele par: as linhas de user_tenant_organizations que apontavam para esse vínculo são removidas em cascata pelo banco (FK onDelete: Cascade). Um usuário RESTRICTED que só tinha essa cooperativa concedida fica sem nenhuma no tenant — ver User Tenant Organizations.


Listar tenants de uma cooperativa

GET /api/organizations/:organizationId/tenants

Leitura inversa de GET /api/tenants/:tenantId/organizations — quais tenants alcançam esta cooperativa. Alimenta o painel de vínculos na tela de detalhe da cooperativa.

ParamTipoRegras
organizationIduuidCooperativa existente (404 ORGANIZATION_NOT_FOUND se não existir)
curl http://localhost:3000/api/organizations/$ORGANIZATION_ID/tenants \
  -H "Authorization: Bearer $ACCESS_TOKEN"

200 OK

{
  "data": [
    {
      "id": "t1b2c3d4-e5f6-4789-a123-456789abcdef",
      "customerId": "c1b2c3d4-e5f6-4789-a123-456789abcdef",
      "appId": "a1b2c3d4-e5f6-4789-a123-456789abcdef",
      "name": "Acme Tenant",
      "description": null,
      "active": true,
      "metadata": null,
      "keycloakClientId": "acme-tenant",
      "allowedScopes": [],
      "createdAt": "2026-09-04T12:00:00.000Z",
      "updatedAt": null
    }
  ]
}

Erros

StatuscodeQuando
403NOT_ALLOWEDPersona sem organization-tenants:read
404ORGANIZATION_NOT_FOUNDCooperativa não encontrada

Listar cooperativas de um tenant

GET /api/tenants/:tenantId/organizations — retorna as cooperativas alcançadas pelo tenant. Exige tenants:read (não organization-tenants:read): a rota é anterior a este módulo e não foi migrada para não quebrar quem já a consome.

curl http://localhost:3000/api/tenants/$TENANT_ID/organizations \
  -H "Authorization: Bearer $ACCESS_TOKEN"

200 OK

{
  "data": [
    {
      "id": "o1b2c3d4-e5f6-4789-a123-456789abcdef",
      "name": "Cooperativa Recicla Já",
      "alias": null,
      "federalTaxId": "12345678000190",
      "active": true,
      "listed": true
    }
  ]
}

Erros

StatuscodeQuando
404TENANT_NOT_FOUNDTenant não existe

App-to-app (/service)

O tenant é sempre o do chamador, resolvido do token de serviço — nunca vem na rota nem no corpo.

Vincular cooperativa ao tenant autenticado

POST /service/organizations/:organizationId/tenants

curl -X POST \
  http://localhost:3000/service/organizations/$ORGANIZATION_ID/tenants \
  -H "Authorization: Bearer $SERVICE_ACCESS_TOKEN"

201 Created

{
  "organizationId": "o1b2c3d4-e5f6-4789-a123-456789abcdef",
  "tenantId": "t1b2c3d4-e5f6-4789-a123-456789abcdef"
}

Erros

StatuscodeQuando
404ORGANIZATION_NOT_FOUNDCooperativa não encontrada

Vincular enxerga qualquer cooperativa existente, inclusive uma que o tenant ainda não alcança — é justamente para isso que a rota serve (@UnscopedService()). A cooperativa também entra no escopo do usuário de X-Username, se o dele no tenant é restrito. O vínculo feito pelo painel (POST /api/tenants/:tenantId/organizations) não concede nada a ninguém: lá quem vincula é o administrador. Já desvincular opera dentro do escopo: uma cooperativa que não é sua responde 404 ORGANIZATION_NOT_FOUND.

Desvincular cooperativa do tenant autenticado

DELETE /service/organizations/:organizationId/tenants

curl -X DELETE \
  http://localhost:3000/service/organizations/$ORGANIZATION_ID/tenants \
  -H "Authorization: Bearer $SERVICE_ACCESS_TOKEN"

204 No Content — vínculo removido (ou já inexistente).

Erros

StatuscodeQuando
404ORGANIZATION_NOT_FOUNDCooperativa não encontrada