# API REST

URL: /pt/docs/developer/access-management/api

API REST do Access Management — base, autenticação, referência gerada do OpenAPI e exemplos.

# Access Management API [#access-management-api]

O `access-management-api` guarda departamentos, perfis, utilizadores, aplicações e permissões, emite os tokens e gere as sessões — ver o [modelo de controlo de acesso](./modelo-acesso.mdx).

| | |
|---|---|
| **Base (workspace)** | `http://<slug>:2575/gateway-api/<serviço>-access-management` |
| **Prefixo** | `/api` (está no caminho dos recursos) |
| **Referência** | [Gerada do OpenAPI do serviço](./referencia-api/utilizadores.mdx) (springdoc em `/v3/api-docs`) |

O `<serviço>` é o nome com que o serviço está registado no Eureka.

---

## Autenticação [#autenticação]

```http
Authorization: Bearer <token>
```

Nas apps é o token do utilizador: leva as permissões do perfil ativo em `permissions` e o departamento dele em `org` — ver [Claims do JWT](./modelo-acesso.mdx#claims-do-jwt). Entre serviços usa um token `client_credentials` — ver [API M2M](./m2m-api.mdx).

Cada endpoint de gestão exige uma permissão (`igrp.users.view`, `igrp.users.manage`, …), indicada na referência de cada operação.

---

## Referência [#referência]

Cada operação, com a permissão, os parâmetros, o corpo do pedido, as respostas e exemplos em várias linguagens:

<Cards>
  <Card title="Utilizadores" href="/docs/developer/access-management/referencia-api/utilizadores">Utilizadores, os seus perfis, o perfil ativo e as permissões.</Card>
  <Card title="Utilizador autenticado" href="/docs/developer/access-management/referencia-api/utilizador-autenticado">Dados, perfis, permissões e aplicações do próprio utilizador.</Card>
  <Card title="Convites" href="/docs/developer/access-management/referencia-api/convites">Convidar utilizadores e validar o convite.</Card>
  <Card title="Departamentos" href="/docs/developer/access-management/referencia-api/departamentos">Árvore de departamentos e o que está disponível em cada um.</Card>
  <Card title="Perfis" href="/docs/developer/access-management/referencia-api/perfis">Perfis, árvore de perfis e permissões de cada perfil.</Card>
  <Card title="Aplicações" href="/docs/developer/access-management/referencia-api/aplicacoes">Aplicações, menus, recursos e campos personalizados.</Card>
  <Card title="Autorização" href="/docs/developer/access-management/referencia-api/autorizacao">Verificar se um utilizador pode fazer uma ação.</Card>
  <Card title="M2M" href="/docs/developer/access-management/referencia-api/m2m">Sincronização no arranque e consultas entre serviços.</Card>
  <Card title="Sessões" href="/docs/developer/access-management/referencia-api/sessoes">Sessão do utilizador e administração de sessões.</Card>
  <Card title="Clientes OAuth" href="/docs/developer/access-management/referencia-api/clientes-oauth">Clientes OAuth e contas de serviço.</Card>
  <Card title="Ficheiros e configuração" href="/docs/developer/access-management/referencia-api/ficheiros-e-configuracao">Ficheiros e configuração global.</Card>
  <Card title="Auditoria" href="/docs/developer/access-management/referencia-api/auditoria">Eventos de segurança e relatórios.</Card>
</Cards>

<Callout type="info" title="Testar no playground">
O botão **Testar no playground** envia o pedido a partir do browser para o servidor indicado (por omissão, o gateway do workspace em `localhost:2575`). Preenche o `host` e o `servico` do teu ambiente e um token em **Autorização**. O gateway tem de aceitar pedidos (CORS) vindos da origem da documentação; se não aceitar, usa os exemplos `curl`.
</Callout>

---

## Exemplos [#exemplos]

`$AM` é a base do serviço (por exemplo `http://<slug>:2575/gateway-api/<serviço>-access-management`).

### Listar aplicações [#listar-aplicações]

```bash
curl "$AM/api/applications" \
  -H "Authorization: Bearer $TOKEN"
```

### Criar aplicação [#criar-aplicação]

```bash
curl -X POST "$AM/api/applications" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "MINHA_APP",
    "name": "Minha App",
    "type": "INTERNAL",
    "description": "Aplicação de exemplo"
  }'
```

`code`, `name` e `type` (`INTERNAL`, `EXTERNAL` ou `SYSTEM`) são obrigatórios.

### Verificar uma autorização [#verificar-uma-autorização]

```bash
curl -X POST "$AM/api/authorize/check" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "action": "igrp.documento.ler" }'
```

`action` é o código da permissão; `resource` é opcional e identifica o recurso. A resposta diz se é permitido (`allowed`), por que perfis (`viaRoles`) e o motivo (`reason`).

### Trocar o perfil ativo [#trocar-o-perfil-ativo]

```bash
curl -X POST "$AM/api/users/me/roles/active" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "roleCode": "arquivista", "departmentCode": "ARQ" }'
```

A troca revoga as sessões do utilizador: o token seguinte traz as permissões do novo perfil.


# Variáveis de ambiente

URL: /pt/docs/developer/access-management/configuracao

Variáveis de ambiente necessárias em cada componente do sistema de gestão de acessos.

# Configuração — Variáveis de Ambiente [#configuração--variáveis-de-ambiente]

### 4. Configuração — Variáveis de ambiente [#4-configuração--variáveis-de-ambiente]

Quatro intervenientes, quatro superfícies de configuração distintas. Esta secção é a referência canónica para a configuração do framework.

<Tabs>

<TabItem value="am" label="Access Management">

#### 4.1 Access Management (AM) [#41-access-management-am]

**Caminho**: `access-management/backend/access-management-api/src/main/resources/application.properties`.

##### 4.1.1 Base de dados (obrigatório) [#411-base-de-dados-obrigatório]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `DATABASE_URL` | `spring.datasource.url` | — | Alternativamente, use as três variáveis `POSTGRES_*` abaixo. |
| `POSTGRES_HOST` | (composto) | — | Usado quando `DATABASE_URL` não está definida. |
| `POSTGRES_PORT` | (composto) | `5432` | |
| `POSTGRES_DATABASE` | (composto) | — | |
| `DATABASE_USERNAME` / `POSTGRES_USER` | `spring.datasource.username` | — | |
| `DATABASE_PASSWORD` / `POSTGRES_PASSWORD` | `spring.datasource.password` | — | Segredo. |

O Flyway é executado automaticamente durante o arranque da aplicação. Não desative este comportamento, exceto se o ciclo de vida das migrações for gerido externamente.

##### 4.1.2 Cache de sessão (opcional mas recomendado) [#412-cache-de-sessão-opcional-mas-recomendado]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `REDIS_HOST` | `spring.data.redis.host` | — | Se não definida, a cache de sessão recorre a memória interna (apenas para deployments de um único pod). |
| `REDIS_PORT` | `spring.data.redis.port` | `6379` | |
| `REDIS_PASSWORD` | `spring.data.redis.password` | — | Segredo. |

##### 4.1.3 Servidor de autorização OAuth2 — chaves e emissor (obrigatório) [#413-servidor-de-autorização-oauth2--chaves-e-emissor-obrigatório]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `IGRP_OAUTH_ISSUER` | `igrp.oauth.issuer` | `http://localhost:8080` | Correspondência ao carácter com a claim `iss` do JWT. Inclua o prefixo do proxy inverso, se existir (`https://api.example.com/igrp-access-management`). |
| `IGRP_OAUTH_PUBLIC_KEY` | `igrp.oauth.keys.public` | valor por omissão do classpath | Caminho para a chave pública RSA primária. |
| `IGRP_OAUTH_PRIVATE_KEY` | `igrp.oauth.keys.private` | valor por omissão do classpath | Caminho para a chave privada RSA primária. Segredo. |
| `IGRP_OAUTH_KEY_ID` | `igrp.oauth.keys.kid` | `igrp-oauth-key` | Anunciado como `kid` no JWKS e nos JWTs emitidos. |
| `IGRP_OAUTH_SECONDARY_*` | `igrp.oauth.keys.secondary.*` | — | Slot opcional de rotação de chaves sem período de indisponibilidade (pública, privada, kid). |
| `AUTH_JWT_ISSUER` | `spring.security.oauth2.resourceserver.jwt.issuer-uri` | — | Emissor que o próprio AM usa para validar os JWTs recebidos que não emitiu (raro — normalmente igual a `IGRP_OAUTH_ISSUER`). |

Quando o emissor (*issuer*) tiver de ser ajustado: se o AM estiver atrás de um *proxy* que reescreve o URL, `IGRP_OAUTH_ISSUER` deve corresponder ao URL público utilizado pela SPA e pelos backends das aplicações, e não ao URL interno do serviço. Caso contrário, os *resource servers* rejeitarão os tokens devido a uma incompatibilidade no valor da *claim* `iss`.

##### 4.1.4 Cliente OAuth2 por omissão (semeado automaticamente) [#414-cliente-oauth2-por-omissão-semeado-automaticamente]

| Variável de ambiente | Propriedade | Notas |
|---|---|---|
| `IGRP_OAUTH_DEFAULT_CLIENT_ID` | `igrp.oauth.default-client.client-id` | Deixar em branco → sem semear. |
| `IGRP_OAUTH_DEFAULT_CLIENT_SECRET` | `igrp.oauth.default-client.secret` | Segredo. |

Utilizado para inicializar o primeiro cliente OAuth2 numa base de dados vazia, permitindo que o _Application Center_ comunique com o AM.

Na prática, este cliente é configurado apenas uma vez. Após a inicialização, os restantes clientes OAuth2 devem ser geridos através da interface do _Application Center_ (ver [Gestão de Aplicações](/docs/utilizador/access-manager/application)); no workspace local esse registo é automático — ver [Registo de clientes OAuth](/docs/developer/workspace/registo-de-clientes).

##### 4.1.5 Federação com o IdP a montante (opcional — ativa o SSO) [#415-federação-com-o-idp-a-montante-opcional--ativa-o-sso]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `IGRP_OAUTH_EXTERNAL_IDP_ENABLED` | `igrp.oauth.external-idp.enabled` | `true` | Defina como `false` para executar o AM como emissor de identidade autónomo, sem federação a montante. |
| `IGRP_OAUTH_EXTERNAL_IDP_ISSUER_URI` | `igrp.oauth.external-idp.issuer-uri` | — | URL do emissor OIDC do IdP a montante. |
| `IGRP_OAUTH_EXTERNAL_IDP_CLIENT_ID` | `igrp.oauth.external-idp.client-id` | — | Cliente do AM no IdP a montante. |
| `IGRP_OAUTH_EXTERNAL_IDP_CLIENT_SECRET` | `igrp.oauth.external-idp.client-secret` | — | Segredo. |
| `IGRP_OAUTH_LOGOUT_CASCADE` | `igrp.oauth.external-idp.logout-cascade` | `true` | O RP-Initiated Logout propaga-se ao IdP a montante em `/connect/logout`. |
| `IGRP_OAUTH_REQUIRE_EMAIL_VERIFIED` | `igrp.oauth.external-idp.require-email-verified` | `true` | Recusa logins em que o IdP a montante não tenha verificado a claim de email. OWASP A07. |
| `IGRP_OAUTH_ALLOWED_EMAIL_DOMAINS` | `igrp.oauth.allowed-email-domains` | — | Separados por vírgula. P. ex. `nosi.cv,gov.cv`. Vazio = qualquer domínio. |
| `IGRP_OAUTH_REQUIRED_ACR_VALUES` | `igrp.oauth.required-acr-values` | — | Aplica autenticação reforçada (step-up). P. ex. `mfa` exige um ACR com capacidade MFA no IdP a montante. |

##### 4.1.6 CORS [#416-cors]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `IGRP_CORS_ALLOWED_ORIGINS` | `igrp.security.cors.allowed-origins` | `http://localhost:3000,http://localhost:8080` | Separados por vírgula. O wildcard `*` é proibido, porque o AM envia `Access-Control-Allow-Credentials: true`. Liste as origens exatas das SPAs. |

##### 4.1.7 Inicialização do superadministrador [#417-inicialização-do-superadministrador]

| Variável de ambiente | Propriedade | Notas |
|---|---|---|
| `IGRP_SUPERADMIN_EMAIL` | `igrp.superadmin.email` | Email do superadministrador de inicialização. |
| `IGRP_SUPERADMIN_USER_EXTERNAL_ID` | `igrp.superadmin.user-external-id` | O `sub` da mesma pessoa no IdP a montante. Só é usado no primeiro arranque para semear o registo; as pesquisas em runtime são agora baseadas em perfil (`DEPT_IGRP.superadmin`) — ver o histórico do refactor(users). |

##### 4.1.8 Armazenamento (MinIO ou S3) [#418-armazenamento-minio-ou-s3]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `IGRP_STORAGE_PROVIDER` | `igrp.storage-provider` | `minio` | `minio` ou `s3`. |
| `IGRP_STORAGE_ENDPOINT` | `igrp.minio.endpoint` | — | Anfitrião (host). |
| `IGRP_STORAGE_PORT` | `igrp.minio.port` | — | Porta. |
| `IGRP_STORAGE_ACCESS_KEY` | `igrp.minio.access-key` | — | Segredo. |
| `IGRP_STORAGE_SECRET_KEY` | `igrp.minio.secret-key` | — | Segredo. |
| `IGRP_STORAGE_BUCKET_NAME` | `igrp.minio.bucket-name` | `igrp` | |
| `IGRP_STORAGE_PRESIGNED_URL_EXPIRATION_TIME` | `igrp.minio.url-expiration-time` | `300` | |

##### 4.1.9 SMTP + notificações [#419-smtp--notificações]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `IGRP_NOTIFICATIONS_ENABLED` | `igrp.notifications.enabled` | `none` | `none` desativa todo o correio de saída. Defina `smtp` em produção. |
| `SMTP_HOST` | `igrp.mail.host` | — | |
| `SMTP_PORT` | `igrp.mail.port` | `587` | |
| `SMTP_USERNAME` | `igrp.mail.username` | — | |
| `SMTP_PASSWORD` | `igrp.mail.password` | — | Segredo. |
| `IGRP_MAIL_SMTP_AUTH` | `igrp.mail.auth` | `true` | |
| `IGRP_MAIL_SMTP_STARTTLS_ENABLE` | `igrp.mail.starttls.enable` | `true` | |
| `IGRP_MAIL_INVITE_TEMPLATE` | `igrp.mail.invite.template` | — | Modelo HTML para o email de convite. |

##### 4.1.10 Aplicação de sessão [#4110-aplicação-de-sessão]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `IGRP_SESSION_ABSOLUTE_TIMEOUT_SECONDS` | `igrp.session.absolute-timeout-seconds` | `28800` | 8 h. Limite máximo absoluto da sessão, independentemente da atividade. |
| `IGRP_SESSION_MAX_PER_USER` | `igrp.session.max-per-user` | `5` | Sessões ativas simultâneas por utilizador. A mais antiga é terminada quando excedido. |
| `IGRP_SESSION_HEARTBEAT_DEBOUNCE_SECONDS` | `igrp.session.heartbeat-debounce-seconds` | `30` | Intervalo mínimo entre escritas na base de dados do heartbeat de sessão. |
| `IGRP_SESSION_ENFORCEMENT_ENABLED` | `igrp.session.enforcement-enabled` | `true` | `false` desativa as verificações de vivacidade da sessão (o JWT bearer continua a ser validado). |

##### 4.1.11 Observabilidade + sistema [#4111-observabilidade--sistema]

| Variável de ambiente | Propriedade | Valor por omissão | Notas |
|---|---|---|---|
| `SERVICE_PORT` | `server.port` | `8080` | |
| `IGRP_APP_CENTER_URL` | `igrp.app-center.url` | — | URL público do Application Center; usado nos emails de saída (os convites remetem para aqui). |
| `OTEL_COLLECTOR_ENDPOINT` | `otel.exporter.otlp.endpoint` | — | Se definido, os traces e métricas OTEL são enviados para o coletor. |
| `OTEL_DISABLED` | `otel.sdk.disabled` | `true` | Desativado por omissão; defina `false` para ativar em produção. |
| `server.forward-headers-strategy` | (propriedade) | `framework` | Não altere sem compreender o bug de ciclo que já encontrámos; os matchers em `AuthorizationServerConfig` dependem da configuração atual. |

</TabItem>

<TabItem value="ac" label="Application Center">

#### 4.2 Application Center (AC) [#42-application-center-ac]

**Caminho**: `access-management/frontend/application-center/` (ramo dev).

**Referência canónica**: `application-center/.env.example` no ramo dev.

O Application Center (AC) é, por si próprio, um consumidor Next.js do AM. A partir da versão atual do framework disponível na branch `dev`, o AC utiliza o provider `igrp-auth`, autenticando-se diretamente através do AM.

O AM é responsável pela federação com o provedor de identidade (Identity Provider - IdP) configurado no seu lado, como por exemplo Autentika, Keycloak, WSO2, entre outros.

Desta forma, o AC deixou de necessitar de variáveis específicas do IdP, não sendo necessárias configurações como `KEYCLOAK_*` ou `WSO2_*`.

##### Autenticação (obrigatório) [#autenticação-obrigatório]

| Variável de ambiente | Valor por omissão | Notas |
|---|---|---|
| `AUTH_PROVIDER` | — | Definir como `igrp-auth`. A única alternativa atualmente é `none` (desativa a autenticação — apenas dev/demonstração). |
| `IGRP_AUTH_CLIENT_ID` | — | Id do cliente OAuth2 registado no AM para esta instância do AC. Tipicamente `igrp-access-management` para o deployment de plataforma do AC. |
| `IGRP_AUTH_CLIENT_SECRET` | — | Segredo emparelhado com `IGRP_AUTH_CLIENT_ID`. |
| `IGRP_AUTH_ISSUER` | — | URL base do AM, sem barra final. O framework acrescenta `/.well-known/openid-configuration` para descoberta OIDC. Exemplo: `https://api.example.com/igrp-access-management`. Correspondência ao carácter com o `IGRP_OAUTH_ISSUER` do AM. |
| `IGRP_AUTH_SCOPES` | `openid` | Separados por espaço. O documento de descoberta do AM anuncia `scopes_supported: ["openid"]` de raiz — qualquer scope adicional (`email`, `profile`, …) TEM de ser adicionado ao scopes do cliente no AM antes de ser pedido aqui; caso contrário o Spring AS descarta-o ou rejeita-o silenciosamente. |

As seguintes URIs devem ser configuradas no cliente OAuth2/OIDC do AM (ajustar os caminhos conforme o valor de `NEXT_PUBLIC_BASE_PATH`):

- **Callback OAuth:** `${NEXTAUTH_URL_ORIGIN}${NEXT_PUBLIC_BASE_PATH}/api/auth/callback/igrp-auth`
- **Redirecionamento pós-logout:** `${NEXTAUTH_URL_ORIGIN}${NEXT_PUBLIC_BASE_PATH}/login`

A ausência de uma destas configurações origina frequentemente comportamentos como:

- ciclo de redirecionamento contínuo para `/login` após autenticação;
- sessão SSO do provedor de identidade (upstream IdP) mantida após o logout.

A especificação OIDC exige que estes valores tenham correspondência exata byte a byte entre o cliente configurado no AM e os valores enviados pela aplicação.

O provider `igrp-auth` ativa automaticamente PKCE (RFC 7636, S256) e o parâmetro nonce durante o fluxo de autorização com troca de authorization code. Não é necessária qualquer configuração adicional no cliente. No entanto, os seguintes elementos devem permanecer consistentes durante todo o fluxo:

- `NEXTAUTH_URL`;
- `NEXTAUTH_SECRET`;
- caminho configurado no reverse proxy.

Estes valores são utilizados para proteger o fluxo de autenticação, uma vez que os cookies de PKCE, state e nonce são cifrados através de JWE utilizando o `NEXTAUTH_SECRET` e associados ao caminho definido por `NEXTAUTH_URL`. Qualquer inconsistência nestas configurações será identificada durante o callback, originando erros como:

- `PKCE_ERROR`;
- `OAUTH_CALLBACK_ERROR`.

Estes erros ocorrem no lado da aplicação durante o processamento do callback, e não como uma rejeição pelo provedor de identidade (IdP).

##### NextAuth (obrigatório) [#nextauth-obrigatório]

| Variável de ambiente | Valor por omissão | Notas |
|---|---|---|
| `NEXTAUTH_URL` | — | URL da raiz da API do NextAuth — não a raiz da aplicação. O NextAuth v4 deriva `signin`/`callback` a partir deste valor tal como está e não acrescenta automaticamente `/api/auth`. O valor TEM de terminar em `/api/auth` e TEM de incluir `NEXT_PUBLIC_BASE_PATH` quando definido. Raiz: `http://localhost:3000/api/auth`. Subcaminho (`/app-center`): `https://apps.example.com/app-center/api/auth`. Fazer isto mal produz um ciclo de login com uma cadeia `?callbackUrl=…?callbackUrl=…` profundamente aninhada. |
| `NEXTAUTH_SECRET` | — | Segredo JWE que cifra os tokens do cookie de sessão. Gere com `openssl rand -base64 32`. O NextAuth falha de forma irrecuperável no arranque em produção se não estiver definido. Rodar este valor invalida o cookie de sessão existente de todos os utilizadores. |
| `NEXTAUTH_URL_INTERNAL` | igual a `NEXTAUTH_URL` | URL servidor-a-servidor usado dentro do pod / cluster (p. ex. `http://app-center.igrp.svc.cluster.local:3000/api/auth`). Mesma regra de sufixo `/api/auth` que `NEXTAUTH_URL`. Difere de `NEXTAUTH_URL` quando existe um nome de anfitrião interno de serviço além de um público. |

##### Framework IGRP (obrigatório para sincronização; opcional no restante) [#framework-igrp-obrigatório-para-sincronização-opcional-no-restante]

| Variável de ambiente | Valor por omissão | Notas |
|---|---|---|
| `IGRP_ACCESS_MANAGEMENT_API` | — | URL base da API REST do AM. Igual ao valor de `IGRP_AUTH_ISSUER` na grande maioria dos deployments (o AM emite tokens E aloja a API de administração). |
| `IGRP_APP_CODE` | — | Código de aplicação do próprio AC no AM. Fixo em `APP_IGRP_CENTER`. |
| `IGRP_PREVIEW_MODE` | `false` | `true` salta as verificações de autenticação — apenas dev/demonstração. |
| `IGRP_SYNC_ACCESS` | `false` | Interruptor principal do fluxo de sincronização de arranque (aplicação + recursos/rotas + menus definidos no código). O AC vem com este valor a `false` no ramo dev, porque as aplicações e menus do AC são geridos pela UI de administração, não pelo código. Ative apenas se detiver a fonte de verdade code-first para a árvore de menus do AC. |
| `IGRP_SYNC_ON_CODE_MENUS` | `false` | Envio da árvore de menus definida no código. Mesma lógica que `IGRP_SYNC_ACCESS`. |
| `IGRP_SYNC_ON_CODE_MENU_ROLES` | `true` | Quando os menus SÃO sincronizados, se também deve reconciliar as atribuições de perfil referenciadas. Só é considerado quando `IGRP_SYNC_ON_CODE_MENUS=true`. |
| `IGRP_SERVICE_ID` | — | Identidade de serviço enviada como `X-Machine-Service-ID` nas chamadas M2M. Kebab-case, 2–64 carateres. Obrigatório quando `IGRP_SYNC_ACCESS=true`. |

##### Superfícies de navegação específicas do Application Center (opcional) [#superfícies-de-navegação-específicas-do-application-center-opcional]

| Variável de ambiente | Notas |
|---|---|
| `NEXT_IGRP_APP_CENTER_URL` | URL público do seletor de aplicações partilhado. Se não definido, o "chip" do seletor de aplicações fica oculto. |
| `NEXT_PUBLIC_BASE_PATH` | `basePath` do Next.js. Vazio (raiz) ou, por exemplo, `/app-center`. |
| `NEXT_PUBLIC_IGRP_APP_HOME_SLUG` | Rota de destino após login (p. ex. `/dashboard`). Vazio recorre a `/`. |
| `NEXT_PUBLIC_ALLOWED_DOMAINS` | Lista de anfitriões separados por vírgula para o `images.remotePatterns` do `next.config.ts`. |
| `NEXT_PUBLIC_IGRP_PROFILE_URL` | URL externo para a página de perfil centralizada (quando a plataforma executa a sua UI de perfil como um serviço separado). Vazio recorre à versão local integrada. |
| `NEXT_PUBLIC_IGRP_NOTIFICATION_URL` | Mesmo padrão para as notificações. |
| `NEXT_PUBLIC_IGRP_SETTINGS_URL` | Mesmo padrão para as definições. |

##### Modo de pré-visualização / bypass de autenticação [#modo-de-pré-visualização--bypass-de-autenticação]

O framework unifica dois percursos de variáveis de ambiente independentes numa única verificação `isAuthBypass()`:

```bash
IGRP_PREVIEW_MODE=true
# — ou —
AUTH_PROVIDER=none
```

Quando qualquer um está definido:

- O middleware deixa passar todos os caminhos não relacionados com autenticação e redireciona `/login`, `/logout`, `/api/auth/*` para `/`.
- `serverSession()` / `getSession()` devolvem `null`; `verifySession()` devolve uma sessão simulada (stub).
- O construtor de configuração troca para dados simulados de `src/temp/users/`, `src/temp/menus/`, `src/temp/applications/`.
- A reobtenção de sessão do lado do cliente é desativada.
- Todos os controlos de permissão resolvem-se como superadministrador — todas as verificações passam. Use a pré-visualização *off* contra um backend real para ver o comportamento de recusa/403.

Qualquer alteração ao middleware, ao layout raiz ou ao construtor de configuração tem de ser testada com o bypass **LIGADO** e **DESLIGADO**. Ver `docs/AUTHENTICATION.md#preview-mode--auth-bypass` do framework.

</TabItem>

<TabItem value="backend-app" label="Backend da Aplicação">

#### 4.3 Backend da Aplicação [#43-backend-da-aplicação]

A lista exaustiva está em [*Permissions Integration Guide — secção 5*](/docs/developer/framework/backend/permissoes#configuracao). Reproduzida aqui por conveniência:

| Variável de ambiente | Propriedade | Notas |
|---|---|---|
| `SPRING_APPLICATION_NAME` | `spring.application.name` | Regista-se como um Resource no AM. |
| `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` | `spring.security.oauth2.resourceserver.jwt.issuer-uri` | Tem de ser exatamente igual ao `IGRP_OAUTH_ISSUER` do AM. |
| `IGRP_ACCESS_BASE_URL` | `igrp.access.base-url` | URL base do AM. |
| `IGRP_ACCESS_M2M_CLIENT_ID` | `igrp.access.m2m.client-id` | Cliente OAuth2 do backend (criado no AM uma vez por projeto). |
| `IGRP_ACCESS_M2M_CLIENT_SECRET` | `igrp.access.m2m.client-secret` | Segredo. |
| `IGRP_ACCESS_M2M_SCOPE` | `igrp.access.m2m.scope` | Apenas quando o cliente tem scope restrito no AM. |
| `IGRP_ACCESS_M2M_SYNC_TOKEN` | `igrp.access.m2m.sync-token` | Descontinuado. Alternativa quando as client-credentials não estão definidas. |
| `SERVER_FORWARD_HEADERS_STRATEGY` | `server.forward-headers-strategy` | `framework` por trás de um ingress; ver a ressalva em ponto 4.1. |

A conta de serviço que representa a identidade **M2M** (*Machine-to-Machine*) deste backend é gerida no AM através de `/api/service-accounts`. Crie-a através da [API M2M](/docs/developer/access-management/m2m-api) ou, na ausência de uma conta inicial, inicialize-a através de *seed* por SQL ou do *bootstrap* em `/api/m2m/...`.

</TabItem>

<TabItem value="frontend-app" label="Frontend da Aplicação">

#### 4.4 Frontend da Aplicação [#44-frontend-da-aplicação]

**Referência canónica**: `shared/frontend/nextjs/templates/demo-v1/.env.example` no ramo dev.

Mesma estrutura do Application Center (ponto 4.2), uma vez que ambos utilizam o _provider_ `igrp-auth` do framework. Os frontends de destino necessitam adicionalmente de credenciais M2M, porque enviam a respetiva árvore de menus para o AM durante o arranque da aplicação. O Application Center não necessita deste fluxo, uma vez que `IGRP_SYNC_ACCESS=false` na configuração de entrega (_ship config_) do AC.

##### Autenticação — igual ao AC [#autenticação--igual-ao-ac]

Idêntico a ponto 4.2 "Autenticação (obrigatório)". Cada SPA recebe o seu próprio par `IGRP_AUTH_CLIENT_ID` / `IGRP_AUTH_CLIENT_SECRET` registado no AM.

##### NextAuth — igual ao AC [#nextauth--igual-ao-ac]

Idêntico a ponto 4.2 "NextAuth (obrigatório)".

##### Configuração do framework IGRP (obrigatório) [#configuração-do-framework-igrp-obrigatório]

| Variável de ambiente | Valor por omissão | Notas |
|---|---|---|
| `IGRP_ACCESS_MANAGEMENT_API` | — | URL base do AM. Igual ao valor de `IGRP_AUTH_ISSUER`. |
| `IGRP_APP_CODE` | — | Código desta app tal como aparecerá no AM (`INVOICING`, `CADASTRO`, …). Tem de ser igual a todos os `applicationCode` em `menus.ts`. |
| `NEXT_PUBLIC_IGRP_APP_CODE` | — | Mesmo valor de `IGRP_APP_CODE`, exposto ao browser. Obrigatório porque a UI do seletor de aplicações da SPA o lê do lado do cliente. |
| `IGRP_PREVIEW_MODE` | `false` | Salta as verificações de autenticação. Apenas dev. |
| `IGRP_SYNC_ACCESS` | `false` (`true` em produção) | Interruptor principal. Ativa todo o fluxo de sincronização de arranque — registo da aplicação, sincronização de recursos/rotas, sincronização de menus. |
| `IGRP_SYNC_ON_CODE_MENUS` | `false` | Envia `src/temp/menus/menus.ts` para o AM no arranque. |
| `IGRP_SYNC_ON_CODE_MENU_ROLES` | `true` | Ao enviar os menus, também cria automaticamente os perfis referenciados caso não existam. Fique atento aos registos de aviso (WARN) da sincronização no primeiro arranque — códigos de perfil mal escritos tornam-se perfis vazios. |

##### Credenciais M2M (obrigatório quando a sincronização automática está activa) [#credenciais-m2m-obrigatório-quando-a-sincronização-automática-está-activa]

O frontend invoca os endpoints de sincronização M2M do AM através de OAuth2 `client_credentials`. O bearer token é obtido em `${IGRP_ACCESS_MANAGEMENT_API}/oauth2/token` utilizando autenticação Basic com o par de credenciais indicado abaixo. O token é armazenado em cache até expirar e renovado de forma transparente em todas as chamadas de sincronização.

| Variável de ambiente | Notas |
|---|---|
| `IGRP_SERVICE_ID` | Identidade desta app para auditoria M2M — enviada como `X-Machine-Service-ID`. Kebab-case, 2–64 carateres. |
| `IGRP_M2M_CLIENT_ID` | Id de cliente OAuth2 emitido pelo AM para a integração M2M DESTA app. Distinto de `IGRP_AUTH_CLIENT_ID` (que serve para o login do utilizador final). |
| `IGRP_M2M_CLIENT_SECRET` | Segredo. Apenas do lado do servidor; nunca exposto ao browser. Rode através da administração do AM. |

Todos os quatro (`IGRP_SERVICE_ID`, `IGRP_M2M_CLIENT_ID`, `IGRP_M2M_CLIENT_SECRET`, `IGRP_ACCESS_MANAGEMENT_API`) são validados em tempo de renderização quando `IGRP_SYNC_ACCESS=true` e `IGRP_PREVIEW_MODE=false`. Uma configuração incorreta surge como `IgrpConfigError` em `global-error.tsx`, em vez de um 4xx opaco vindo do AM.

##### Bloco .env pronto a colar para uma nova SPA [#bloco-env-pronto-a-colar-para-uma-nova-spa]

```bash
# ============================================
# Autenticação (comunica apenas com o AM; o IdP a montante é transparente)
# ============================================
AUTH_PROVIDER=igrp-auth
IGRP_AUTH_CLIENT_ID=invoicing-spa
IGRP_AUTH_CLIENT_SECRET=${INVOICING_SPA_CLIENT_SECRET}
IGRP_AUTH_ISSUER=https://api.example.com/igrp-access-management
IGRP_AUTH_SCOPES=openid email

# ============================================
# NextAuth — Nota: ambos os URLs TÊM de terminar em /api/auth
# (e TÊM de incluir NEXT_PUBLIC_BASE_PATH quando definido)
# ============================================
NEXTAUTH_URL=https://invoicing.example.com/api/auth
NEXTAUTH_URL_INTERNAL=http://invoicing-spa.invoicing.svc.cluster.local:3000/api/auth
NEXTAUTH_SECRET=${NEXTAUTH_SECRET}     # falha de forma irrecuperável no arranque em produção se não definido

# ============================================
# Framework IGRP
# ============================================
IGRP_ACCESS_MANAGEMENT_API=https://api.example.com/igrp-access-management
IGRP_APP_CODE=INVOICING
NEXT_PUBLIC_IGRP_APP_CODE=INVOICING
IGRP_PREVIEW_MODE=false
IGRP_SYNC_ACCESS=true
IGRP_SYNC_ON_CODE_MENUS=true
IGRP_SYNC_ON_CODE_MENU_ROLES=true

# ============================================
# Credenciais M2M para a sincronização de arranque
# ============================================
IGRP_SERVICE_ID=invoicing-spa
IGRP_M2M_CLIENT_ID=invoicing-spa-m2m
IGRP_M2M_CLIENT_SECRET=${INVOICING_SPA_M2M_SECRET}

# ============================================
# Navegação
# ============================================
NEXT_IGRP_APP_CENTER_URL=https://apps.example.com/app-center
NEXT_PUBLIC_BASE_PATH=
NEXT_PUBLIC_IGRP_APP_HOME_SLUG=/invoices
NEXT_PUBLIC_ALLOWED_DOMAINS=cdn.example.com
NEXT_PUBLIC_IGRP_PROFILE_URL=
NEXT_PUBLIC_IGRP_NOTIFICATION_URL=
NEXT_PUBLIC_IGRP_SETTINGS_URL=
```

##### Dois clientes OAuth2 por SPA, não um [#dois-clientes-oauth2-por-spa-não-um]

Cada target SPA tem tipicamente dois clientes OAuth2 distintos no AM:

- `invoicing-spa` — `authorization_code` + `refresh_token`. Usado pelo NextAuth para o login do utilizador final. O seu segredo vai para `IGRP_AUTH_CLIENT_SECRET`.
- `invoicing-spa-m2m` — `client_credentials`. Usado pela sincronização de arranque do framework (menus, registo de aplicação). O seu segredo vai para `IGRP_M2M_CLIENT_SECRET`.

Ambos são criados através da página de gestão de clientes assim que estiver disponível, ou, entretanto, via SQL ou semeação M2M.

</TabItem>

</Tabs>


# Guia prático

URL: /pt/docs/developer/access-management/guia-pratico

Percurso de ponta a ponta para iniciar um novo projeto com Access Management e resolução de problemas comuns.

### 5. Percurso de ponta a ponta: iniciar um novo projeto [#5-percurso-de-ponta-a-ponta-iniciar-um-novo-projeto]

Concretamente: está a criar um novo par backend + frontend chamado **INVOICING**. Eis o que fazer, por ordem, desde repositórios vazios até uma aplicação em funcionamento cujos menus e permissões estão visíveis no AM.

<Steps>
  <Step>
    #### Passo 1 — Criar os clientes OAuth2 no AM (uma vez por projeto) [#passo-1--criar-os-clientes-oauth2-no-am-uma-vez-por-projeto]

    Para cada projeto, registe três clientes OAuth2 no AM (a separação por tipo de grant reduz o raio de impacto de uma rotação / revogação):

    - `invoicing-spa` — `authorization_code` + `refresh_token`. Consumido pelas variáveis `IGRP_AUTH_*` do frontend (login do utilizador final através do fornecedor `igrp-auth` do framework).
    - `invoicing-spa-m2m` — `client_credentials`. Consumido pelas variáveis `IGRP_M2M_*` do frontend para a sincronização de arranque de menus / aplicação.
    - `invoicing-backend-m2m` — `client_credentials`. Consumido pelas propriedades `igrp.access.m2m.*` do backend da aplicação para a sincronização de permissões.

    Registe-os através da (brevemente disponível) página de gestão de clientes no AC, ou inserindo registos via SQL a partir do padrão de semeação `igrp.oauth.default-client.*`, até a página estar disponível.

    Para o cliente `invoicing-spa`, lembre-se também de registar os dois URIs de redirecionamento (ver ponto 4.2 "Autenticação"):

    - **Callback:** `${NEXTAUTH_URL_ORIGIN}${NEXT_PUBLIC_BASE_PATH}/api/auth/callback/igrp-auth`
    - **Pós-logout:** `${NEXTAUTH_URL_ORIGIN}${NEXT_PUBLIC_BASE_PATH}/login`

    Guarde o `clientSecret` em bruto de cada resposta de criação — só é mostrado uma vez.
  </Step>
  <Step>
    #### Passo 2 — Configurar o backend da aplicação [#passo-2--configurar-o-backend-da-aplicação]

    Siga a [secção 2.4 do Permissions Integration Guide](/docs/developer/framework/backend/permissoes#processador-anotacoes) para o `pom.xml` (repositórios, dependências, caminho do processador de anotações) e a [secção 5](/docs/developer/framework/backend/permissoes#configuracao) do mesmo guia para o `application.properties`.

    No mínimo:

    ```properties
    spring.application.name=invoicing-backend
    spring.security.oauth2.resourceserver.jwt.issuer-uri=https://api.example.com/igrp-access-management

    igrp.access.base-url=https://api.example.com/igrp-access-management
    igrp.access.m2m.client-id=invoicing-backend-m2m
    igrp.access.m2m.client-secret=${INVOICING_BACKEND_M2M_SECRET}
    ```

    Declare as suas permissões:

    ```java
    public final class InvoicePermissions {
        private InvoicePermissions() {}

        @IgrpPermission(name = "my.invoice.create", description = "Create invoice")
        public static final String INVOICE_CREATE = "my.invoice.create";
        // …
    }
    ```

    Arranque-o. Verifique os registos para:

    - `[Authorization Sync] Authenticating to Access Management via client_credentials grant (clientId=invoicing-backend-m2m)`
    - `[Authorization Sync] Resource synchronization completed.`
    - `[Permission Sync] Successfully synchronized N permissions.`

    Verifique no AC: **Settings → Applications → INVOICING** deve agora listar as suas permissões.
  </Step>
  <Step>
    #### Passo 3 — Atribuir um perfil → permissão [#passo-3--atribuir-um-perfil--permissão]

    Crie um perfil no AC (p. ex. `INV.reader` no departamento `FIN`), associe-lhe permissões e atribua o perfil a um utilizador de teste.
  </Step>
  <Step>
    #### Passo 4 — Configurar o frontend da aplicação [#passo-4--configurar-o-frontend-da-aplicação]

    Crie `src/temp/menus/menus.ts` (ponto 3.1) e `src/igrp.template.config.ts` (ponto 3.2). Defina as variáveis de ambiente conforme o ponto 4.4.

    Arranque a SPA. Nos registos do servidor NextJS:

    - `[startup-sync] Application sync ok: appCode=INVOICING`
    - `[startup-sync] Menu sync ok: appCode=INVOICING, items=4, syncRoles=true`

    Verifique no AC: **Settings → Applications → INVOICING → Menus** deve agora mostrar a sua árvore.
  </Step>
  <Step>
    #### Passo 5 — Iniciar sessão e utilizar [#passo-5--iniciar-sessão-e-utilizar]

    Inicie sessão com o utilizador de teste ao qual foi atribuído `INV.reader` no passo 3. A barra lateral da SPA mostra apenas os itens de menu cujo `roles[]` inclui `INV.reader/FIN`. As chamadas à API do backend da aplicação transportam o JWT do utilizador com `permissions: ["my.invoice.read", …]`, e o `@PreAuthorize` aceita ou rejeita em conformidade.
  </Step>
</Steps>


# Visão geral

URL: /pt/docs/developer/access-management

Referência técnica para integrar um projeto iGRP com o Access Management — panorama, sincronização, configuração e modelo de acesso.

# Gestão de Acessos & Permissões [#gestão-de-acessos--permissões]

Descreve como integrar um projeto (API backend + frontend Next.js) com o iGRP Access Management. No final, a aplicação terá permissões e menus sincronizados, autenticação via fornecedor de identidade partilhado e a SPA apresentará a árvore de menus correta por perfil (_role_).

<Callout type="objective" title="Âmbito">
Abrange a sincronização de permissões do backend, a sincronização de menus do frontend, e a superfície de variáveis de ambiente dos quatro intervenientes do sistema:

- **Access Management (AM)** — o servidor de autorização OAuth2 + API de administração.
- **Application Center (AC)** — o portal Next.js onde os administradores gerem utilizadores, perfis (_roles_), permissões, menus e (brevemente) clientes / contas de serviço.
- **Backend da Aplicação** — um serviço Spring Boot que expõe APIs de negócio.
- **Frontend da Aplicação** — uma SPA Next.js que consome essas APIs.
</Callout>

<Callout type="info" title="O IdP é transparente para os frontends">
A partir das versões atuais do framework (`igrp-framework-nextjs@dev`, `application-center@dev`), a SPA deixou de comunicar diretamente com o fornecedor de identidade externo (Autentika / Keycloak / WSO2). Todos os frontends autenticam-se contra o Access Management através do fornecedor interno `igrp-auth` do framework; é depois o AM que trata internamente da federação com o IdP real. Isto significa deixar de existir variáveis `KEYCLOAK_*` do lado da SPA — apenas o bloco `AUTH_PROVIDER=igrp-auth` + `IGRP_AUTH_*` voltado para o AM, abaixo.
</Callout>

<Callout type="info" title="Percorrer a administração pelo Application Center">
Esta página é uma referência técnica para integradores. Para os fluxos passo‑a‑passo do administrador (login, gestão de aplicações, departamentos, utilizadores, convites, etc.), a documentação vive em **Utilizadores → Gestão de Acessos**:

- [Identidade & Login](/docs/utilizador/access-manager/identityAccess)
- [Application Center — Home & Definições](/docs/utilizador/access-manager/application-center)
- [Aplicações & Menus](/docs/utilizador/access-manager/application)
- [Departamentos & Perfis](/docs/utilizador/access-manager/departamento)
- [Utilizadores & Convites](/docs/utilizador/access-manager/user)
- [Fluxo de Convite (OTP)](/docs/utilizador/access-manager/invitation-flow)
- [Âmbito do Gestor (Hierarquia)](/docs/utilizador/access-manager/manager-scope)

Documentação de integração (esta secção, para developers):

- [Sincronização](./sincronizacao.mdx) — backend (permissões) e frontend (menus)
- [Configuração](./configuracao.mdx) — variáveis de ambiente por componente
- [Guia Prático](./guia-pratico.mdx) — percurso ponta-a-ponta e resolução de problemas
- [Referência](./referencia.mdx) — localização de ficheiros e documentos relacionados
- [Modelo de Acesso](./modelo-acesso.mdx) — entidades, hierarquias e fluxos de permissão
- [Integração com Notification Service](./notification-service.mdx)
- [API M2M & SDK Java](./m2m-api.mdx)
</Callout>

<Callout type="info">
Para uma análise aprofundada sobre a declaração de permissões e a configuração de `@PreAuthorize`, consulte o guia complementar [**iGRP Permissions Integration Guide**](/docs/developer/framework/backend/permissoes). Este documento é a visão de alto nível que liga permissões, menus e configuração.
</Callout>

## Panorama geral [#panorama-geral]

<AccessIntegrationMap />

| # | Quem | Quando | O que acontece | Endpoints |
|---|---|---|---|---|
| ① | Frontend do projeto | Login | O utilizador entra pelo fornecedor `igrp-auth` do framework; o Access Management emite o JWT | `/oauth2/authorize`, `/oauth2/token`, `/connect/logout` |
| ② | Frontend do projeto | Arranque | Publica os metadados da aplicação e os menus declarados no código (`src/temp/menus/menus.ts`) | `POST /api/m2m/sync/applications`, `POST /api/m2m/sync/applications/{code}/menus` |
| ③ | Backend do projeto | Arranque | Obtém um token `client_credentials` e publica o recurso e as permissões `@IgrpPermission` | `POST /oauth2/token`, `POST /api/m2m/sync/resources`, `POST /api/m2m/sync/permissions` |
| ④ | Application Center | Uso | Administração: utilizadores, departamentos, perfis, permissões, aplicações e menus | `/api/users`, `/api/departments` (perfis e permissões em `…/{code}/roles/…`), `/api/applications` (menus em `…/{code}/menus`), `/api/clients`, `/api/service-accounts` |
| ⑤ | Frontend → backend | Cada pedido | Chamadas de negócio com o JWT do utilizador; o backend decide com `@PreAuthorize` | — |
| ⑥ | Backend do projeto | Cada pedido | Valida a assinatura e o emissor do JWT com as chaves do Access Management | `/.well-known/openid-configuration`, JWKS |
| ⑦ | Access Management | Login | Federa a autenticação com o fornecedor de identidade configurado; os frontends nunca falam com ele | — |

Os passos ② e ③ são os da [Sincronização](./sincronizacao); o ciclo completo de uma permissão (declaração, publicação, atribuição, verificação) está em [Framework › Modelo de permissões](/docs/developer/framework/permissoes).


# API M2M e SDK Java

URL: /pt/docs/developer/access-management/m2m-api

Endpoints Machine-to-Machine do Access Management e SDK Java para chamadas entre serviços com client credentials.

# API M2M & SDK Java [#api-m2m--sdk-java]

Documenta a superfície **Machine‑to‑Machine (M2M)** do Access Management — os *endpoints* que outros serviços invocam com um JWT *client credentials*, e o SDK Java (`cv.igrp.platform.access:client`) que abstrai as chamadas.

## Autenticação [#autenticação]

Todas as chamadas `/api/m2m/*` exigem um **JWT client credentials**. O *caller* obtém‑no de:

```http
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(clientId:clientSecret)>

grant_type=client_credentials&scope=m2m
```

<Callout type="warning" title="M2MTokenRejectionFilter">
  Um token M2M **não** pode ser usado para endpoints direccionados a utilizadores humanos (por exemplo, `/api/users/me/*`). O filtro `M2MTokenRejectionFilter` rejeita‑o com `403` e código `M2M_TOKEN_ON_USER_ENDPOINT`. Simetricamente, um JWT de utilizador não é aceite em `/api/m2m/*`.
</Callout>

## Endpoints de sincronização [#endpoints-de-sincronização]

Utilizados pelo *AuthorizationSyncRunner* (framework `core-spring-boot`) e pelo `startup-sync.ts` do Next.js:

| Método + Path | Corpo | Efeito |
|---|---|---|
| `POST /api/m2m/sync/permissions` | `List<PermissionDTO>` | Cria/actualiza o catálogo de permissões do *caller*. |
| `POST /api/m2m/sync/resources` | `ResourceDTO` | Registra o *caller* como *Resource* (`type=API`, `name=spring.application.name`). |
| `POST /api/m2m/sync/applications` | `ApplicationDTO` | Registra/actualiza o *caller* como *Application*. Sincroniza `name`, `slug`, `type`, `description`, `status`. |
| `POST /api/m2m/sync/applications/{code}/menus?syncRoles=true` | `List<MenuEntryDTO>` | Sincroniza a árvore de menus da aplicação. Com `syncRoles=true`, também associa perfis a cada entrada. |

## Endpoints de leitura [#endpoints-de-leitura]

Servem para os *target projects* consultarem o modelo de AM sem duplicar a lógica de scoping/RBAC. Todos aceitam um corpo JSON com filtros.

| Método + Path | Corpo (filtros) | Resposta |
|---|---|---|
| `POST /api/m2m/users` | `activeOnly`, `applicationCode`, `departmentCode`, `roleCode`, `permissionName`, `includeChildrenDepartments`, `includeChildrenRoles` | Lista de utilizadores filtrada. |
| `POST /api/m2m/departments` | `activeOnly`, `parentCode`, `includeChildren` (recursivo) | Lista de departamentos filtrada. |
| `POST /api/m2m/roles` | `activeOnly`, `applicationCode`, `departmentCode`, `includeChildrenRoles` | Lista de perfis filtrada. |

### Exemplo — lookup de utilizadores num departamento e descendentes [#exemplo--lookup-de-utilizadores-num-departamento-e-descendentes]

```http
POST /api/m2m/users
Authorization: Bearer <JWT client credentials>
Content-Type: application/json

{
  "activeOnly": true,
  "departmentCode": "DEPT_RH",
  "includeChildrenDepartments": true,
  "roleCode": "MANAGER"
}
```

```json
[
  {
    "id": "...",
    "email": "ana@example.cv",
    "preferredUsername": "ana",
    "givenName": "Ana",
    "familyName": "Silva",
    "departmentCode": "DEPT_RH_CV",
    "roles": ["MANAGER"]
  }
]
```

<Callout type="info" title="Aplicabilidade do âmbito">
  Estes endpoints são utilizados por serviços — não por gestores humanos — logo **não** aplicam a regra do âmbito do gestor. Se o objectivo for espelhar a experiência UI, o cliente do *target project* deve invocá‑los com um JWT de utilizador e por endpoints correspondentes (`/api/departments/manageable`, etc.).
</Callout>

## SDK Java [#sdk-java]

Todas as chamadas M2M têm um wrapper Java. Duas classes principais:

| Classe | Responsabilidade |
|---|---|
| **`ApiClient`** | Configura URL base, credenciais client_credentials, refresh do token em cache, timeouts. Reutilizável entre todos os serviços. |
| **`M2MApi`** | Fachada com um método por endpoint (`syncPermissions`, `syncResources`, `syncApplication`, `syncApplicationMenus`, `getUsersForBusiness`, `getDepartmentForBusiness`, `getRolesForBusiness`). Thread‑safe. |

### Configuração — Spring Boot [#configuração--spring-boot]

```java
@Configuration
public class AccessMgmtM2mConfig {

    @Bean
    ApiClient accessMgmtApiClient(
        @Value("${igrp.access.api.base-url}") String baseUrl,
        @Value("${igrp.access.m2m.client-id}") String clientId,
        @Value("${igrp.access.m2m.client-secret}") String clientSecret,
        @Value("${igrp.access.m2m.scope:m2m}") String scope
    ) {
        ApiClient client = new ApiClient();
        client.setBaseUrl(baseUrl);
        client.setClientCredentials(baseUrl, clientId, clientSecret, scope);
        return client;
    }

    @Bean
    M2MApi accessMgmtM2mApi(ApiClient client) {
        return new M2MApi(client);
    }
}
```

### Utilização típica [#utilização-típica]

```java
@Component
public class DepartmentSyncJob {

    private final M2MApi m2m;

    @Scheduled(fixedDelayString = "PT5M")
    public void syncActiveManagers() {
        var filter = new UsersForBusinessRequest()
                .activeOnly(true)
                .roleCode("MANAGER")
                .includeChildrenDepartments(true);
        List<UserDTO> managers = m2m.getUsersForBusiness(filter, "my-invoice-service");
        managers.forEach(u -> ... );
    }
}
```

<Callout type="info" title="Já vem incluído pelo framework">
  Se o teu serviço utiliza `cv.igrp.framework.auth:core-spring-boot`, o `AuthorizationSyncRunner` já invoca `syncPermissions` e `syncResources` automaticamente no arranque. Não é necessário replicar esse código — basta ter as env vars `IGRP_ACCESS_API_BASE_URL`, `IGRP_ACCESS_M2M_CLIENT_ID` e `IGRP_ACCESS_M2M_CLIENT_SECRET` configuradas. Ver o *Permissions Integration Guide*.
</Callout>

## Códigos de erro [#códigos-de-erro]

| HTTP | Código | Causa comum |
|---|---|---|
| 401 | `INVALID_TOKEN` | JWT ausente, mal formado ou expirado. |
| 403 | `M2M_TOKEN_ON_USER_ENDPOINT` | JWT M2M usado num endpoint de utilizador. |
| 403 | `USER_TOKEN_ON_M2M_ENDPOINT` | JWT de utilizador usado em `/api/m2m/*`. |
| 400 | `IGRP_AUTH_APPLICATION_CODE_REQUIRED` | `sync/applications` invocado sem `code`. |
| 500 | `IGRP_AUTH_APPLICATION_SYNC_FAILED` | Falha genérica na sincronização (ver logs). |

## Cross‑refs [#crossrefs]

- [Overview](./index.mdx) — as env vars do M2M client no *target project*.
- [Aplicações & Menus](/docs/utilizador/access-manager/application) — o efeito de `sync/applications` e `sync/applications/{code}/menus` (secção de utilizador).
- [Departamentos & Perfis](/docs/utilizador/access-manager/departamento) — o modelo que os endpoints de leitura devolvem (secção de utilizador).
- [Âmbito do Gestor](/docs/utilizador/access-manager/manager-scope) — porque estes endpoints não aplicam a mesma regra (secção de utilizador).
- [*Permissions Integration Guide*](/docs/developer/framework/backend/permissoes) — para o pipeline de sincronização automática via `core-spring-boot`.


# Modelo de controlo de acesso

URL: /pt/docs/developer/access-management/modelo-acesso

Entidades do Access Management, árvores de departamentos e de perfis, perfil ativo, permissões no JWT, verificação por pedido e invalidação de sessões.

Esta página explica como o Access Management organiza departamentos, perfis, utilizadores e permissões, e como isso se traduz no token que cada pedido leva.

## Entidades [#entidades]

| Entidade | Tabela | Para quê |
|---|---|---|
| Departamento | `t_department` | Unidade organizacional, em árvore (`parent_id`) |
| Perfil | `t_role` | Conjunto de permissões; pertence a **um** departamento e pode ter um perfil pai (`parent`) |
| Utilizador | `t_user` | Pessoa que entra na plataforma; tem um perfil ativo (`active_role_id`) |
| Atribuição | `t_user_role_assignment` | Liga um utilizador a um perfil, com data de expiração opcional |
| Permissão | `t_permission` | Capacidade nomeada (`igrp.documento.ler`) |
| Aplicação | `t_application` | Aplicação registada; ligada a departamentos, perfis, recursos e menus |
| Recurso | `t_resource` | Agrupa permissões de uma aplicação (por exemplo, o recurso de um backend) |
| Entrada de menu | `t_menu_entry` | Item de menu de uma aplicação, visível a perfis e departamentos |
| Sessão | `t_user_session` | Sessão no servidor, identificada pelo claim `sid` do JWT |

<AccessDataModel />

Tabelas de ligação: `t_role_permission` (perfil ↔ permissão), `t_permission_department` (permissão ↔ departamento), `t_department_application`, `t_application_role`, `t_application_resource`, `t_resource_permission` e `t_department_menuentry`.

## Departamentos [#departamentos]

Os departamentos formam uma árvore: cada um aponta para o pai, sem limite de profundidade.

<DepartmentTree />

O departamento não dá permissões a ninguém. Define **que permissões estão disponíveis** para os perfis desse departamento e dos departamentos abaixo dele.

## Perfis [#perfis]

Cada perfil pertence a exatamente um departamento. O código do perfil é único **dentro do departamento** (dois departamentos podem ter um perfil `tecnico`). O código de um perfil filho leva o do pai como prefixo: o Técnico criado debaixo do Gestor fica `gestor.tecnico`.

Os perfis de um departamento também podem formar uma árvore, mas a árvore é uma **regra de atribuição, não herança**:

<RoleTree />

- Ao dar permissões a um perfil filho, só são aceites as que o perfil pai já tem.
- E só as que estão disponíveis no departamento do perfil ou num departamento acima.
- Nenhum perfil recebe automaticamente as permissões do pai nem dos filhos.

## Permissões [#permissões]

As permissões são **globais por nome**. A convenção é `igrp.{domínio}.{ação}` (`igrp.documento.ler`), mas não é imposta: o nome aceita letras, números, `.`, `_` e `-`.

Uma permissão chega a um perfil por três passos:

1. O backend da aplicação declara-a (`@IgrpPermission`) e publica-a no arranque, ligada ao recurso da aplicação — ver [Sincronização](./sincronizacao).
2. Um administrador torna-a disponível num departamento.
3. O administrador dá-a a um perfil desse departamento (ou de um departamento abaixo).

## Perfil ativo e permissões efetivas [#perfil-ativo-e-permissões-efetivas]

Um utilizador pode ter vários perfis, em departamentos diferentes, cada um por uma atribuição (que pode expirar). Em cada momento **um** é o perfil ativo.

<EffectivePermissions />

- O JWT leva em `permissions` **só as permissões diretas do perfil ativo**.
- `org` é o código do departamento do perfil ativo.
- `resource_access` lista todos os perfis ativos do utilizador.
- Para trocar de perfil ativo: `POST /api/users/me/roles/active` com `{ roleCode, departmentCode }` (o administrador usa `POST /api/users/{id}/roles/active`). A troca revoga as sessões do utilizador: o token seguinte já vem com as permissões do novo perfil.

### Claims do JWT [#claims-do-jwt]

```json
{
  "sub": "f3c1…",
  "sid": "8a2d…",
  "device_id": "c41e…",
  "selectedRole": "tecnico",
  "org": "SADM",
  "permissions": ["igrp.documento.ler", "igrp.documento.criar"],
  "resource_access": {
    "minha-app": { "roles": ["tecnico", "arquivista"] }
  },
  "is_super_admin": false,
  "email": "maria.silva@exemplo.cv",
  "exp": 1793055600,
  "iss": "https://<gateway>/igrp-access-management"
}
```

| Claim | Conteúdo |
|---|---|
| `sid` | Sessão no servidor; revogá-la invalida o token no pedido seguinte |
| `selectedRole`, `org` | Perfil ativo e o departamento dele |
| `permissions` | Permissões diretas do perfil ativo |
| `resource_access` | Perfis do utilizador, por cliente |
| `is_super_admin` | `true` se alguma atribuição válida for de superadministrador |
| `name`, `email`, `locale`, … | Conforme os scopes pedidos no login |

## Verificação em cada pedido [#verificação-em-cada-pedido]

Na API do Access Management, cada pedido passa por duas verificações:

<AccessRequestCheck />

Os backends dos projetos validam o JWT e decidem com `@PreAuthorize` sobre o `permissions` do token — ver o percurso em [Framework › Backend › Permissões](/docs/developer/framework/backend/permissoes-referencia).

## Invalidação [#invalidação]

Cada alteração estrutural publica um evento. Os eventos revogam as sessões afetadas e, nalguns casos, limpam a cache de permissões no Redis:

<InvalidationEvents />

Como as permissões vão no token, uma alteração só tem efeito com um token novo. A revogação da sessão força isso: o pedido seguinte com o `sid` antigo recebe `401` e a aplicação volta a autenticar o utilizador.

## Cenário completo [#cenário-completo]

<AccessScenario />

1. Os Serviços Administrativos (`SADM`) têm o perfil Técnico, com `igrp.documento.ler` e `igrp.documento.criar`. O Arquivo (`ARQ`), filho de `SADM`, tem o perfil Arquivista, com `igrp.arquivo.consultar`.
2. A Maria Silva tem os dois perfis atribuídos, com o Técnico ativo. O token dela leva as permissões do Técnico e `org: SADM`.
3. Para consultar o arquivo, troca o perfil ativo para Arquivista. As sessões são revogadas e o token novo leva `igrp.arquivo.consultar` e `org: ARQ` — e já não as permissões do Técnico.

## Resumo [#resumo]

| Conceito | Regra |
|---|---|
| Departamento | Árvore sem limite de profundidade; define as permissões disponíveis para os seus perfis e os dos departamentos abaixo |
| Perfil | Pertence a um departamento; código único dentro dele |
| Árvore de perfis | Restrição: o filho só recebe permissões que o pai tem. Não há herança |
| Utilizador | Vários perfis por atribuição (com expiração opcional); um perfil ativo |
| Permissões no JWT | Só as diretas do perfil ativo; `org` é o departamento dele |
| Troca de perfil | Revoga as sessões; o token seguinte traz as permissões do novo perfil |
| Nome das permissões | Convenção `igrp.{domínio}.{ação}`, global, não imposta |
| Invalidação | Alterações estruturais revogam as sessões afetadas; algumas também limpam a cache de permissões |


# Integração com notificações

URL: /pt/docs/developer/access-management/notification-service

Como o Access Management envia os emails de convite e OTP pelo iGRP Notification Service, o que configurar dos dois lados e o que acontece quando falha.

O Access Management envia os emails do [Fluxo de Convite](/docs/utilizador/access-manager/invitation-flow) — convite, reenvio, cancelamento, resposta e código OTP — de uma de duas formas:

| Modo | Quando escolher |
|---|---|
| **Notification Service** | Produção. Templates editáveis por evento e por idioma, histórico de envios, retentativas. |
| **SMTP local** | Instalações simples. Texto fixo, sem templates. Também é o recurso quando o Notification Service falha. |

| Variável | Valor | Efeito |
|---|---|---|
| `IGRP_NOTIFICATION_SERVICE_ENABLED` | `true` | Usa o Notification Service como caminho principal |
| `IGRP_NOTIFICATIONS_ENABLED` | `mail` | Liga o SMTP local (`SMTP_HOST`, `SMTP_PORT`, …), usado sozinho ou como recurso |
| `IGRP_NOTIFICATIONS_ENABLED` | `none` (omissão) | Sem SMTP local: se o Notification Service não enviar, o email não sai |

Com os dois desligados os convites ficam registados, mas nenhum email é enviado.

## Como o email sai [#como-o-email-sai]

<InvitationEmailSequence />

1. O Access Management pede um token `client_credentials` ao seu próprio `/oauth2/token` e guarda-o em cache.
2. Chama `POST /api/v1/notifications` no Notification Service com o código do template do evento, o locale do convidado e as variáveis.
3. Se não houver template publicado nesse locale (`422 TEMPLATE_ERROR`), repete **uma vez** com o locale por omissão do módulo (`igrp.i18n.default-locale`).
4. Se voltar a falhar, ou o serviço não responder, envia pelo SMTP local quando `IGRP_NOTIFICATIONS_ENABLED=mail`.

Nenhum evento de convite falha por causa do email: o convite é criado, reenviado ou cancelado na mesma. A exceção é a **validação por OTP**: se nem o Notification Service nem o SMTP local entregarem o código, a validação falha com erro.

## O que configurar [#o-que-configurar]

### No Access Management [#no-access-management]

| Requisito | Porquê |
|---|---|
| Um **cliente M2M** cuja conta de serviço está associada à aplicação **`APP_IGRP_CENTER`** | O pedido não envia `applicationCode`: a aplicação vem do claim `application_code` do token, e os templates de convite pertencem a `APP_IGRP_CENTER` |
| A conta de serviço com a permissão **`igrp.notification.send`** | Sem ela o Notification Service responde `403` |

### No Notification Service [#no-notification-service]

| Requisito | Porquê |
|---|---|
| **SMTP configurado** (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`) | Sem ele o envio é recusado (`422 CHANNEL_NOT_ENABLED`) — ver [Configurar os canais](/docs/developer/notification#email-smtp) |
| `AUTH_JWT_ISSUER` igual ao `iss` dos tokens do Access Management | O serviço rejeita tokens de outro emissor |
| Os templates de convite publicados | Já vêm publicados: o serviço cria-os no arranque em `pt` e `en` (ver [Templates da plataforma](/docs/developer/notification/templates#templates-da-plataforma)) |

## Variáveis de configuração [#variáveis-de-configuração]

| Variável | Omissão | Descrição |
|---|---|---|
| `IGRP_NOTIFICATION_SERVICE_ENABLED` | `false` | Liga o envio pelo Notification Service |
| `IGRP_NOTIFICATION_SERVICE_BASE_URL` | — | Base do Notification Service, sem `/api/v1` |
| `IGRP_NOTIFICATION_SERVICE_AUTH_URL` | — | Base do Access Management para o `/oauth2/token` (ver aviso abaixo) |
| `IGRP_NOTIFICATION_SERVICE_CLIENT_ID`, `…_CLIENT_SECRET` | — | Cliente M2M |
| `IGRP_NOTIFICATION_SERVICE_SCOPE` | vazio | Scope pedido no token |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_TEMPLATE_CODE` | `user-invitation` | Convite |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_RESEND_TEMPLATE_CODE` | `user-invitation` | Reenvio |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_CANCELLATION_TEMPLATE_CODE` | `user-invitation-cancelled` | Cancelamento |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_RESPONSE_TEMPLATE_CODE` | `user-invitation-responded` | Resposta ao convite |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_OTP_TEMPLATE_CODE` | `user-invitation-otp` | Código OTP |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_DEFAULT_LOCALE` | `en` | Locale usado quando o pedido não traz um |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_PROJECT` | `iGRP` | Valor da variável `project` nos templates |
| `IGRP_NOTIFICATION_SERVICE_INVITATION_APP_CENTER_URL` | — | Valor da variável `appCenterUrl` (só enviada se definida) |

<Callout type="warning" title="AUTH_URL só é usado com SCOPE definido">
Com `IGRP_NOTIFICATION_SERVICE_SCOPE` vazio, o Access Management ignora `IGRP_NOTIFICATION_SERVICE_AUTH_URL` e pede o token a `{IGRP_NOTIFICATION_SERVICE_BASE_URL}/oauth2/token` — o Notification Service, que responde 401. O envio cai então para o SMTP local. Até isto ser corrigido, define um `SCOPE` aceite pelo cliente M2M para que o token seja pedido ao `AUTH_URL`.
</Callout>

## Variáveis dos templates [#variáveis-dos-templates]

| Template (omissão) | Variáveis |
|---|---|
| `user-invitation` | `project`, `user`, `link`, `year`, `appCenterUrl` |
| `user-invitation-cancelled` | `project`, `user`, `year`, `appCenterUrl` |
| `user-invitation-responded` | `project`, `user`, `year`, `appCenterUrl` |
| `user-invitation-otp` | `project`, `user`, `otp`, `year`, `appCenterUrl` |

`user` é o nome do convidado (ou o email, se não houver nome). Para mudar o texto, edita estes templates na [Notifications UI](/docs/developer/notification/templates#criar-na-consola) — aplicação `APP_IGRP_CENTER` — e publica uma versão nova.

## Exemplos de emails [#exemplos-de-emails]

<StudioPreview imageName="acInviteEmail" alt="Exemplo — email de convite" />

<StudioPreview imageName="acInviteOtpEmail" alt="Exemplo — email de OTP" />

## Ver também [#ver-também]

- [Fluxo de Convite (OTP)](/docs/utilizador/access-manager/invitation-flow) — o que o convidado vê.
- [Utilizadores & Convites](/docs/utilizador/access-manager/user) — como o administrador dispara os convites.
- [Notificações › Envio de notificações](/docs/developer/notification#envio-de-notificações) — os requisitos de envio do lado do Notification Service.


# Aplicações

URL: /pt/docs/developer/access-management/referencia-api/aplicacoes

Aplicações registadas, menus, recursos e campos personalizados.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Listar aplicações

`GET /api/applications`

Permissão: `igrp.applications.list`.

**Parâmetros**

- `code` (query, string)
- `name` (query, string)
- `slug` (query, string)
- `departmentCode` (query, string)
- `type` (query, string)

**Respostas**

- `200` The List of Application — `ApplicationDTO`

## Criar aplicação

`POST /api/applications`

Permissão: `igrp.applications.create`.

**Corpo do pedido** (`application/json`, `ApplicationDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `type` ("EXTERNAL" | "INTERNAL" | "SYSTEM", obrigatório)
- `owner` (string)
- `picture` (string)
- `url` (string (uri))
- `slug` (string)
- `createdBy` (string)
- `createdDate` (string)
- `lastModifiedBy` (string)
- `lastModifiedDate` (string)
- `lastAccess` (string (date-time))
- `departments` (string[])

**Respostas**

- `201` The Persisted Application — `ApplicationDTO`

## Obter aplicação por id

`GET /api/applications/{id}`

Permissão: `igrp.applications.view`.

**Parâmetros**

- `id` (path, integer (int32), obrigatório)

**Respostas**

- `200` The Application Data — `ApplicationDTO`

## Obter aplicação por código

`GET /api/applications/by-code/{code}`

Permissão: `igrp.applications.view`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `ApplicationDTO`

## Eliminar aplicação

`DELETE /api/applications/{code}`

Permissão: `igrp.applications.delete`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `204` No content — `string`

## Atualizar aplicação

`PUT /api/applications/{code}`

Permissão: `igrp.applications.update`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ApplicationDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `type` ("EXTERNAL" | "INTERNAL" | "SYSTEM", obrigatório)
- `owner` (string)
- `picture` (string)
- `url` (string (uri))
- `slug` (string)
- `createdBy` (string)
- `createdDate` (string)
- `lastModifiedBy` (string)
- `lastModifiedDate` (string)
- `lastAccess` (string (date-time))
- `departments` (string[])

**Respostas**

- `200` The Updated Application — `ApplicationDTO`

## Aplicações de um utilizador

`GET /api/applications/by-user/{uid}`

**Parâmetros**

- `uid` (path, string, obrigatório)

**Respostas**

- `200` List of accessible Applications — `ApplicationDTO`

## Menus da aplicação

`GET /api/applications/{code}/menus`

Permissão: `igrp.applications.view`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `200` Get all application menus — `MenuEntryDTO`

## Criar menu

`POST /api/applications/{applicationCode}/menus`

Permissão: `igrp.applications.manage`.

**Parâmetros**

- `applicationCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `MenuEntryDTO`)

- `id` (integer (int32))
- `code` (string)
- `name` (string, obrigatório)
- `type` ("SYSTEM_PAGE" | "MENU_PAGE" | "EXTERNAL_PAGE" | "FOLDER" | "GROUP")
- `position` (integer (int32))
- `icon` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `target` (string)
- `url` (string)
- `pageSlug` (string)
- `parentCode` (string)
- `applicationCode` (string)
- `createdBy` (string)
- `createdDate` (string)
- `lastModifiedBy` (string)
- `lastModifiedDate` (string)
- `roles` (RoleDepartmentDTO[])

**Respostas**

- `201` The Persisted Menu — `MenuEntryDTO`

## Eliminar menu

`DELETE /api/applications/{applicationCode}/menus/{menuCode}`

Permissão: `igrp.applications.delete`.

**Parâmetros**

- `applicationCode` (path, string, obrigatório)
- `menuCode` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Atualizar menu

`PUT /api/applications/{applicationCode}/menus/{menuCode}`

Permissão: `igrp.applications.manage`.

**Parâmetros**

- `applicationCode` (path, string, obrigatório)
- `menuCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `MenuEntryDTO`)

- `id` (integer (int32))
- `code` (string)
- `name` (string, obrigatório)
- `type` ("SYSTEM_PAGE" | "MENU_PAGE" | "EXTERNAL_PAGE" | "FOLDER" | "GROUP")
- `position` (integer (int32))
- `icon` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `target` (string)
- `url` (string)
- `pageSlug` (string)
- `parentCode` (string)
- `applicationCode` (string)
- `createdBy` (string)
- `createdDate` (string)
- `lastModifiedBy` (string)
- `lastModifiedDate` (string)
- `roles` (RoleDepartmentDTO[])

**Respostas**

- `200` The Updated Menu — `MenuEntryDTO`

## Mostrar menu a perfis

`POST /api/applications/{applicationCode}/menus/{menuCode}/roles`

Permissão: `igrp.applications.manage`.

**Parâmetros**

- `departmentCode` (query, string, obrigatório)
- `applicationCode` (path, string, obrigatório)
- `menuCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `MenuEntryDTO`

## Esconder menu de perfis

`DELETE /api/applications/{applicationCode}/menus/{menuCode}/roles`

Permissão: `igrp.applications.manage`.

**Parâmetros**

- `departmentCode` (query, string, obrigatório)
- `applicationCode` (path, string, obrigatório)
- `menuCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `MenuEntryDTO`

## Ligar recurso à aplicação

`POST /api/applications/{code}/resources`

Permissão: `igrp.applications.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Desligar recurso da aplicação

`DELETE /api/applications/{code}/resources`

Permissão: `igrp.applications.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Campos personalizados da aplicação

`GET /api/applications/{code}/custom-fields`

Permissão: `igrp.applications.view`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `200` Application Custom Fields  — `string`

## Adicionar campos personalizados

`POST /api/applications/{code}/custom-fields`

Permissão: `igrp.applications.update`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `object`)

**Respostas**

- `204` No Content — `string`

## Remover campos personalizados

`DELETE /api/applications/{code}/custom-fields`

Permissão: `igrp.applications.update`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

# Auditoria

URL: /pt/docs/developer/access-management/referencia-api/auditoria

Eventos de segurança e relatórios de acessos, auditoria e configuração.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Listar eventos de auditoria

`GET /api/auth/audit`

**Parâmetros**

- `userId` (query, string)
- `username` (query, string)
- `eventType` (query, string)
- `category` (query, string)
- `ipAddress` (query, string)
- `startDate` (query, string (date-time))
- `endDate` (query, string (date-time))
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageSecurityAuditLogDTO`

## Validar a cadeia de auditoria

`POST /api/auth/audit/validate`

**Respostas**

- `200` OK — `object`

## Apagar eventos de auditoria antigos

`DELETE /api/auth/audit/purge`

**Respostas**

- `200` OK — `object`

## Relatório de acessos

`GET /api/auth/reports/access`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `role` (query, string)
- `module` (query, string)
- `action` (query, string)
- `status` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageAccessReportRowDTO`

## Relatório de acessos (CSV)

`GET /api/auth/reports/access.csv`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `role` (query, string)
- `module` (query, string)
- `action` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Relatório de acessos (PDF)

`GET /api/auth/reports/access.pdf`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `role` (query, string)
- `module` (query, string)
- `action` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Relatório de acessos (XLSX)

`GET /api/auth/reports/access.xlsx`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `role` (query, string)
- `module` (query, string)
- `action` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Arquivar relatório de acessos

`POST /api/auth/reports/access/archive`

**Parâmetros**

- `format` (query, "PDF" | "XLSX" | "CSV", obrigatório)
- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `role` (query, string)
- `module` (query, string)
- `action` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `AuditReportFileDTO`

## Relatório de auditoria

`GET /api/auth/reports/audit`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `module` (query, string)
- `accessRole` (query, string)
- `operationState` (query, string)
- `authorizedBy` (query, string)
- `status` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageAuditReportRowDTO`

## Relatório de auditoria (CSV)

`GET /api/auth/reports/audit.csv`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `module` (query, string)
- `accessRole` (query, string)
- `operationState` (query, string)
- `authorizedBy` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Relatório de auditoria (PDF)

`GET /api/auth/reports/audit.pdf`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `module` (query, string)
- `accessRole` (query, string)
- `operationState` (query, string)
- `authorizedBy` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Relatório de auditoria (XLSX)

`GET /api/auth/reports/audit.xlsx`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `module` (query, string)
- `accessRole` (query, string)
- `operationState` (query, string)
- `authorizedBy` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Arquivar relatório de auditoria

`POST /api/auth/reports/audit/archive`

**Parâmetros**

- `format` (query, "PDF" | "XLSX" | "CSV", obrigatório)
- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `username` (query, string)
- `module` (query, string)
- `accessRole` (query, string)
- `operationState` (query, string)
- `authorizedBy` (query, string)
- `status` (query, string)

**Respostas**

- `200` OK — `AuditReportFileDTO`

## Relatório de configuração

`GET /api/auth/reports/settings`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `performedBy` (query, string)
- `area` (query, string)
- `entityType` (query, string)
- `operation` (query, string)
- `entityName` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageSettingsReportRowDTO`

## Relatório de configuração (CSV)

`GET /api/auth/reports/settings.csv`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `performedBy` (query, string)
- `area` (query, string)
- `entityType` (query, string)
- `operation` (query, string)
- `entityName` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Relatório de configuração (PDF)

`GET /api/auth/reports/settings.pdf`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `performedBy` (query, string)
- `area` (query, string)
- `entityType` (query, string)
- `operation` (query, string)
- `entityName` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Relatório de configuração (XLSX)

`GET /api/auth/reports/settings.xlsx`

**Parâmetros**

- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `performedBy` (query, string)
- `area` (query, string)
- `entityType` (query, string)
- `operation` (query, string)
- `entityName` (query, string)

**Respostas**

- `200` OK — `StreamingResponseBody`

## Arquivar relatório de configuração

`POST /api/auth/reports/settings/archive`

**Parâmetros**

- `format` (query, "PDF" | "XLSX" | "CSV", obrigatório)
- `startDate` (query, string (date-time), obrigatório)
- `endDate` (query, string (date-time), obrigatório)
- `performedBy` (query, string)
- `area` (query, string)
- `entityType` (query, string)
- `operation` (query, string)
- `entityName` (query, string)

**Respostas**

- `200` OK — `AuditReportFileDTO`

## Listar relatórios arquivados

`GET /api/auth/reports/archives`

**Parâmetros**

- `reportType` (query, string)
- `format` (query, string)
- `generatedBy` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageAuditReportFileDTO`

# Autorização

URL: /pt/docs/developer/access-management/referencia-api/autorizacao

Verificar se um utilizador pode fazer uma ação.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Verificar uma autorização

`POST /api/authorize/check`

**Corpo do pedido** (`application/json`, `PermissionCheckRequestDTO`)

- `resource` (string)
- `action` (string)

**Respostas**

- `200` OK — `PermissionCheckResponseDTO`

## Verificar várias autorizações

`POST /api/authorize/batch-check`

**Corpo do pedido** (`application/json`, `PermissionCheckRequestDTO[]`)

**Respostas**

- `200` OK — `PermissionCheckResponseDTO`

# Clientes OAuth

URL: /pt/docs/developer/access-management/referencia-api/clientes-oauth

Clientes OAuth e contas de serviço para chamadas entre serviços.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Listar clientes OAuth

`GET /api/clients`

Permissão: `igrp.client.list`.

**Respostas**

- `200` OK — `OAuthClientDTO[]`

## Criar cliente OAuth

`POST /api/clients`

Permissão: `igrp.client.create`. O segredo do cliente só aparece nesta resposta: guarda-o.

**Corpo do pedido** (`application/json`, `OAuthClientRequestDTO`)

- `clientId` (string, obrigatório)
- `clientName` (string, obrigatório)
- `description` (string)
- `active` (boolean)
- `requirePkce` (boolean)
- `applicationId` (integer (int32))
- `accessTokenTtl` (integer (int32))
- `refreshTokenTtl` (integer (int32))
- `authorizationCodeTtl` (integer (int32))
- `scopes` (string[], obrigatório)
- `redirectUris` (string[])
- `postLogoutRedirectUris` (string[])
- `grantTypes` (string[], obrigatório)

**Respostas**

- `200` OK — `OAuthClientDTO`

## Obter cliente OAuth

`GET /api/clients/{id}`

Permissão: `igrp.client.view`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Respostas**

- `200` OK — `OAuthClientDTO`

## Eliminar cliente OAuth

`DELETE /api/clients/{id}`

Permissão: `igrp.client.delete`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Respostas**

- `200` OK

## Atualizar cliente OAuth

`PUT /api/clients/{id}`

Permissão: `igrp.client.update`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Corpo do pedido** (`application/json`, `OAuthClientRequestDTO`)

- `clientId` (string, obrigatório)
- `clientName` (string, obrigatório)
- `description` (string)
- `active` (boolean)
- `requirePkce` (boolean)
- `applicationId` (integer (int32))
- `accessTokenTtl` (integer (int32))
- `refreshTokenTtl` (integer (int32))
- `authorizationCodeTtl` (integer (int32))
- `scopes` (string[], obrigatório)
- `redirectUris` (string[])
- `postLogoutRedirectUris` (string[])
- `grantTypes` (string[], obrigatório)

**Respostas**

- `200` OK — `OAuthClientDTO`

## Listar contas de serviço

`GET /api/service-accounts`

**Respostas**

- `200` OK — `ServiceAccountDTO[]`

## Criar conta de serviço

`POST /api/service-accounts`

**Corpo do pedido** (`application/json`, `ServiceAccountRequestDTO`)

- `name` (string, obrigatório)
- `description` (string)
- `active` (boolean)
- `oauthClientId` (string (uuid), obrigatório)
- `applicationId` (integer (int32))
- `roleIds` (integer (int32)[])
- `permissionIds` (integer (int32)[])

**Respostas**

- `200` OK — `ServiceAccountDTO`

## Obter conta de serviço

`GET /api/service-accounts/{id}`

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Respostas**

- `200` OK — `ServiceAccountDTO`

## Eliminar conta de serviço

`DELETE /api/service-accounts/{id}`

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Respostas**

- `200` OK

## Atualizar conta de serviço

`PUT /api/service-accounts/{id}`

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Corpo do pedido** (`application/json`, `ServiceAccountRequestDTO`)

- `name` (string, obrigatório)
- `description` (string)
- `active` (boolean)
- `oauthClientId` (string (uuid), obrigatório)
- `applicationId` (integer (int32))
- `roleIds` (integer (int32)[])
- `permissionIds` (integer (int32)[])

**Respostas**

- `200` OK — `ServiceAccountDTO`

# Convites

URL: /pt/docs/developer/access-management/referencia-api/convites

Convidar utilizadores e validar o convite (email e OTP).

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Listar convites

`GET /api/users/invite`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `identifierValue` (query, string)

**Respostas**

- `200` OK — `InvitationDTO`

## Convidar utilizador

`POST /api/users/invite`

Permissão: `igrp.users.create`. O convite é enviado por email através do Notification Service.

**Corpo do pedido** (`application/json`, `InviteUserDTO`)

- `email` (string (email), obrigatório)
- `departmentCode` (string)
- `roles` (string[])

**Respostas**

- `200` OK — `InvitationDTO`

## Obter convite

`GET /api/users/invite/{id}`

**Parâmetros**

- `id` (path, integer (int32), obrigatório)

**Respostas**

- `200` OK — `InvitationDTO`

## Cancelar convite

`DELETE /api/users/invite/{id}`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `id` (path, integer (int32), obrigatório)

**Respostas**

- `200` OK — `InvitationDTO`

## Reenviar convite

`PUT /api/users/invite/{id}`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `id` (path, integer (int32), obrigatório)

**Respostas**

- `200` OK — `InvitationDTO`

## Obter convite pelo token

`GET /api/users/invite/by-token/{token}`

**Parâmetros**

- `token` (path, string, obrigatório)

**Respostas**

- `200` OK — `InvitationDTO`

## Validar email e enviar OTP

`POST /api/users/invite/validate-email`

Valida o email do convidado e envia-lhe um código OTP.

**Corpo do pedido** (`application/json`, `ValidateInvitationEmailCommand`)

- `token` (string, obrigatório)
- `email` (string, obrigatório)

**Respostas**

- `200` OK — `OtpResponseDTO`

## Validar código OTP

`POST /api/users/invite/validate-otp`

**Corpo do pedido** (`application/json`, `ValidateInvitationOtpCommand`)

- `token` (string, obrigatório)
- `otpCode` (string, obrigatório)

**Respostas**

- `200` OK — `OtpResponseDTO`

## Responder ao convite

`POST /api/users/invite/response`

**Parâmetros**

- `token` (query, string, obrigatório)

**Corpo do pedido** (`application/json`, `UserInvitationResponseDTO`)

- `accept` (boolean, obrigatório)
- `observation` (string)

**Respostas**

- `200` OK — `InvitationDTO`

# Departamentos

URL: /pt/docs/developer/access-management/referencia-api/departamentos

Árvore de departamentos e as aplicações, menus, recursos e permissões disponíveis em cada um.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Listar departamentos

`GET /api/departments`

Permissão: `igrp.departments.list`.

**Parâmetros**

- `name` (query, string)
- `status` (query, string)
- `code` (query, string)
- `parentCode` (query, string)

**Respostas**

- `200` List of Departments — `DepartmentDTO`

## Criar departamento

`POST /api/departments`

Permissão: `igrp.departments.create`.

**Corpo do pedido** (`application/json`, `DepartmentDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `status` ("ACTIVE" | "INACTIVE" | "DELETED")
- `icon` (string)
- `parentCode` (string)

**Respostas**

- `201` Created — `DepartmentDTO`

## Obter departamento por id

`GET /api/departments/{id}`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `id` (path, integer (int32), obrigatório)

**Respostas**

- `200` Department Data — `DepartmentDTO`

## Obter departamento por código

`GET /api/departments/by-code/{code}`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `DepartmentDTO`

## Eliminar departamento

`DELETE /api/departments/{code}`

Permissão: `igrp.departments.delete`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Atualizar departamento

`PUT /api/departments/{code}`

Permissão: `igrp.departments.update`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `DepartmentDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `status` ("ACTIVE" | "INACTIVE" | "DELETED")
- `icon` (string)
- `parentCode` (string)

**Respostas**

- `200` Updated Department — `DepartmentDTO`

## Aplicações do departamento

`GET /api/departments/{code}/applications`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `applicationCode` (query, string)
- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `ApplicationDTO`

## Associar aplicações ao departamento

`POST /api/departments/{code}/applications`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Desassociar aplicações do departamento

`DELETE /api/departments/{code}/applications`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Aplicações disponíveis para o departamento

`GET /api/departments/{code}/applications/available`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `ApplicationDTO`

## Menus do departamento numa aplicação

`GET /api/departments/{departmentCode}/applications/{applicationCode}/menus`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `menuCode` (query, string)
- `departmentCode` (path, string, obrigatório)
- `applicationCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `MenuEntryDTO`

## Associar menus ao departamento

`POST /api/departments/{departmentCode}/applications/{applicationCode}/menus`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `applicationCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Desassociar menus do departamento

`DELETE /api/departments/{departmentCode}/applications/{applicationCode}/menus`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `applicationCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Menus disponíveis para o departamento

`GET /api/departments/{departmentCode}/applications/{applicationCode}/menus/available`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `applicationCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `MenuEntryDTO`

## Recursos do departamento

`GET /api/departments/{code}/resources`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `resourceName` (query, string)
- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `ResourceDTO`

## Recursos disponíveis para o departamento

`GET /api/departments/{code}/resources/available`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `ResourceDTO`

## Associar recursos ao departamento

`POST /api/departments/{departmentCode}/resources`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Desassociar recursos do departamento

`DELETE /api/departments/{departmentCode}/resources`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Permissões disponíveis no departamento

`GET /api/departments/{code}/permissions`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `permissionName` (query, string)
- `resourceName` (query, string)
- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `PermissionDTO`

## Tornar permissões disponíveis no departamento

`POST /api/departments/{code}/permissions`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Retirar permissões do departamento

`DELETE /api/departments/{code}/permissions`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `204` No Content — `string`

## Permissões que podem ser adicionadas ao departamento

`GET /api/departments/{departmentCode}/permissions/available`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `resourceName` (query, string)
- `departmentCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `PermissionDTO`

# Ficheiros e configuração

URL: /pt/docs/developer/access-management/referencia-api/ficheiros-e-configuracao

Ficheiros públicos e privados e configuração global.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Carregar ficheiro público

`POST /api/files/public`

Uma destas permissões: `igrp.users.update`, `igrp.applications.update`.

**Parâmetros**

- `folder` (query, string, obrigatório)

**Corpo do pedido** (`application/json`, `object`)

- `file` (string (binary), obrigatório)

**Respostas**

- `200` The file ID — `string`

## Carregar ficheiro privado

`POST /api/files/private`

Uma destas permissões: `igrp.users.update`, `igrp.applications.update`.

**Parâmetros**

- `folder` (query, string, obrigatório)

**Corpo do pedido** (`application/json`, `object`)

- `file` (string (binary), obrigatório)

**Respostas**

- `200` OK — `string`

## URL temporário de um ficheiro privado

`GET /api/files/url`

Uma destas permissões: `igrp.users.view`, `igrp.applications.view`.

**Parâmetros**

- `filePath` (query, string, obrigatório)

**Respostas**

- `200` OK — `FileUrlDTO`

## Obter configuração global

`GET /api/global-configuration`

**Parâmetros**

- `type` (query, string, obrigatório)

**Respostas**

- `200` OK — `GlobalConfigurationDTO`

## Guardar configuração global

`POST /api/global-configuration`

Permissão: `igrp.applications.customize`.

**Corpo do pedido** (`application/json`, `GlobalConfigurationDTO`)

- `config` (string, obrigatório)
- `type` ("CLUSTER" | "ORGANIZATION", obrigatório)

**Respostas**

- `200` OK — `GlobalConfigurationDTO`

# M2M

URL: /pt/docs/developer/access-management/referencia-api/m2m

Sincronização no arranque dos backends e consultas entre serviços.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Sincronizar aplicações

`POST /api/m2m/sync/applications`

Chamado pelos backends no arranque (token `client_credentials`).

**Corpo do pedido** (`application/json`, `ApplicationDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `type` ("EXTERNAL" | "INTERNAL" | "SYSTEM", obrigatório)
- `owner` (string)
- `picture` (string)
- `url` (string (uri))
- `slug` (string)
- `createdBy` (string)
- `createdDate` (string)
- `lastModifiedBy` (string)
- `lastModifiedDate` (string)
- `lastAccess` (string (date-time))
- `departments` (string[])

**Respostas**

- `204` No Content — `string`

## Sincronizar menus de uma aplicação

`POST /api/m2m/sync/applications/{code}/menus`

**Parâmetros**

- `code` (path, string, obrigatório)
- `syncRoles` (query, boolean)

**Corpo do pedido** (`application/json`, `MenuEntryDTO[]`)

**Respostas**

- `204` No Content — `string`

## Sincronizar recursos

`POST /api/m2m/sync/resources`

**Corpo do pedido** (`application/json`, `ResourceDTO`)

- `id` (integer (int32))
- `name` (string, obrigatório)
- `description` (string)
- `type` ("API" | "UI" | "PROCESS", obrigatório)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `applications` (string[])
- `items` (ResourceItemDTO[])
- `externalId` (string)
- `createdBy` (string)
- `createdDate` (string)
- `lastModifiedBy` (string)
- `lastModifiedDate` (string)
- `permissions` (string[])

**Respostas**

- `204` No Content — `string`

## Sincronizar permissões

`POST /api/m2m/sync/permissions`

Publica as permissões declaradas com `@IgrpPermission`.

**Corpo do pedido** (`application/json`, `PermissionDTO[]`)

**Respostas**

- `204` No Content — `string`

## Consultar utilizadores

`POST /api/m2m/users`

**Parâmetros**

- `activeOnly` (query, boolean)
- `applicationCode` (query, string)
- `departmentCode` (query, string)
- `roleCode` (query, string)
- `permissionName` (query, string)
- `includeChildrenDepartments` (query, boolean)
- `includeChildrenRoles` (query, boolean)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `IGRPBusinessUserDTO`

## Consultar departamentos

`POST /api/m2m/departments`

**Parâmetros**

- `activeOnly` (query, boolean)
- `parentCode` (query, string)
- `includeChildrenDepartments` (query, boolean)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `DepartmentDTO`

## Consultar perfis

`POST /api/m2m/roles`

**Parâmetros**

- `activeOnly` (query, boolean)
- `parentCode` (query, string)
- `includeChildrenRoles` (query, boolean)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `RoleDTO`

# Perfis

URL: /pt/docs/developer/access-management/referencia-api/perfis

Perfis de cada departamento, a árvore de perfis e as permissões de cada perfil.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Perfis do departamento

`GET /api/departments/{code}/roles`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `roleCode` (query, string)
- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleDTO`

## Criar perfil

`POST /api/departments/{code}/roles`

Permissão: `igrp.departments.manage`. O código é único dentro do departamento; o de um perfil filho leva o do pai como prefixo.

**Parâmetros**

- `code` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `RoleDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `departmentCode` (string)
- `parentCode` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `icon` (string)
- `expiresAt` (string (date-time))
- `permissions` (string[])

**Respostas**

- `201` Created — `RoleDTO`

## Eliminar perfil

`DELETE /api/departments/{departmentCode}/roles/{roleCode}`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Respostas**

- `204` No Content — `boolean`

## Atualizar perfil

`PUT /api/departments/{departmentCode}/roles/{roleCode}`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `RoleDTO`)

- `id` (integer (int32))
- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `departmentCode` (string)
- `parentCode` (string)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `icon` (string)
- `expiresAt` (string (date-time))
- `permissions` (string[])

**Respostas**

- `200` OK — `RoleDTO`

## Perfis pai

`GET /api/departments/{departmentCode}/roles/{roleCode}/parents`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `level` (query, integer (int32))
- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleParentHierarchyDTO`

## Perfis filho

`GET /api/departments/{departmentCode}/roles/{roleCode}/children`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `level` (query, integer (int32))
- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleChildHierarchyDTO`

## Permissões do perfil

`GET /api/departments/{departmentCode}/roles/{roleCode}/permissions`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `PermissionDTO`

## Dar permissões ao perfil

`POST /api/departments/{departmentCode}/roles/{roleCode}/permissions`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `RoleDTO`

## Retirar permissões ao perfil

`DELETE /api/departments/{departmentCode}/roles/{roleCode}/permissions`

Permissão: `igrp.departments.manage`.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `RoleDTO`

## Permissões que podem ser dadas ao perfil

`GET /api/departments/{departmentCode}/roles/{roleCode}/permissions/available`

Permissão: `igrp.departments.manage`. Só as que o perfil pai já tem e que estão disponíveis no departamento ou acima.

**Parâmetros**

- `departmentCode` (path, string, obrigatório)
- `roleCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `PermissionDTO`

## Obter perfil por id

`GET /api/roles/{id}`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `departmentCode` (query, string, obrigatório)
- `id` (path, integer (int32), obrigatório)

**Respostas**

- `200` OK — `RoleDTO`

## Obter perfis por código

`GET /api/roles/by-code/{code}`

Permissão: `igrp.departments.view`.

**Parâmetros**

- `departmentCode` (query, string, obrigatório)
- `code` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleDTO`

# Sessões

URL: /pt/docs/developer/access-management/referencia-api/sessoes

Sessão do utilizador e administração de sessões (terminar, forçar nova autenticação).

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Sessão atual

`GET /api/session`

Sessão ativa do utilizador autenticado.

**Respostas**

- `200` Session found — `SessionResponseDTO`
- `204` No active session found — `SessionResponseDTO`

## Validar a sessão

`GET /api/session/check`

Estado combinado do JWT e da sessão no servidor. Responde mesmo a um token de sessão revogada, com `valid=false` e o motivo, em vez de `401`.

**Respostas**

- `200` Session check result — `SessionCheckResponseDTO`

## Prolongar a sessão

`POST /api/session/refresh`

Estende a expiração da sessão ativa.

**Corpo do pedido** (`application/json`, `SessionRefreshRequestDTO`)

- `extensionSeconds` (integer (int32))

**Respostas**

- `200` Session refreshed successfully — `SessionResponseDTO`
- `404` No active session to refresh — `SessionResponseDTO`

## Rodar a sessão

`POST /api/session/rotate`

Fecha a sessão atual e cria outra (proteção contra session fixation).

**Corpo do pedido** (`application/json`, `SessionInitRequestDTO`)

- `clientIp` (string)
- `deviceId` (string)
- `userAgent` (string)

**Respostas**

- `200` Session rotated successfully — `SessionResponseDTO`

## Listar sessões

`GET /api/admin/sessions`

Filtro opcional por estado e paginação.

**Parâmetros**

- `status` (query, "ACTIVE" | "CLOSED" | "EXPIRED" | "REVOKED") — Session status filter
- `page` (query, integer (int32)) — Page number (0-based)
- `size` (query, integer (int32)) — Page size

**Respostas**

- `200` Sessions retrieved successfully — `Page`

## Estatísticas de sessões

`GET /api/admin/sessions/statistics`

Sessões atuais e operações de invalidação.

**Respostas**

- `200` Statistics retrieved successfully — `object`

## Sessão ativa de um utilizador

`GET /api/admin/sessions/users/{userId}`

**Parâmetros**

- `userId` (path, string, obrigatório) — User id

**Respostas**

- `200` Session found — `SessionResponseDTO`
- `204` No active session found for user — `SessionResponseDTO`

## Sessões de um perfil

`GET /api/admin/sessions/roles/{roleCode}`

**Parâmetros**

- `roleCode` (path, string, obrigatório) — Role code
- `departmentCode` (query, string) — Department code (optional)
- `status` (query, "ACTIVE" | "CLOSED" | "EXPIRED" | "REVOKED") — Session status filter
- `page` (query, integer (int32)) — Page number (0-based)
- `size` (query, integer (int32)) — Page size

**Respostas**

- `200` Sessions retrieved successfully — `Page`
- `204` No sessions found for role — `PageSessionResponseDTO`

## Terminar sessão

`POST /api/admin/sessions/{sessionId}/kill`

**Parâmetros**

- `sessionId` (path, string (uuid), obrigatório) — Session ID

**Corpo do pedido** (`application/json`, `SessionKillRequestDTO`)

- `reason` (string, obrigatório)
- `killedBy` (string)

**Respostas**

- `204` Session killed successfully
- `404` Session not found

## Terminar sessões de um perfil

`POST /api/admin/sessions/roles/{roleCode}/kill`

Termina as sessões ativas dos utilizadores com esse perfil.

**Parâmetros**

- `roleCode` (path, string, obrigatório) — Role code
- `departmentCode` (query, string) — Department code (role codes are department-scoped)

**Corpo do pedido** (`application/json`, `SessionKillRequestDTO`)

- `reason` (string, obrigatório)
- `killedBy` (string)

**Respostas**

- `204` Sessions killed successfully
- `404` No sessions found for role

## Terminar sessões de um departamento

`POST /api/admin/sessions/departments/{departmentCode}/kill`

Termina as sessões ativas dos utilizadores desse departamento.

**Parâmetros**

- `departmentCode` (path, string, obrigatório) — Department code

**Corpo do pedido** (`application/json`, `SessionKillRequestDTO`)

- `reason` (string, obrigatório)
- `killedBy` (string)

**Respostas**

- `204` Sessions killed successfully

## Terminar uma sessão de um utilizador

`POST /api/admin/users/{userExternalId}/sessions/{sessionId}/kill`

Só termina a sessão se pertencer ao utilizador; caso contrário responde `404`.

**Parâmetros**

- `userExternalId` (path, string, obrigatório) — User external id
- `sessionId` (path, string (uuid), obrigatório) — Session id (UUID)

**Corpo do pedido** (`application/json`, `SessionKillRequestDTO`)

- `reason` (string, obrigatório)
- `killedBy` (string)

**Respostas**

- `204` Session killed
- `404` Session not found or does not belong to user

## Terminar todas as sessões de um utilizador

`POST /api/admin/users/{userExternalId}/logout-all`

**Parâmetros**

- `userExternalId` (path, string, obrigatório) — User external id

**Corpo do pedido** (`application/json`, `SessionKillRequestDTO`)

- `reason` (string, obrigatório)
- `killedBy` (string)

**Respostas**

- `204` Sessions revoked
- `404` User not found

## Forçar nova autenticação

`POST /api/admin/users/{userExternalId}/force-reauth`

Invalida todos os tokens emitidos até agora (incluindo os que estão em uso) e revoga as sessões ativas. Para usar, por exemplo, depois de uma reposição de palavra-passe.

**Parâmetros**

- `userExternalId` (path, string, obrigatório) — User external id

**Corpo do pedido** (`application/json`, `SessionKillRequestDTO`)

- `reason` (string, obrigatório)
- `killedBy` (string)

**Respostas**

- `204` Re-auth enforced
- `404` User not found

# Utilizador autenticado

URL: /pt/docs/developer/access-management/referencia-api/utilizador-autenticado

Dados, perfis, permissões e aplicações do próprio utilizador (`/me`).

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Obter o utilizador autenticado

`GET /api/users/me`

**Respostas**

- `200` OK — `IGRPUserDTO`

## Atualizar os próprios dados

`PUT /api/users/me`

Só altera os dados do próprio utilizador.

**Corpo do pedido** (`application/json`, `IGRPUserDTO`)

- `id` (string)
- `name` (string)
- `username` (string)
- `email` (string (email), obrigatório)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `picture` (string)
- `signature` (string)
- `nic` (string)
- `phoneNumber` (string)
- `emailVerified` (boolean)
- `metadata` (object)

**Respostas**

- `200` OK — `IGRPUserDTO`
- `404` User not found — `IGRPUserDTO`

## Os meus departamentos

`GET /api/users/me/departments`

**Parâmetros**

- `departmentCode` (query, string)

**Respostas**

- `200` OK — `DepartmentDTO`

## Os meus perfis

`GET /api/users/me/roles`

**Parâmetros**

- `departmentCode` (query, string)

**Respostas**

- `200` OK — `RoleDTO`

## Os meus perfis num departamento

`GET /api/users/me/departments/{departmentCode}/roles`

**Parâmetros**

- `roleCode` (query, string)
- `departmentCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleDTO`

## O meu perfil ativo

`GET /api/users/me/roles/active`

**Respostas**

- `200` OK — `RoleDepartmentDTO`

## Mudar o meu perfil ativo

`POST /api/users/me/roles/active`

Corpo `{ roleCode, departmentCode }`. Revoga as sessões: o token seguinte traz as permissões do novo perfil.

**Corpo do pedido** (`application/json`, `RoleDepartmentDTO`)

- `roleCode` (string)
- `departmentCode` (string)

**Respostas**

- `200` OK — `RoleDepartmentDTO`

## As minhas permissões

`GET /api/users/me/permissions`

**Parâmetros**

- `roleCode` (query, string)

**Respostas**

- `200` OK — `PermissionDTO`

## As minhas aplicações

`GET /api/users/me/applications`

**Parâmetros**

- `applicationCode` (query, string)
- `applicationName` (query, string)

**Respostas**

- `200` OK — `ApplicationDTO`

## Os meus menus numa aplicação

`GET /api/users/me/applications/{applicationCode}/menus`

**Parâmetros**

- `menuCode` (query, string)
- `applicationCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `MenuEntryDTO`

## Aplicações favoritas

`GET /api/users/me/applications/favorites`

**Parâmetros**

- `applicationCode` (query, string)
- `applicationName` (query, string)

**Respostas**

- `200` OK — `ApplicationDTO`

## Marcar aplicação como favorita

`POST /api/users/me/applications/favorites/{applicationCode}`

**Parâmetros**

- `applicationCode` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Retirar aplicação dos favoritos

`DELETE /api/users/me/applications/favorites/{applicationCode}`

**Parâmetros**

- `applicationCode` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Aplicações recentes

`GET /api/users/me/applications/recent`

**Parâmetros**

- `applicationCode` (query, string)
- `applicationName` (query, string)
- `max` (query, integer (int32))

**Respostas**

- `200` OK — `ApplicationDTO`

## Registar acesso a uma aplicação

`POST /api/users/me/applications/recent/{applicationCode}`

**Parâmetros**

- `applicationCode` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

# Utilizadores

URL: /pt/docs/developer/access-management/referencia-api/utilizadores

Gerir utilizadores, os seus perfis, o perfil ativo e as permissões.

Servidor: `http://{host}/gateway-api/{servico}-access-management`

## Listar utilizadores

`GET /api/users`

Permissão: `igrp.users.list`.

**Parâmetros**

- `applicationCode` (query, string)
- `departmentCode` (query, string)
- `name` (query, string)
- `id` (query, string)
- `email` (query, string)

**Respostas**

- `200` OK — `IGRPUserDTO`

## Obter utilizador

`GET /api/users/{id}`

Permissão: `igrp.users.view`.

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `IGRPUserDTO`

## Atualizar utilizador

`PUT /api/users/{id}`

Permissão: `igrp.users.update`.

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `IGRPUserDTO`)

- `id` (string)
- `name` (string)
- `username` (string)
- `email` (string (email), obrigatório)
- `status` ("ACTIVE" | "TEMPORARY" | "INACTIVE" | "DELETED")
- `picture` (string)
- `signature` (string)
- `nic` (string)
- `phoneNumber` (string)
- `emailVerified` (boolean)
- `metadata` (object)

**Respostas**

- `200` OK — `IGRPUserDTO`

## Mudar o estado do utilizador

`PUT /api/users/{id}/status`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `value` (query, string, obrigatório)
- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `IGRPUserDTO`

## Obter metadados do utilizador

`GET /api/users/{id}/metadata`

Metadados livres (JSON) do utilizador. O servidor de autorização usa-os para enriquecer o token.

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `UserMetadataDTO`

## Substituir metadados do utilizador

`PUT /api/users/{id}/metadata`

Substitui o JSON de metadados. Usado por operadores e pelos fluxos de federação OAuth.

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `UpdateUserMetadataRequestDTO`)

- `metadata` (object, obrigatório)

**Respostas**

- `200` OK — `UserMetadataDTO`

## Departamentos do utilizador

`GET /api/users/{id}/departments`

Permissão: `igrp.users.view`.

**Parâmetros**

- `departmentCode` (query, string)
- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `DepartmentDTO`

## Perfis do utilizador

`GET /api/users/{id}/roles`

Permissão: `igrp.users.view`.

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleDTO`

## Perfis do utilizador num departamento

`GET /api/users/{id}/departments/{departmentCode}/roles`

Permissão: `igrp.users.view`.

**Parâmetros**

- `roleCode` (query, string)
- `id` (path, string, obrigatório)
- `departmentCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleDTO`

## Atribuir perfis ao utilizador

`POST /api/users/{id}/departments/{departmentCode}/roles`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `id` (path, string, obrigatório)
- `departmentCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `AddRolesToUserRequestDTO`)

- `roles` (string[], obrigatório)
- `expiresAt` (string (date-time))

**Respostas**

- `200` OK — `RoleDTO`
- `201` Created — `RoleDTO`

## Retirar perfis ao utilizador

`DELETE /api/users/{id}/departments/{departmentCode}/roles`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `id` (path, string, obrigatório)
- `departmentCode` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `string[]`)

**Respostas**

- `200` OK — `RoleDTO`

## Perfil ativo do utilizador

`GET /api/users/{id}/roles/active`

Permissão: `igrp.users.manage`.

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `RoleDepartmentDTO`

## Mudar o perfil ativo do utilizador

`POST /api/users/{id}/roles/active`

Permissão: `igrp.users.manage`. Revoga as sessões do utilizador: o token seguinte traz as permissões do novo perfil.

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `RoleDepartmentDTO`)

- `roleCode` (string)
- `departmentCode` (string)

**Respostas**

- `200` OK — `RoleDepartmentDTO`

## Permissões do utilizador

`GET /api/users/{id}/permissions`

Permissão: `igrp.users.view`.

**Parâmetros**

- `roleCode` (query, string)
- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `PermissionDTO`

## Aplicações do utilizador

`GET /api/users/{id}/applications`

Permissão: `igrp.users.view`.

**Parâmetros**

- `applicationCode` (query, string)
- `applicationName` (query, string)
- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `ApplicationDTO`

## Menus do utilizador numa aplicação

`GET /api/users/{id}/applications/{applicationCode}/menus`

Permissão: `igrp.users.view`.

**Parâmetros**

- `menuCode` (query, string)
- `id` (path, string, obrigatório)
- `applicationCode` (path, string, obrigatório)

**Respostas**

- `200` OK — `MenuEntryDTO`

# Resolução de problemas

URL: /pt/docs/developer/access-management/referencia

Resolução de problemas, localização de ficheiros e documentos relacionados com a integração de Access Management.

# Referência [#referência]

### 6. Resolução de problemas [#6-resolução-de-problemas]

| Sintoma | Causa provável | Onde procurar |
|---|---|---|
| `PermissionsRegistry` não gerado | Processador de anotações em falta em `annotationProcessorPaths` | `pom.xml` — [secção 2.4 do Permissions Integration Guide](/docs/developer/framework/backend/permissoes#processador-anotacoes) |
| `[Authorization Sync] Failed…` em todos os arranques | Credenciais M2M erradas / em falta no backend | Ambiente do backend: `igrp.access.m2m.client-id`, `igrp.access.m2m.client-secret` |
| `[startup-sync] failed` em todos os arranques da SPA | O mesmo, mas no frontend | Ambiente do frontend: `IGRP_SERVICE_ID`, `IGRP_M2M_CLIENT_ID`, `IGRP_M2M_CLIENT_SECRET`, `IGRP_ACCESS_MANAGEMENT_API`. O framework apresenta isto como `IgrpConfigError(IGRP_ACCESS_MANAGEMENT_CONFIG_MISSING)` em `global-error.tsx`. |
| Ciclo de login com uma cadeia `?callbackUrl=…?callbackUrl=…` cada vez maior na barra de endereço | `NEXTAUTH_URL` não inclui `/api/auth` (e/ou `NEXT_PUBLIC_BASE_PATH` quando definido). O NextAuth v4 **não** acrescenta automaticamente. | Corrija `NEXTAUTH_URL` segundo as regras do ponto 4.2. Reinicie o pod da SPA (os cookies delimitados ao caminho errado mantêm-se até expirarem — limpe os cookies do browser para o domínio para desbloquear a sessão de desenvolvimento). |
| `PKCE_ERROR` ou `OAUTH_CALLBACK_ERROR` no callback | `NEXTAUTH_SECRET` mudou a meio do fluxo entre `/authorize` e `/callback`, OU o caminho do proxy mudou | Confirme que o segredo é estável entre pods, que `NEXTAUTH_URL` corresponde à origem da SPA onde o browser iniciou o fluxo, e que os cookies do caminho de callback estão de facto a ser enviados (problemas de SameSite / cross-origin). |
| O framework lança um erro no arranque em produção: `"NEXTAUTH_SECRET must be set"` | É exatamente o que diz — o NextAuth falha de forma irrecuperável em produção quando `NEXTAUTH_SECRET` não está definido | Defina-o. Gere com `openssl rand -base64 32`. |
| Pedido do scope `email` mas o token não inclui a claim | Scope não registado no cliente do AM (o valor por omissão do framework para `scopes_supported` é apenas `["openid"]`) | Adicione `email` ao scopes do cliente no AM, e confirme `IGRP_AUTH_SCOPES=openid email` na SPA. |
| 403 numa página mesmo com o utilizador a ter o perfil correto | `igrpAssertAuthorize` foi chamado com um nome de permissão simples, mas o JWT não tem claim `org` (fail-closed) | Confirme que o departamento ativo do utilizador está definido. Use a forma totalmente qualificada `"DEPT_X.suffix"` ao chamar fora do contexto de departamento atual do utilizador. |
| Componente com controlo de permissão continua a renderizar-se após a remoção de um perfil | O controlo por claim do token fica em cache até o TTL do token expirar — ver ponto 3.6 "Latência de revogação" | Aguarde pelo ciclo de renovação, ou force um novo login. A revogação em tempo real tem de ser aplicada na camada de API, não na UI. |
| A UI em modo de pré-visualização funciona mas a produção não | O modo de pré-visualização concede superadministrador — todos os controlos passam silenciosamente | Teste com `IGRP_PREVIEW_MODE=false` + `AUTH_PROVIDER=igrp-auth` contra um AM real com um utilizador que não seja superadministrador, antes de lançar. |
| A barra lateral da SPA aparece vazia | O utilizador não tem perfil ativo, OU o perfil não tem itens de menu | AC → Users → confirme o perfil ativo; AC → Menus → confirme `roles[]` nos itens de menu |
| A API devolve `401` em todos os pedidos | Incompatibilidade do `iss` do JWT — o `issuer-uri` do resource server não é igual ao `IGRP_OAUTH_ISSUER` do AM | Compare a claim `iss` do payload do JWT com o `spring.security.oauth2.resourceserver.jwt.issuer-uri` do backend da aplicação, ao carácter |
| A API devolve `403` apesar de o perfil ter a permissão | JWT desatualizado; JWT sem a permissão; ou erro de escrita no `@PreAuthorize` | Descodifique o bearer, verifique o array `permissions`; confirme que `T(Permission).*` corresponde a uma constante existente |
| Itens de menu aparecem brevemente e depois desaparecem | `syncOnCodeMenus=true` mas o administrador do AC tem estado a editar os menus manualmente | Ou desative a sincronização por código (`IGRP_SYNC_ON_CODE_MENUS=false`), ou opte pelo modo code-first e deixe de editar no AC |
| Explosão silenciosa de perfis no AM | Erros de escrita em `roles[].roleCode` em `menus.ts` + `syncRoles=true` | Procure perfis criados com conjunto de permissões vazio; corrija o erro de escrita ou defina `syncRoles=false` e pré-crie os perfis manualmente |
| Ciclo de redirecionamento após alteração de uma variável de ambiente do AM | Quase de certeza o padrão de incompatibilidade de `AUTH_JWT_ISSUER` documentado em análises no estilo `ANALISE-E-TASKS` | Verifique os registos do pod à procura da linha `WARN` do `OidcLoginFailureHandler` (ver o commit recente) — indica o `errorCode` exato |
| Após um redeploy do backend, os utilizadores veem `ACCESS_DENIED` em `/api/users/me` | O bug de "ressurreição" de utilizador ELIMINADO — corrigido nos commits recentes | Renove o JWT do utilizador (novo login) e confirme na build mais recente do AM |

Para o que não estiver aqui coberto, ative o registo TRACE (`org.springframework.security=TRACE`) no pod com falha — a linha de registo indica o filtro exato e a decisão tomada.

### 7. Referência — localização de ficheiros [#7-referência--localização-de-ficheiros]

| Assunto | Ficheiro |
|---|---|
| Exemplo de declaração de permissões | `access-management/backend/access-management-api/src/main/java/cv/igrp/platform/access_management/shared/infrastructure/authorization/permission/ApplicationPermissions.java` |
| Processador de anotações | `access-management/backend/modules/core/src/main/java/cv/igrp/framework/auth/core/processor/PermissionSourceGenerator.java` |
| Sincronização de arranque do backend | `access-management/backend/modules/core-spring-boot/src/main/java/cv/igrp/framework/auth/core/autoconfig/AuthorizationSyncRunner.java` |
| Sincronização de arranque do frontend | `shared/frontend/nextjs/packages/framework/next/src/lib/startup-sync.ts` |
| Layout raiz do frontend (gatilho de sincronização) | `shared/frontend/nextjs/packages/framework/next/src/layouts/igrp-root-layout.tsx` |
| Exemplo de fonte de menus (target) | `shared/frontend/nextjs/templates/demo-v1/src/temp/menus/menus.ts` |
| Fonte de menus (AC) | `access-management/frontend/application-center/src/temp/menus/menus.ts` |
| Cliente M2M (TS) | `access-management/frontend/packages/packages/client/src/client/m2m-client.ts` |
| API M2M (Java) | `access-management/backend/modules/client/src/main/java/cv/igrp/platform/access/client/api/M2MApi.java` |
| Propriedades do AM | `access-management/backend/access-management-api/src/main/resources/application.properties` |
| Configuração do AC | `access-management/frontend/application-center/src/igrp.template.config.ts` |

### 8. Documentos relacionados [#8-documentos-relacionados]

#### 8.1 Workspace do iGRP AM [#81-workspace-do-igrp-am]

<Cards>
  <Card
    title="Permissions Integration Guide"
    href="/docs/developer/framework/backend/permissoes"
    description="Documentação detalhada sobre @IgrpPermission, o annotation processor, expressões SpEL em @PreAuthorize, e a configuração do pom.xml do backend da aplicação."
  />
  <Card
    title="Clientes OAuth2 e contas de serviço"
    href="/docs/developer/access-management/m2m-api"
    description="Como as contas de serviço M2M são geridas via /api/service-accounts e SDK Java; a gestão de clientes OAuth2 e aplicações faz-se no Application Center."
  />
</Cards>

#### 8.2 Documentação oficial do framework (ramo dev) [#82-documentação-oficial-do-framework-ramo-dev]

Estes documentos residem dentro dos próprios repositórios do framework e são distribuídos de forma idêntica ao demo-v1 e ao Application Center. Quando este guia e a documentação do framework divergirem, a documentação do framework prevalece — abra uma issue de documentação neste workspace para corrigir a discrepância.

| Ficheiro | Conteúdo |
|---|---|
| `demo-v1/docs/ENVIRONMENT.md` | Referência completa de variáveis de ambiente, forma de validação, tabela de registo de URIs de callback/logout. |
| `demo-v1/docs/AUTHENTICATION.md` | `withIGRPAuth`, o modelo de bypass `AUTH_PROVIDER=none` / `IGRP_PREVIEW_MODE=true`, `sanitizeCallbackUrl`, o fluxo autenticado em seis passos. |
| `demo-v1/docs/ACCESS_MANAGEMENT.md` | O "aperto de mão" OAuth2 `client_credentials` com o AM, as três fases de sincronização, e a superfície de falha do `IgrpConfigError`. |
| `demo-v1/docs/PERMISSIONS.md` | O modelo completo de controlo por claims do JWT: `igrpAssertAuthorize`, `<IGRPAuthorization>`, `usePermissions`, a tabela de camadas de aplicação, a latência de revogação, a checklist de "sem recusa por omissão". |
| `demo-v1/docs/TOKENS.md` | O fluxo de access-token / refresh-token / id-token entre o NextAuth e o AM. |
| `demo-v1/docs/ARCHITECTURE.md` | A forma do modelo em quatro camadas (middleware → layout raiz → `(igrp)/layout.tsx` → construtor de configuração). |
| `demo-v1/docs/MIGRATION_GUIDE.md` | Atualizações do framework de versão para versão. |

Os mesmos ficheiros são distribuídos dentro do Application Center em `application-center/docs/*.md`.


# Sincronização

URL: /pt/docs/developer/access-management/sincronizacao

Como o backend sincroniza permissões e o frontend sincroniza menus com o Access Management.

# Sincronização de Permissões e Menus [#sincronização-de-permissões-e-menus]

<Tabs>

<TabItem value="backend" label="Backend — Permissões">

### 2. Sincronização de Permissões (Backend da Aplicação) [#2-sincronização-de-permissões-backend-da-aplicação]

O pipeline `@IgrpPermission` → processador de anotações → `AuthorizationSyncRunner` está totalmente documentado no guia complementar [Permissions Integration Guide](/docs/developer/framework/backend/permissoes). Esta secção é o resumo executivo.

#### 2.1 Declarar permissões no código [#21-declarar-permissões-no-código]

```java
// src/main/java/cv/igrp/demo/shared/authorization/InvoicePermissions.java
public final class InvoicePermissions {
    private InvoicePermissions() {}

    @IgrpPermission(name = "my.invoice.create", description = "Create an invoice")
    public static final String INVOICE_CREATE = "my.invoice.create";

    @IgrpPermission(name = "my.invoice.approve", description = "Approve an invoice")
    public static final String INVOICE_APPROVE = "my.invoice.approve";
}
```

- A anotação possui retenção `SOURCE`. Está presente apenas durante a compilação.
- O annotation processor `PermissionSourceGenerator` processa todas as `@IgrpPermission` e gera um único enum `cv.igrp.framework.auth.generated.PermissionsRegistry` em `target/generated-sources/annotations/`.
- O valor de cada constante `String` deve corresponder exatamente ao valor retornado por `name()`. Isso permite que as IDEs ofereçam autocompletação das permissões em expressões `@PreAuthorize`.

#### 2.2 Sincronizar o arranque [#22-sincronizar-o-arranque]

O `AuthorizationSyncRunner`, disponível no módulo `cv.igrp.framework.auth:core-spring-boot`, é executado durante a fase de `@PostConstruct` e realiza as seguintes:

- Obtém um token M2M junto do AM, efetuando um pedido `POST /oauth2/token` com `grant_type=client_credentials`, e utilizando as credenciais configuradas em `igrp.access.m2m.client-id` e `igrp.access.m2m.client-secret`.
- Regista a aplicação como um Resource, através de um pedido `POST /api/m2m/sync/resources`.
- Envia todas as constantes do enum `PermissionsRegistry.Permission` como objetos `PermissionDTO`, através de um pedido `POST /api/m2m/sync/permissions`.

<Callout type="info" title="Comportamento da sincronização">
  A sincronização é idempotente e segue uma abordagem de **melhor esforço** (*best effort*):

  - As permissões já existentes são atualizadas (por exemplo, para refletir alterações na descrição).
  - As novas permissões são registadas automaticamente.
  - As permissões removidas do código **não são eliminadas do AM**. Esta remoção é intencional, uma vez que essas permissões podem já estar atribuídas a perfis (*roles*). A sua eliminação deve ser efetuada manualmente por um administrador.

  Se o AM estiver indisponível, o backend da aplicação regista o erro com o nível `ERROR` e continua o processo de arranque. A aplicação não interrompe a inicialização, uma vez que uma falha na sincronização não deve impedir que os serviços consumidores sejam iniciados.
</Callout>

#### 2.3 Aplicar em tempo de pedido [#23-aplicar-em-tempo-de-pedido]

```java
@PreAuthorize("@igrpAuthorization.checkPermission(T(Permission).MY_INVOICE_CREATE)")
@PostMapping("/api/invoices")
public InvoiceDTO create(@RequestBody InvoiceDTO dto) { … }
```

- `@igrpAuthorization` refere-se ao bean `IgrpAuthorizationService`.
- A expressão `T(Permission)` é resolvida através de `MethodSecurityConfig.IgrpTypeLocator`, que pré-regista o pacote `cv.igrp.framework.auth.generated`.
- O código da permissão é recebido no JWT como uma "_granted authority_" em formato de `String`, sem qualquer prefixo. Essa autoridade é populada pelo `IgrpJwtAuthenticationConverter` a partir do claim `permissions` presente no token.

#### 2.4 Controladores gerados pelo Studio [#24-controladores-gerados-pelo-studio]

Nos **controllers** gerados pelo iGRP Studio, não edite o código-fonte para adicionar `@PreAuthorize`, pois essas alterações serão perdidas na próxima regeneração. Defina a permissão no separador **Security** do IGRP Studio. O gerador de código emitirá automaticamente a anotação `@PreAuthorize` correspondente durante a regeneração.

</TabItem>

<TabItem value="frontend" label="Frontend — Menus">

### 3. Sincronização de Menus (Frontend da Aplicação) [#3-sincronização-de-menus-frontend-da-aplicação]

Os menus são definidos no **frontend**, porque é aí que a árvore de rotas da SPA é a fonte de verdade. O AM apenas mantém uma cópia sincronizada dessa informação; o backend nunca cria nem define menus.

#### 3.1 Declarar menus no código [#31-declarar-menus-no-código]

Todos os frontends da aplicação têm um ficheiro de menus canónico:

```typescript
// src/temp/menus/menus.ts
import type { IGRPMenuItemArgs } from '@igrp/framework-next';

export const onCodeMenus: IGRPMenuItemArgs[] = [
  {
    code: 'invoices',
    name: 'Invoices',
    type: 'FOLDER',
    icon: 'FileText',                 // nome do ícone lucide
    position: 10,
    status: 'ACTIVE',
    applicationCode: 'INVOICING',
    roles: [                           // vazio = invisível para todos nesta app
      { roleCode: 'INV.reader',   departmentCode: 'FIN' },
      { roleCode: 'INV.approver', departmentCode: 'FIN' },
    ],
  },
  {
    code: 'invoices.list',
    name: 'All Invoices',
    type: 'MENU_PAGE',
    pageSlug: 'invoices',              // → /invoices
    position: 10,
    status: 'ACTIVE',
    parentCode: 'invoices',            // aninhado sob a pasta "invoices"
    applicationCode: 'INVOICING',
    roles: [{ roleCode: 'INV.reader', departmentCode: 'FIN' }],
  },
  {
    code: 'invoices.new',
    name: 'New Invoice',
    type: 'MENU_PAGE',
    pageSlug: 'invoices/new',          // → /invoices/new
    position: 20,
    status: 'ACTIVE',
    parentCode: 'invoices',
    applicationCode: 'INVOICING',
    roles: [{ roleCode: 'INV.approver', departmentCode: 'FIN' }],
  },
  {
    code: 'reports.external',
    name: 'Financial Report',
    type: 'EXTERNAL_PAGE',
    url: 'https://reports.example.com/finance',
    target: '_blank',
    position: 100,
    status: 'ACTIVE',
    applicationCode: 'INVOICING',
    roles: [{ roleCode: 'INV.approver', departmentCode: 'FIN' }],
  },
];
```

**Estrutura do item de menu**

| Campo | Tipo | Notas |
|---|---|---|
| `code` | `string` | Único dentro da aplicação. Referenciado pelos filhos através de `parentCode`. Kebab-case ou notação por pontos, ambos válidos. |
| `name` | `string` | Etiqueta legível apresentada na barra lateral. |
| `type` | `'MENU_PAGE' \| 'FOLDER' \| 'EXTERNAL_PAGE' \| 'GROUP' \| 'SYSTEM_PAGE'` | Ver abaixo. |
| `position` | `number` | Ordem entre itens do mesmo nível. Uma numeração espaçada (10, 20, 30) deixa espaço para inserções futuras. |
| `icon` | `string?` | Nome do ícone Lucide (`FileText`, `Users`, `Settings`, `Bot`, …). |
| `status` | `'ACTIVE' \| 'INACTIVE' \| 'DELETED'` | Os itens não-`ACTIVE` são enviados para o AM mas ficam ocultos dos utilizadores. |
| `target` | `'_self' \| '_blank'` | Para `EXTERNAL_PAGE`. |
| `pageSlug` | `string \| null` | Slug de rota do NextJS, sem barra inicial. Preferível a `url` para páginas internas. |
| `url` | `string \| null` | URL absoluto. Usar apenas para `EXTERNAL_PAGE`. |
| `parentCode` | `string \| null` | `null` = nível de topo. Deve referenciar o `code` de um item irmão no mesmo ficheiro. |
| `applicationCode` | `string` | O código da app a que este menu pertence. Deve corresponder a `IGRP_APP_CODE` (ponto 4). |
| `roles` | `{ roleCode; departmentCode }[]` | Visibilidade RBAC. **Array vazio = não visível para nenhum utilizador** (a entrada fica oculta da navegação — a query filtra por `INNER JOIN` em `t_menu_entry_roles`). Não vazio = visível apenas para utilizadores cujo **perfil ativo** (`active_role_id`, não a união dos perfis) corresponda a qualquer entrada. Exceção: um utilizador com a role `IGRP_SUPER_ADMIN` recebe todos os menus da aplicação, ignorando o filtro. |

**Tipos de Menu:**

- `FOLDER` — Nó de agrupamento não clicável. É apresentado na barra lateral como uma secção expansível.
- `GROUP` — Semelhante a `FOLDER` mas é apresentado como um cabeçalho de secção que separa grupos de itens, em vez de um nó expansível.
- `MENU_PAGE` — Item de navegação interna clicável. Utiliza o `pageSlug` (por exemplo `/settings/users` → slug `settings/users`).
- `EXTERNAL_PAGE` — Item de navegação externa clicável. Utiliza os campos `url` e `target`.
- `SYSTEM_PAGE` — Página interna de utilidade (por exemplo, dashboard ou perfil). Não é apresentada na navegação principal, mas continua disponível como uma rota interna acessível.

<Callout type="info">
  O array é **_flat_**; a hierarquia é construída durante a renderização com base nos campos `parentCode` e `position`. Esta abordagem torna a inserção de novos itens mais eficiente (adicionar um filho não exige alterações ao elemento pai) e facilita a comparação de alterações (_diffs_) entre versões.
</Callout>

#### 3.2 Ligar os Menus ao Layout [#32-ligar-os-menus-ao-layout]

```typescript
// src/igrp.template.config.ts
import { IGRPConfigArgs } from '@igrp/framework-next';
import { onCodeMenus } from '@/temp/menus/menus';

export async function igrpTemplateConfig(): Promise<IGRPConfigArgs> {
  return {
    app: {
      appCode: process.env.IGRP_APP_CODE!,
      previewMode: process.env.IGRP_PREVIEW_MODE === 'true',
      syncAccess: process.env.IGRP_SYNC_ACCESS === 'true',
    },
    accessManagement: {
      apiBaseUrl: process.env.IGRP_ACCESS_MANAGEMENT_API!,
      // O cliente M2M do framework lê IGRP_SERVICE_ID + IGRP_M2M_CLIENT_ID +
      // IGRP_M2M_CLIENT_SECRET diretamente de process.env — ver ponto 4.4. Não é
      // necessária nenhuma configuração adicional aqui. O modo legado de
      // token estático (IGRP_M2M_TOKEN) é respeitado por compatibilidade
      // retroativa, mas está descontinuado; migre para client_credentials.
      syncOnCodeMenus: process.env.IGRP_SYNC_ON_CODE_MENUS === 'true',
      onCodeMenus,
    },
    // …
  };
}
```

```typescript
// src/app/layout.tsx
import { IGRPRootLayout } from '@igrp/framework-next';
import { igrpTemplateConfig } from '@/igrp.template.config';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const config = await igrpTemplateConfig();
  return <IGRPRootLayout config={config}>{children}</IGRPRootLayout>;
}
```

<Callout type="success">
  Esta é toda a configuração necessária. A partir daqui, o framework assume o restante do processo.
</Callout>

#### 3.3 O que acontece no arranque (o fluxo de sincronização) [#33-o-que-acontece-no-arranque-o-fluxo-de-sincronização]

O componente `IGRPRootLayout` é executado na primeira renderização da aplicação após um arranque a frio (_cold boot_). A sua lógica, implementada em `shared/frontend/nextjs/packages/framework/next/src/layouts/igrp-root-layout.tsx`, realiza as seguintes operações:

- Valida a configuração de forma síncrona, adotando uma abordagem _fail-fast_. A inicialização é interrompida imediatamente caso existam configurações obrigatórias em falta, como `appCode` ou `apiBaseUrl`.
- Agenda a execução de `igrpStartupSync(config)` através da função `after()` do Next.js. A sincronização é iniciada apenas após o envio da resposta HTTP ao cliente, garantindo que o utilizador não tenha de esperar pela sua conclusão.
- Evita sincronizações duplicadas durante renderizações concorrentes. Uma `Promise` definida ao nível do módulo garante que a sincronização é executada apenas uma vez durante o ciclo de vida da aplicação.

A implementação de `igrpStartupSync` encontra-se em `shared/frontend/nextjs/packages/framework/next/src/lib/startup-sync.ts`.

```text
1. If !config.app.syncAccess               → skip everything (dev / preview).
2. Fetch OAuth2 M2M token via ApiClient.setClientCredentials(...).
3. syncApplication({
      code:       config.app.appCode,
      name:       config.app.appName ?? config.app.appCode,
      type:       'INTERNAL',
      description:'Auto-registered by IGRP framework on startup',
   })
     → POST /api/m2m/sync/applications
4. If config.accessManagement.syncOnCodeMenus && onCodeMenus?.length:
      syncApplicationMenus(config.app.appCode, mapToMenuEntryDTO(onCodeMenus), syncRoles=true)
        → POST /api/m2m/sync/applications/{appCode}/menus?syncRoles=true
```

**Idempotência:**

- **Sincronização da aplicação:** atualização (*upsert*) com base no `code`. O `name` e a `description` são atualizados, enquanto o `type` é preservado.
- **Sincronização dos menus:** substituição completa da árvore de menus da aplicação identificada por `applicationCode`. Os itens que não estiverem presentes no pedido são eliminados do AM. Os itens existentes com o mesmo `code` são atualizados e os novos são inseridos.
- **Perfis (Roles):** quando `syncRoles=true` (valor predefinido no `IGRPRootLayout`), qualquer `roleCode` presente no `roles[]` de um item de menu que ainda não exista para o departamento de destino é criado automaticamente com o estado `ACTIVE` e um conjunto de permissões vazio. Este comportamento permite que um projeto novo seja inicializado sem configuração prévia, ou seja, não é necessário criar previamente os perfis no AC para que a SPA possa arrancar. Como contrapartida, um `roleCode` escrito incorretamente cria silenciosamente um novo perfil vazio. Por isso, recomenda-se verificar os registos (*logs*) de sincronização durante o primeiro arranque.
- **Importante:** esta sincronização corresponde a uma substituição completa da árvore de menus. Se um item for removido do ficheiro `menus.ts` e a aplicação for publicada, esse item será eliminado do AM no arranque seguinte. Este é o comportamento esperado, uma vez que o código-fonte constitui a fonte de verdade (*source of truth*).

<Callout type="info" title="Tratamento de falhas">
  O comportamento é idêntico ao da sincronização das permissões do backend: a sincronização é efetuada em modo de **melhor esforço** (*best-effort*) e as falhas não impedem a execução da aplicação.

  Se o AM estiver indisponível, a SPA continua a ser renderizada normalmente e a sincronização será novamente tentada no próximo arranque a frio (*cold boot*).

  Para diagnosticar problemas de sincronização, consulte as entradas `[startup-sync] WARN` nos registos (*logs*) do servidor Next.js.
</Callout>

#### 3.4 Renderização de Menus em Runtime (filtro por perfil) [#34-renderização-de-menus-em-runtime-filtro-por-perfil]

Após a sincronização, o AM disponibiliza a árvore de menus efetiva para cada utilizador através do endpoint `GET /api/users/me/applications/{applicationCode}/menus` (o Application Center utiliza o mesmo endpoint).

A resposta já é filtrada no lado do servidor: o backend devolve apenas os itens cujo `roles[]` tem interseção com o perfil ativo do utilizador. A SPA não realiza qualquer filtragem no lado do cliente e, por isso, não consegue aceder a itens para os quais o utilizador não tenha permissões. Os utilizadores com perfil de **Superadministrador** têm acesso a todos os itens do menu.

O componente de barra lateral (*sidebar*) do framework consome automaticamente esta resposta e renderiza a árvore de menus, não sendo necessária qualquer implementação adicional no projeto de destino.

#### 3.5 Edição de menus a partir da UI de administração [#35-edição-de-menus-a-partir-da-ui-de-administração]

O ecrã `/settings/menus` do Application Center utiliza a mesma estrutura `MenuEntryDTO` e permite aos administradores adicionar, remover e reordenar itens de menu sem necessidade de efetuar uma nova publicação (*redeploy*). Contudo, essas alterações manuais apenas são preservadas após a sincronização baseada no código quando se verificam as seguintes condições:

- O `code` do item não entra em conflito com o `code` de um item definido no código-fonte. Em caso de conflito, prevalece sempre a definição existente no código.
- O `applicationCode` pertence a uma aplicação cujo frontend está configurado com `syncOnCodeMenus=false`, desativando a sincronização automática dos menus. Esta configuração destina-se a aplicações que gerem integralmente os menus através da interface de administração.

Na prática, a maioria dos projetos adota uma abordagem *code-first*, com `syncOnCodeMenus=true`: os menus são definidos no ficheiro `menus.ts` e propagados através de uma nova publicação da aplicação. Neste cenário, a interface de administração é utilizada sobretudo para fins de monitorização e consulta, sendo as alterações em tempo de execução reservadas para casos excecionais.

#### 3.6 Controlo de acesso no frontend (baseado em claims do JWT, sem rede) [#36-controlo-de-acesso-no-frontend-baseado-em-claims-do-jwt-sem-rede]

A visibilidade dos menus constitui apenas uma das camadas de controlo de acesso, ocultando os itens da barra lateral. No entanto, um utilizador pode tentar aceder diretamente a uma página introduzindo o respetivo URL.

Para lidar com este cenário, o framework disponibiliza um mecanismo independente de controlo de acesso ao nível da página e dos componentes. Este mecanismo baseia-se na descodificação das claims do token JWT, que já acompanha cada pedido, não sendo necessárias chamadas adicionais ao backend.

**Estrutura das claims**

O access token emitido pelo AM inclui as seguintes claims no respetivo payload (após descodificação):

```json
{
  "org": "DEPT_IGRP",                                       // departamento ativo
  "selectedRole": "DEPT_IGRP.superadmin",
  "is_super_admin": true,                                   // ultrapassa todas as verificações
  "resource_access": { "igrp-access-management": { "roles": ["DEPT_IGRP.superadmin"] } },
  "permissions": ["DEPT_IGRP.manage_access"]                // todos os departamentos; forma qualificada `${dept}.${suffix}`
}
```

##### Regra de correspondência [#regra-de-correspondência]

O mecanismo `claimsAllow` determina o acesso com base nas *claims* presentes no token JWT, aplicando as seguintes regras:

- Se `is_super_admin === true`, o acesso é concedido a todos os recursos.
- Se o nome da permissão contiver um ponto (`.`), ou seja, estiver no formato `departamento.permissão`, a correspondência é feita diretamente com os valores presentes no array `permissions[]`.
- Se o nome da permissão for fornecido sem o prefixo do departamento (por exemplo, `"manage_access"`), o framework qualifica automaticamente a permissão com a organização ativa, gerando o formato `${org}.${name}`, e efetua a correspondência com o array `permissions[]`.
- Se a permissão for fornecida sem o prefixo do departamento e o token não contiver informação sobre a organização ativa (`org`), o acesso é negado. Este comportamento segue o princípio **fail-closed**, ou seja, na ausência de informação suficiente, o acesso é recusado por defeito.

##### Configuração inicial [#configuração-inicial]

No projeto `demo-v1`, esta configuração já se encontra efetuada.

<Steps>
  <Step>
    **Ativar `forbidden()` no `next.config.ts`:**

    ```typescript
    experimental: { authInterrupts: true }
    ```
  </Step>
  <Step>
    **Inicializar o provider no layout autenticado.** No layout autenticado (`src/app/(igrp)/layout.tsx`), inicialize o provider responsável pelo controlo de permissões:

    ```typescript
    import { IGRPLayoutFull, igrpGetClaims } from "@igrp/framework-next";
    import { IGRPSectionPermissions } from "@igrp/framework-next-ui";

    await verifySession();           // TEM de correr antes de igrpGetClaims — semeia o token do pedido
    const claims = await igrpGetClaims();
    return (
      <IGRPSectionPermissions state={claims}>
        <IGRPLayoutFull config={config}>{children}</IGRPLayoutFull>
      </IGRPSectionPermissions>
    );
    ```
  </Step>
  <Step>
    **Adicionar uma página de erro 403 (`forbidden`).** Crie uma página de erro **403 (Acesso Proibido)** no ficheiro `src/app/(igrp)/forbidden.tsx` para tratar tentativas de acesso a recursos para os quais o utilizador não possui permissões.

    ```typescript
    import { IGRPForbidden } from "@igrp/framework-next-ui";
    export default function Forbidden() { return <IGRPForbidden />; }
    ```
  </Step>
</Steps>

##### Utilização no dia a dia [#utilização-no-dia-a-dia]

O framework disponibiliza quatro APIs para o controlo de permissões, adequadas a diferentes cenários de utilização:

| Superfície | Onde | O que faz |
|---|---|---|
| `await igrpAssertAuthorize("manage_access")` | Componente de servidor (página) | Em falta → `forbidden()` → UI de 403. Este é o verdadeiro portão de renderização; só no servidor; não pode ser contornado do lado do cliente. |
| `if (!(await igrpAuthorize("delete_invoice"))) …` | Server action | Verificação booleana para usar dentro de uma server action antes de invocar o AM. |
| `<IGRPAuthorization permission="delete_invoice" fallback={…}>{…}</IGRPAuthorization>` | Componente de cliente | Renderiza `children` quando permitido, `fallback` (ou desmonta) quando recusado. Cosmético — não confiar nisto para segurança. |
| `const { isAllowed } = usePermissions(); depois isAllowed("read_invoice")` | Componente de cliente | Hook, para quando se quer desativar em vez de ocultar. |

- **Verificação de múltiplas permissões:** é possível fornecer um array de permissões, por exemplo, `<IGRPAuthorization permission={["a", "b"]} mode="all" | "any">`. Por predefinição, o parâmetro `mode` assume o valor `"all"`, exigindo que o utilizador possua todas as permissões indicadas. Quando definido como `"any"`, basta que possua pelo menos uma.
- **Especificação dos nomes das permissões:** na maioria dos casos, deve ser utilizado apenas o sufixo da permissão (por exemplo, `"manage_access"`). O framework adiciona automaticamente o prefixo correspondente à organização ativa presente no token JWT. Uma permissão totalmente qualificada, como `"DEPT_X.manage_access"`, deve ser utilizada apenas quando for necessário validar explicitamente permissões de outro departamento.

##### Camadas de controlo de acesso [#camadas-de-controlo-de-acesso]

É importante compreender que o controlo de acesso é efetuado em diferentes camadas, cada uma com uma responsabilidade específica:

| Âmbito | Controlo | Robustez |
|---|---|---|
| Componente `<IGRPAuthorization>` / `usePermissions()` (cliente, a partir do token) | Cosmético — ocultar/mostrar UI | Menu |
| Filtragem do lado do servidor no AM de `/api/users/me/applications/{app}/menus` | Navegação de confiança do servidor | Página |
| `igrpAssertAuthorize(name)` → `forbidden()` (servidor, a partir do token) | Portão de renderização aplicado | Ação atrás de um controlo |
| `igrpAuthorize(name)` na server action, E `@PreAuthorize` no backend da aplicação | Aplicação real | |

O controlo de acesso baseado nas claims do JWT é rápido e conveniente, mas a fonte de verdade continua a ser o backend da aplicação, através de mecanismos como `@PreAuthorize`. A proteção no frontend não substitui a validação no servidor.

O framework **não aplica uma política de negação por omissão (default-deny)**. A proteção das páginas é opcional (opt-in): se `igrpAssertAuthorize` não for utilizado numa página, esta permanecerá acessível, mesmo que o respetivo menu esteja oculto.

**Latência de revogação.** Os controlos leem claims em cache do token. Uma revogação de permissão do lado do servidor não se reflete até o token ser renovado (TTL curto) ou o utilizador voltar a fazer login. Para operações que precisem de bloquear o acesso de forma instantânea, aplique a verificação na API do backend da aplicação (que está sempre atualizada — volta a ler a base de dados, não o token).

**Modo de pré-visualização / bypass** (ponto 4.2 / ponto 4.4 "Modo de pré-visualização / bypass de autenticação"). As claims resolvem-se como superadministrador, pelo que todos os controlos passam. Para ver um estado recusado ou a página de 403, execute com a pré-visualização desligada contra um backend real com um utilizador sem a permissão.

##### Referência de API [#referência-de-api]

O framework disponibiliza as seguintes APIs para implementação do controlo de permissões:

| Símbolo | Pacote | Finalidade |
|---|---|---|
| `igrpGetClaims()` | `@igrp/framework-next` | Descodifica as claims do pedido; semeia o fornecedor |
| `igrpAuthorize(name)` | `@igrp/framework-next` | Verificação booleana (server actions / lógica do lado do servidor) |
| `igrpAssertAuthorize(name)` | `@igrp/framework-next` | Proteção de página → `forbidden()` em caso de recusa |
| `IGRPSectionPermissions` | `@igrp/framework-next-ui` | Fornecedor do lado do cliente; semear com `igrpGetClaims()` |
| `usePermissions()` | `@igrp/framework-next-ui` | `{ isAllowed, permissions, roles, selectedRole, isSuperAdmin, status, error }` |
| `<IGRPAuthorization>` | `@igrp/framework-next-ui` | Envolve os filhos para mostrar/ocultar consoante a permissão |
| `<IGRPGuardPage>` | `@igrp/framework-next-ui` | Proteção de página do lado do cliente (conveniência — a proteção do servidor é a autoritativa) |
| `IGRPForbidden` | `@igrp/framework-next-ui` | UI de 403 para `forbidden.tsx` |

</TabItem>

</Tabs>


# Instalação via ArgoCD (GitOps)

URL: /pt/docs/developer/cloud/argocd

Deployment do igrp-stack por GitOps — repositório de deployment, modelo de segredos por camadas e as armadilhas do sync.

# Instalação via ArgoCD (GitOps) [#instalação-via-argocd-gitops]

Modelo de deployment em que se copia o `Chart.yaml` e um `values.yaml` para um repositório git de deployment, se editam os parâmetros do ambiente, e o ArgoCD sincroniza com o cluster. Os subcharts são resolvidos do registry OCI.

---

## 1. Pré-requisitos [#1-pré-requisitos]

### Subcharts publicados no registry [#subcharts-publicados-no-registry]

Todos os subcharts `igrp-*` e o umbrella têm de existir em `oci://registry.nosi.cv/igrp-charts`. O CI do repositório do chart trata disso: job manual `publish-all` para bootstrap, mais publicação automática no merge.

### Registry OCI registado no ArgoCD [#registry-oci-registado-no-argocd]

Sem isto, o `helm dependency build` do repo-server falha ao puxar os subcharts.

```bash
argocd repo add registry.nosi.cv --type helm --enable-oci \
  --name igrp-charts --username <user> --password <pass>
```

Ou declarativo, como Secret no namespace do ArgoCD:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: igrp-charts-oci
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  name: igrp-charts
  url: registry.nosi.cv
  type: helm
  enableOCI: "true"
  username: <user>
  password: <pass>
```

### Cluster [#cluster]

Operador `cloudnative-pg`, StorageClass (Longhorn / Azure Disk) e `nginx` IngressController — ver a [visão geral](./index.mdx).

---

## 2. Repositório de deployment [#2-repositório-de-deployment]

Estrutura mínima por ambiente:

```text
deployments/
└── igrp-stack/
    ├── Chart.yaml          ← CÓPIA do Chart.yaml do igrp-stack (não editar deps)
    ├── values.yaml         ← parâmetros do ambiente (domínios, réplicas, toggles…)
    └── sealed-secrets/     ← só em produção
```

O `Chart.yaml` copia-se **tal e qual**: as dependências apontam para o OCI com versão fixa, e assim os paths dos values ficam idênticos aos de toda a documentação (`igrp-access-management.oauth…`). Para o `values.yaml`, parte dos ficheiros do chart e ajusta os três domínios `global.*`.

### A Application [#a-application]

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: igrp-stack
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://<servidor-git>/<grupo>/<repo-deployments>.git
    targetRevision: main
    path: deployments/igrp-stack
  destination:
    server: https://kubernetes.default.svc
    namespace: igrp
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true        # ConfigMap grande do realm do Keycloak
    # automated: { prune: true, selfHeal: true }   # opt-in
  ignoreDifferences:
    - group: postgresql.cnpg.io
      kind: Cluster
      jsonPointers: [ /status ]
```

<Callout type="note" title="Comportamento dos Jobs no primeiro sync">
O ArgoCD converte hooks `post-install` em **PostSync**, que só corre quando **todos** os recursos estão Healthy. Num sync inicial de raiz, os process backends não ficam Healthy sem os Secrets e BDs que estes Jobs criariam — pode ficar circular. O chart já mitiga isto com `argocd.argoproj.io/hook: Sync` e a coreografia de waves. No fluxo `helm upgrade --install` pelo CLI esta limitação não existe.
</Callout>

---

## 3. Segredos — modelo por camadas [#3-segredos--modelo-por-camadas]

O chart cria por default os Secrets a partir dos values (`create: true`). Para GitOps há toggles que desligam essa criação, e o chart passa a apenas **referenciar** Secrets pré-existentes.

| Secret | Toggle nos values | Chaves esperadas |
| --- | --- | --- |
| `igrp-*-db-user` (4, ou 6 com notificações) | `igrp-pg-cluster.dbUsers.createSecrets: false` | `username`/`password`, type `kubernetes.io/basic-auth`, label `cnpg.io/reload: "true"` |
| `igrp-access-management-secrets` | `igrp-access-management.secret.create: false` | `smtp-password` |
| `igrp-access-management-oauth-client` | `igrp-access-management.oauthClient.create: false` | `client-id`/`client-secret` |
| `igrp-access-management-oauth-keys` | já é sempre `existingSecret` | `public.pem`/`private.pem` — via `scripts/generate-oauth-keys.sh` |
| `igrp-minio-creds` | `igrp-minio.credsSecret.create: false` | `MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD` |
| `igrp-app-center-nextauth` | `igrp-app-center.nextauth.existingSecret` | `nextauth-secret` — **partilhado por TODOS os frontends Next.js** |
| nextauth das process UIs | **nenhum** — apontam por default ao Secret do app-center | `nextauth-secret` |
| `igrp-keycloak-admin` | `igrp-keycloak.keycloak.auth.existingSecret` | `admin-password` |
| `igrp-redis-auth` | `igrp-redis.authSecret.create: false` (prod) ou password explícita (dev) | `redis-password` |
| `igrp-process-*-oauth-client` (4) | já é sempre `existingSecret`, ou Job `m2m.autoRegister` | `client-id`/`client-secret` |
| `igrp-notification-{api,ui}-oauth-client` (2) | idem | `client-id`/`client-secret` |

<Callout type="warning" title="Sem authSecret explícito, a password do Redis roda a cada sync">
O `lookup` + random do wrapper gera uma password nova em cada sincronização do ArgoCD. Define `authSecret.create: false` (produção) ou `authSecret.password` (dev).
</Callout>

<Callout type="warning" title="O mapa dbUsers.users exige password mesmo com o módulo desligado">
As entradas `notification` e `notificationTemporal` vêm declaradas no `values.yaml`. A validação `validate-db-users` exige password a cada entrada do mapa **mesmo com o módulo de notificações desligado**, e um `null` no values do ambiente não as remove. Em GitOps: `dbUsers.createSecrets: false` e pré-criar os dois Secrets, ou dar-lhes password no values do ambiente.
</Callout>

<Callout type="danger" title="Excepção: o clientSecret do Keycloak não pode vir de um Secret">
O `igrp-keycloak.igrp.clientSecrets.accessManagement` é renderizado para dentro do `realm_config.json`, que é um ConfigMap. Só é usado no modo de federação Keycloak. As opções são: aceitar este único valor no values, ou importar o realm com um valor temporário e definir o secret real na consola do Keycloak (`kcadm`) depois do primeiro arranque.
</Callout>

### 3.1 Dev e stage — segredos nos values [#31-dev-e-stage--segredos-nos-values]

Manter os defaults (`create: true`) e preencher os valores no `values.yaml` do repo de deployment. Aceitável **apenas** porque o repo é privado e as credenciais são de ambientes de teste. Nunca com credenciais de produção.

### 3.2 Produção — Sealed Secrets [#32-produção--sealed-secrets]

Ver [Instalação de produção](./producao.mdx) para o procedimento completo. Em resumo: instalar o controller, guardar o backup da chave privada fora do git, gerar os SealedSecrets com `scripts/bootstrap-secrets.sh --seal`, commitar a pasta cifrada, e ligar os toggles no values de produção.

<Callout type="note" title="O ArgoCD não mistura chart e manifests soltos no mesmo source">
Se a pasta `sealed-secrets/` estiver dentro do `path` de um Application com chart Helm, cria um segundo Application (ou um segundo source) para ela.
</Callout>

### 3.3 Sem ferramenta — bootstrap manual [#33-sem-ferramenta--bootstrap-manual]

O mesmo script sem `--seal` cria os Secrets directamente no cluster, uma vez por ambiente, antes do primeiro sync. Os values usam os mesmos toggles.

### 3.4 Futuro — External Secrets Operator [#34-futuro--external-secrets-operator]

O contrato "Secrets pré-existentes com estes nomes e chaves" é exactamente o que o ESO cumpre. Quando se adoptar Azure Key Vault, o bootstrap ou os SealedSecrets são substituídos por recursos `ExternalSecret` que criam os mesmos Secrets — **sem alterações ao chart nem aos values**.

---

## 4. Checklist de primeiro deploy [#4-checklist-de-primeiro-deploy]

1. `publish-all` corrido no CI (registry populado);
2. registry OCI registado no ArgoCD;
3. Secrets do ambiente criados + chaves RSA (`generate-oauth-keys.sh`);
4. `Chart.yaml` + `values.yaml` no repo de deployment;
5. (CNPG &lt; 1.25) `igrp-pg-cluster.cluster.databases: []` nos values, e criar `access-manager-db` / `keycloak` à mão depois do PostgreSQL subir — as BDs dos `process-*` são criadas pelo Job `db-init` em qualquer versão do CNPG;
6. criar o Application e sincronizar, acompanhando a ordem: pg-cluster → redis/minio/keycloak → access-management → app-center/gateway → process-*;
7. registar os clientes OAuth dos `process-*` (Job `m2m.autoRegister` ou [manualmente](./clientes-oauth.mdx));
8. `rollout restart` do gateway se serviços novos não aparecerem nas rotas.

---

## 5. Armadilhas de uma instalação de raiz [#5-armadilhas-de-uma-instalação-de-raiz]

Validado num cluster novo, apagando tudo e sincronizando só pelo ArgoCD. Correu — mas só depois destas quatro coisas.

### 5.1 O sync tem de ser disparado pelo caminho oficial [#51-o-sync-tem-de-ser-disparado-pelo-caminho-oficial]

<Callout type="danger" title="kubectl patch do .operation corre com skipHooks=true">
Nenhum hook do chart é executado — nem `db-init`, nem `oauth-register`, nem `catalog-seed`, nem `oauth-keys-init`.
</Callout>

O sintoma não parece um problema de sync, parece um chart partido: o Temporal em CrashLoop porque as suas BDs não existem, e cinco apps em `CreateContainerConfigError` à espera dos Secrets `*-oauth-client`. E a instalação fica em ciclo — a operação espera que a wave 3 fique Healthy, e a wave 3 depende de hooks que não correram.

Dois agravantes:

- Se já houver uma operação em `Running`, o controller **retoma-a** (`Resuming in-progress operation`) e ignora a nova. Para a substituir é preciso terminá-la primeiro e esperar que saia de `Running`/`Terminating`.
- `syncStrategy: {apply:{}}` é apply-only e salta hooks **por definição**. Um merge-patch não remove uma estratégia já lá posta — fica `{apply:{},hook:{}}`, e o ArgoCD escolhe `apply`.

Usa o botão **SYNC** da UI, ou `argocd app sync`. Diagnóstico rápido:

```bash
kubectl logs -n argocd sts/argocd-application-controller --since=5m \
  | grep "application=<app>" | grep -o 'skipHooks=[a-z]*' | sort | uniq -c
```

### 5.2 Um repo que redeclara o umbrella não herda o `values.yaml` dele [#52-um-repo-que-redeclara-o-umbrella-não-herda-o-valuesyaml-dele]

Se o repo de deployment tem um `Chart.yaml` próprio com os subcharts como dependências, em vez de depender do umbrella publicado, **não há camada base de values** — todo o wiring tem de ser reproduzido.

O que mais escapa: `igrp-pg-cluster.cluster.cluster.roles` e `cluster.databases`. O access-management e o Keycloak **não** têm Job `db-init` — as suas roles e BDs são declarativas (CNPG ≥ 1.25). Sem esse bloco o Keycloak entra em CrashLoop com `password authentication failed for user "keycloak_user"` e o Access Management fica preso no initContainer `sas-schema-init`.

Verificação: renderizar o chart do repo de deployment e comparar o conjunto de recursos com um render do umbrella publicado.

### 5.3 Renderizar sem `--namespace` envenena o RBAC [#53-renderizar-sem---namespace-envenena-o-rbac]

`helm template <release> .` deixa `.Release.Namespace` em `default`. Os `RoleBinding` dos hooks passam a apontar a ServiceAccounts em `default/…` e os recursos namespaced saem com `metadata.namespace: default`. Os manifestos parecem bem; os Jobs falham em runtime com `Forbidden … cannot get resource`. Ao comparar com o cluster, renderiza **sempre** com `--namespace <ns>`.

### 5.4 Chaves RSA — manual ou automático, mas conscientemente [#54-chaves-rsa--manual-ou-automático-mas-conscientemente]

No subchart e no `values.prod.yaml`, o Secret `igrp-access-management-oauth-keys` é criado por ops. Enquanto não existir, o pod do Access Management fica em `FailedMount` indefinidamente, porque o Deployment monta-o como volume.

Para GitOps de raiz sem passos manuais: `oauth.keys.generate.enabled: true` (já ligado no values de teste), que corre o Job `oauth-keys-init` na sync-wave `-5`. Gera **só se o Secret não existir**, por isso não roda chaves em upgrades. Em produção continua com o Secret de ops — a chave privada fica gerível fora do cluster e sobrevive a um `helm uninstall`.

### 5.5 Uma sync não basta para convergir [#55-uma-sync-não-basta-para-convergir]

A instalação de raiz convergiu ao fim de **três** `argocd app sync` seguidos. Cada um avança as waves, mas termina em `Failed` porque Deployments de waves posteriores ainda não têm as dependências e batem no progress deadline. Com `syncPolicy.automated` + `retry` na Application, o ArgoCD faz esses ciclos sozinho e uma única acção chega.

---

## 6. Armadilhas de um upgrade [#6-armadilhas-de-um-upgrade]

### 6.1 Republicar a mesma versão: são DUAS caches, não uma [#61-republicar-a-mesma-versão-são-duas-caches-não-uma]

A política do chart permite corrigir **in place**, sem bump de versão. Mas o `rollout restart` do repo-server — que era a receita documentada — **não chega**:

| Cache | O que guarda | Como se limpa |
| --- | --- | --- |
| repo-server (disco do pod) | os `.tgz` dos subcharts OCI, por **versão** | `kubectl rollout restart deploy/<release>-argocd-repo-server -n argocd` |
| manifestos gerados (Redis) | o YAML renderizado, indexado pela **revisão git** do repo de deployment | **hard refresh** — a revisão não mudou, por isso a cache continua válida |

```bash
kubectl rollout restart deploy/<release>-argocd-repo-server -n argocd
kubectl rollout status  deploy/<release>-argocd-repo-server -n argocd
kubectl exec -n argocd sts/<release>-argocd-application-controller -- \
  sh -c 'argocd --core app get <app> --hard-refresh'
argocd app sync <app>
```

<Callout type="danger" title="Synced + Healthy pode estar a mentir">
Com só o restart, o sync reportou `Synced` e `Healthy` **enquanto aplicava o render antigo**. E parecia meio aplicado, porque annotations postas à mão sobrevivem ao merge patch. Confirma sempre num campo que o chart novo mudou — nunca no estado `Synced`.
</Callout>

### 6.2 Correr o CLI `argocd` quando não está instalado no nó [#62-correr-o-cli-argocd-quando-não-está-instalado-no-nó]

Usa o binário que já vive no pod do **application-controller**. O do `argocd-server` não tem RBAC para o modo `--core`:

```bash
kubectl exec -n argocd sts/<release>-argocd-application-controller -- \
  sh -c 'argocd --core app sync <app> --timeout 600'
```

### 6.3 O ArgoCD quer podar PVCs que ficaram como backup [#63-o-argocd-quer-podar-pvcs-que-ficaram-como-backup]

Um PVC criado por uma versão anterior do chart mantém o `argocd.argoproj.io/tracking-id` e aparece em `requiresPruning: true` — **um sync com prune apaga os dados**. Para o preservar, tira-o do tracking (não apaga nada):

```bash
kubectl label    pvc <pvc> -n <ns> app.kubernetes.io/instance-
kubectl annotate pvc <pvc> -n <ns> argocd.argoproj.io/tracking-id-
```

### 6.4 Recursos com campos imutáveis [#64-recursos-com-campos-imutáveis]

O `spec.selector` de Deployments e StatefulSets não se altera. Quando um subchart muda de esqueleto, o sync falha com `field is immutable` e o recurso tem de ser apagado à mão antes. Despiste genérico, a correr **antes** de um upgrade que troque a origem de um subchart:

```bash
# comparar os selectors renderizados com os que estão vivos
helm template <rel> . -n <ns> | grep -A4 'matchLabels'
kubectl get deploy,sts -n <ns> -o custom-columns='NAME:.metadata.name,SEL:.spec.selector.matchLabels'
```


# Registar clientes OAuth

URL: /pt/docs/developer/cloud/clientes-oauth

Registar clientes OAuth e service accounts no Access Management por curl — backends, frontends, actualização de redirects e smoke tests.

# Registar clientes OAuth por `curl` [#registar-clientes-oauth-por-curl]

Cada serviço que fala com o Access Management precisa de um **cliente OAuth** e de um **service account** ligado a esse cliente. O chart regista-os automaticamente quando `m2m.autoRegister.enabled: true` — esta página é para o registo manual: apps fora do stack, depuração, ou quando o Job falha.

<Callout type="note" title="Convenções desta página">
Substitui `<domainapi>` pelo teu domínio de APIs e `<domain>` pelo das UIs. Os UUIDs mostrados são exemplos — o teu ambiente devolve outros.
</Callout>

---

## 0. Obter o token M2M — pré-requisito de tudo [#0-obter-o-token-m2m--pré-requisito-de-tudo]

Usa-se o **cliente seeded**, que tem as permissões `igrp.client.*` e `igrp.service_account.*` através do service account seeded. O `client_id` e o `client_secret` são os do Secret `igrp-access-management-oauth-client` (ou o valor de `oauth.oauthClient.clientSecret` nos values).

```bash
TOKEN=$(curl -sk -X POST https://<domainapi>/igrp-access-management/oauth2/token \
  -d "grant_type=client_credentials&client_id=<oauthClient.clientId>&client_secret=<oauthClient.clientSecret>" \
  | sed 's/.*"access_token":"\([^"]*\)".*/\1/')
echo "${TOKEN:0:20}..."
```

<Callout type="warning" title="Se o token vier vazio">
O fluxo OAuth2 do Spring Authorization Server exige o **gateway HTTPS**, com o issuer correcto. Autenticar pelo URL de Service interno (`http://igrp-access-management:8080`) não emite token.
</Callout>

---

## 1. Registar um backend — sem redirects [#1-registar-um-backend--sem-redirects]

Um backend só precisa de `client_credentials`: não há browser, não há callback.

```bash
curl -sk -X POST https://<domainapi>/igrp-access-management/api/clients \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "clientId": "igrp-process-studio-api",
  "clientName": "iGRP Process Studio API",
  "active": true,
  "requirePkce": false,
  "accessTokenTtl": 3600,
  "refreshTokenTtl": 86400,
  "authorizationCodeTtl": 300,
  "scopes": ["openid"],
  "grantTypes": ["client_credentials"]
}'
```

<Callout type="danger" title="A resposta traz o clientSecret UMA única vez">
Devolve `id` (o UUID interno) e `clientSecret`. O secret **só é devolvido na criação** — guarda-o imediatamente. Se o perderes, tens de apagar e recriar o cliente.
</Callout>

---

## 2. Registar um frontend — com redirects [#2-registar-um-frontend--com-redirects]

Um frontend acrescenta os callbacks, o `authorization_code` e o scope `profile`, que traz o nome e os atributos do utilizador no `id_token`.

```bash
curl -sk -X POST https://<domainapi>/igrp-access-management/api/clients \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "clientId": "igrp-process-management-ui",
  "clientName": "iGRP Process Management UI",
  "active": true,
  "requirePkce": false,
  "accessTokenTtl": 3600,
  "refreshTokenTtl": 86400,
  "authorizationCodeTtl": 300,
  "scopes": ["openid", "email", "profile"],
  "grantTypes": ["authorization_code", "refresh_token", "client_credentials"],
  "redirectUris": [
    "https://<domain>/apps/<app>/api/auth/callback/igrp-auth",
    "http://localhost:3000/api/auth/callback/igrp-auth"
  ],
  "postLogoutRedirectUris": [
    "https://<domain>/apps/<app>/login",
    "http://localhost:3000/login"
  ]
}'
```

<Callout type="warning" title="O redirectUri tem de bater exactamente com o NEXTAUTH_URL">
O callback é sempre `<NEXTAUTH_URL>/callback/igrp-auth`. Se o `NEXTAUTH_URL` da app não terminar em `/api/auth`, ou não incluir o `basePath`, o login falha com `redirect_uri` inválido.
</Callout>

O `localhost:3000` serve o desenvolvimento local. Deixa-o fora dos clientes de produção.

---

## 3. Criar o service account [#3-criar-o-service-account]

Sem service account, o token M2M do cliente não consegue usar os endpoints do Access Management.

```bash
curl -sk -X POST https://<domainapi>/igrp-access-management/api/service-accounts \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "name": "igrp-process-studio-api",
  "description": "iGRP Process Studio API Service Account",
  "active": true,
  "oauthClientId": "<UUID-devolvido-no-POST-do-cliente>",
  "roleIds": [],
  "permissionIds": []
}'
```

<Callout type="danger" title="oauthClientId é o UUID, não a string do clientId">
É o campo `id` devolvido no `POST /api/clients` — um UUID na forma `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, **não** a string `igrp-process-studio-api`. É o erro mais comum deste procedimento.
</Callout>

As listas `roleIds` e `permissionIds` ficam vazias: as permissões são atribuídas depois, se o sync delas precisar.

---

## 4. Guardar o secret no namespace [#4-guardar-o-secret-no-namespace]

```bash
kubectl -n <ns> create secret generic igrp-<serviço>-oauth-client \
  --from-literal=client-id='igrp-<serviço>' \
  --from-literal=client-secret='<devolvido no registo>'
```

<Callout type="note" title="Aspas simples não são opcionais">
O `clientSecret` pode começar por `-`, e aí o shell interpreta-o como uma flag.
</Callout>

---

## 5. Actualizar os redirects de um cliente existente [#5-actualizar-os-redirects-de-um-cliente-existente]

```bash
curl -sk -X PUT https://<domainapi>/igrp-access-management/api/clients/<UUID> \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{
  "clientId": "igrp-process-management-ui",
  "clientName": "iGRP Process Management UI",
  "active": true,
  "scopes": ["openid", "email", "profile"],
  "grantTypes": ["authorization_code", "refresh_token", "client_credentials"],
  "redirectUris": ["... a lista COMPLETA ..."],
  "postLogoutRedirectUris": ["... a lista COMPLETA ..."]
}'
```

<Callout type="danger" title="O PUT substitui o objecto inteiro">
Envia sempre a lista **completa** de `redirectUris` e `postLogoutRedirectUris`. O que não for enviado é removido.
</Callout>

Para o cliente OIDC partilhado, o chart tem o Job `default-client-uri-sync` (`oauth.defaultClient.redirectUriSync.enabled`), que **funde** os URIs em vez de os substituir — é o caminho seguro em GitOps.

---

## 6. Referência — listar, ver, apagar [#6-referência--listar-ver-apagar]

```bash
# listar todos os clientes
curl -sk https://<domainapi>/igrp-access-management/api/clients \
  -H "Authorization: Bearer $TOKEN"

# ver um cliente
curl -sk https://<domainapi>/igrp-access-management/api/clients/<UUID> \
  -H "Authorization: Bearer $TOKEN"

# apagar (ex.: para re-registar e obter um clientSecret novo)
curl -sk -X DELETE https://<domainapi>/igrp-access-management/api/clients/<UUID> \
  -H "Authorization: Bearer $TOKEN"
```

### Smoke test [#smoke-test]

O novo cliente tem de conseguir obter token, e o JWT tem de trazer o service account ligado:

```bash
curl -sk -X POST https://<domainapi>/igrp-access-management/oauth2/token \
  -d "grant_type=client_credentials&client_id=<novo-client>&client_secret=<novo-secret>"
```

---

## 7. Armadilhas [#7-armadilhas]

| Sintoma | Causa |
| --- | --- |
| `403` em `GET /api/clients` com um token que parece válido | O catálogo de permissões não foi semeado — o service account seeded tem 0 permissões. O Job `catalog-seed` (`catalogSeed.enabled`, default `true`) deteta o 403 e reinicia o Access Management uma vez. |
| Token M2M vem vazio | Autenticaste pelo Service interno em HTTP. Usa o gateway HTTPS. |
| `redirect_uri` inválido no login | O `redirectUri` registado não bate exactamente com `<NEXTAUTH_URL>/callback/igrp-auth`. |
| O service account não dá acesso a nada | Puseste o `clientId` em string no `oauthClientId` em vez do UUID. |
| Perdeste o `clientSecret` | Só é devolvido na criação. Apaga o cliente e regista de novo, ou usa `autoRegister.forceRecreate: true`. |


# Configuração do chart

URL: /pt/docs/developer/cloud/configuracao

Os campos de values do umbrella igrp-stack que se configuram na prática — global, base de dados, storage, identidade, gateway e frontends.

# Configuração do chart [#configuração-do-chart]

Campos do `values.yaml` do umbrella. Cada subchart tem muitas mais opções — estas são as que se configuram na prática.

<Callout type="warning" title="Uma chave no nível errado é ignorada em silêncio">
O Helm não valida chaves desconhecidas, e este chart não tem `values.schema.json`. Uma chave indentada no sítio errado não dá erro, não dá aviso, e o `helm template` passa — simplesmente não faz nada. Para despistar: `helm template ... | grep -c <nome-do-recurso>` e confirma que o número de recursos muda quando mexes na chave.
</Callout>

---

## `global` [#global]

| Campo | Default | Descrição |
| --- | --- | --- |
| `domain` / `domainapi` / `domains3api` | — | Os três DNS |
| `corsExtraOrigins` | `""` | Origens CORS extra em **todos** os ingress |
| `smtp.{host,port,username,sender_id}` | Office365 | Relay de e-mail (a password é Secret) |
| `igrp.notifications_enabled` | `none` | `mail` activa o envio; exige a `smtp-password` real |
| `igrp.storage_provider` | `s3` | |

---

## `igrp-pg-cluster` — PostgreSQL [#igrp-pg-cluster--postgresql]

| Campo | Default | Descrição |
| --- | --- | --- |
| `dbUsers.users.<svc>.password` | teste / `""` | **Obrigatória em todas as entradas** — é o que força o `--set` em produção |
| `dbUsers.createSecrets` | `true` | `false` em GitOps (ops pré-cria os Secrets) |
| `cluster.cluster.instances` | `1` (prod `3`) | Total de pods = 1 primário + N−1 réplicas |
| `cluster.cluster.enablePDB` | `false` (prod `true`) | Com 1 instância o PDB bloqueia o drain do nó |
| `cluster.cluster.storage.{storageClass,size}` | `longhorn` / `8Gi` | O WAL partilha este volume |
| `…postgresql.parameters.max_slot_wal_keep_size` | `2GB` | **Travão do WAL** |
| `…postgresql.parameters.max_connections` | `250` | |
| `…initdb.{localeCollate,localeCType}` | `"C"` | ⚠️ A `""` cria `OutOfSync` permanente no ArgoCD |

<Callout type="danger" title="max_slot_wal_keep_size não é opcional">
Sem este travão, uma réplica em baixo enche o volume de WAL e o primário **pára**. Dimensiona-o ao volume: 2Gi → `512MB`, 10Gi → `2GB`, 50Gi → `8GB`.
</Callout>

---

## `igrp-redis` [#igrp-redis]

| Campo | Default | Descrição |
| --- | --- | --- |
| `authSecret.{create,password}` | `true` / teste | ⚠️ Password vazia é gerada e **roda a cada sync** em GitOps |
| `redis.global.persistence.{size,storageClass}` | `8Gi` / `""` | |

O Redis é **obrigatório** — guarda a sessão do access-management.

---

## `igrp-minio` — Object storage [#igrp-minio--object-storage]

| Campo | Default | Descrição |
| --- | --- | --- |
| `minio.auth.{rootUser,rootPassword}` | `admin` / teste | Popula o Secret `igrp-minio-creds`, lido pelo Access Management e pelas notificações |
| `minio.persistence.{size,storageClass}` | `10Gi` / `""` | ⚠️ `storageClass` é imutável num PVC já criado |
| `minio.persistence.existingClaim` | `""` | Reutilizar volume **só** se o MinIO anterior for igual ou mais antigo |
| `credsSecret.create` | `true` | `false` em GitOps |
| `bucketInit.{enabled,name,makePublic}` | `true` / `igrp` / `true` | Bucket inicial |
| `bucketInit.anonymousPolicy` | `download` | Leitura anónima; a escrita exige sempre credenciais |
| `ingress.enabled` / `apiIngress.enabled` | `true` | Consola · API S3 |

<Callout type="warning" title="A consola de administração do MinIO já não existe na edição community">
A MinIO removeu Identity/Users, Groups e Access Policies da consola entre Abril e Julho de 2025. A partir de `minio 2025.7.x` fica só o <em>Object Browser</em>. O chart fica de propósito no **Bitnami 16.0.10 / MinIO 2025.4.22**, a última linha com a consola completa. Quem subir estas tags perde a gestão de acessos pela UI — passa a ser só por `mc admin user/policy`.
</Callout>

---

## `igrp-keycloak` — IdP opcional [#igrp-keycloak--idp-opcional]

| Campo | Default | Descrição |
| --- | --- | --- |
| `enabled` | `true` | **`false`** em modo Autentika ou utilizadores locais |
| `keycloak.auth.adminPassword` | teste | Consola do Keycloak |
| `igrp.clientSecrets.accessManagement` | teste | Tem de coincidir com o cliente do realm |

---

## `igrp-access-management` — Authorization Server [#igrp-access-management--authorization-server]

| Campo | Default | Descrição |
| --- | --- | --- |
| `database.schemaInit.enabled` | `true` | Cria as tabelas do Authorization Server |
| `oauth.keys.generate.enabled` | `true` (prod `false`) | Gera as chaves RSA se o Secret não existir. **Nunca as roda** |
| `oauth.keys.existingSecret` | `igrp-access-management-oauth-keys` | Em produção: Secret criado por ops |
| `oauth.externalIdp.enabled` | `true` | `false` = utilizadores locais |
| `oauth.externalIdp.{issuerUri,clientId,clientSecret}` | Keycloak | Preencher em modo Autentika |
| `oauth.externalIdp.requireEmailVerified` | `true` | `"false"` se o IdP não emitir `email_verified` |
| `oauth.defaultClient.{redirectUris,postLogoutRedirectUris}` | listas | Callbacks de **todos** os frontends |
| `oauth.defaultClient.redirectUriSync.enabled` | `true` | Funde (nunca remove) os URIs no cliente |
| `oauthClient.{clientId,clientSecret}` | — | Cliente OIDC partilhado + M2M do Access Management |
| `catalogSeed.enabled` | `true` | Semeia o catálogo de permissões numa base de dados nova |
| `app.superadminExternalId` | — | O `sub` do IdP externo, não o email |
| `secrets.smtp-password` | `CHANGE_ME` | |

<Callout type="note" title="O superadmin identifica-se pelo external-id, não pelo email">
O superadmin é identificado pelo **external-id** — gravado no `username` de `t_user`, igual ao `sub` do IdP. O `superadminEmail` é só um atributo. Se o superadmin não coincidir com o utilizador do IdP externo, põe o `sub` real em `app.superadminExternalId`.
</Callout>

---

## `igrp-gateway-api` [#igrp-gateway-api]

| Campo | Default | Descrição |
| --- | --- | --- |
| `discovery.allNamespaces` | `true` | Isolado por `gateway.name` |
| `routeRefresh.periodic.enabled` | `true` | Sidecar que faz `POST /actuator/gateway/refresh` a cada 30s — Services novos ganham rota sozinhos |
| `routeRefresh.bootstrapUrls` | lista | URLs acordadas após o sync, para os frontends se registarem |

---

## Frontends — `igrp-app-center` e `*-ui` [#frontends--igrp-app-center-e--ui]

| Campo | Default | Descrição |
| --- | --- | --- |
| `auth.provider` / `auth.clientId` | `igrp-auth` / `igrp-access-management` | **Sempre** — autenticam no Access Management |
| `nextauth.secret` *(app-center)* | teste | Dono do Secret `igrp-app-center-nextauth` |
| `nextauth.existingSecret` *(UIs)* | `igrp-app-center-nextauth` | Ver aviso abaixo |
| `m2m.autoRegister.enabled` | `true` | Regista o cliente OAuth próprio da UI |
| `app.{syncAccess,syncOnCodeMenus,previewMode}` | `true`/`true`/`false` | Sincronização de menus e permissões |
| `app.settingsUrl` | `https://<global.domain>/settings` | Página de definições, sempre no domínio do app-center |

<Callout type="danger" title="Todas as UIs têm de ler o MESMO Secret de NextAuth">
Se cada UI tiver o seu `nextauth-secret`, a sessão perde-se ao trocar de app no mesmo domínio. O default (`existingSecret: igrp-app-center-nextauth`) está correcto — só o mudes se souberes porquê.
</Callout>

<Callout type="warning" title="NEXT_PUBLIC_* é baked no next build">
O chart corrige a env de runtime, mas o bundle client-side só leva o valor novo quando a imagem for reconstruída com ele como `--build-arg`. Mudar o values não chega para variáveis `NEXT_PUBLIC_*`.
</Callout>

---

## APIs dos módulos — `*-api` [#apis-dos-módulos---api]

| Campo | Default | Descrição |
| --- | --- | --- |
| `database.envFromSecret.name` | `igrp-<svc>-db-user` | Credenciais da base de dados |
| `database.autoCreate.enabled` | `true` | Job que cria role + BD por SQL directo |
| `gateway.expose` | `true` | Publica o serviço no gateway |
| `m2m.autoRegister.enabled` | `true` | Regista o cliente M2M no Access Management |

<Callout type="warning" title="spring.application.name tem de ser igual ao client-id">
O `AuthorizationSyncRunner` procura o *resource* por esse nome. Se divergirem, o arranque falha com `404 IGRP_AUTH_RESOURCE_NOT_FOUND_BY_NAME`. O chart lê o `SPRING_APPLICATION_NAME` do Secret M2M, por isso isto só acontece em configurações manuais.
</Callout>

---

## Notificações [#notificações]

| Campo | Default | Descrição |
| --- | --- | --- |
| `igrp-notification-{temporal,api,ui}.enabled` | `true` (prod `false`) | **Activar os três ou nenhum** |
| `igrp-notification-temporal.webIngress.enabled` | `false` | ⚠️ A Web UI do Temporal **não tem autenticação** |


# Visão geral

URL: /pt/docs/developer/cloud

Helm Chart de instalação do stack do IGRP num cluster Kubernetes — componentes, pré-requisitos, instalação e verificação.

# IGRP Stack em Kubernetes [#igrp-stack-em-kubernetes]

O **igrp-stack** é o Helm Chart que instala a plataforma IGRP num cluster Kubernetes: base de dados, gestão de acessos, gateway de APIs, object storage, processos e notificações. Enquanto o <a href="/docs/developer/workspace/criar-workspace">Workspace</a> te dá a plataforma no teu portátil, o igrp-stack dá-te a mesma plataforma num cluster — para ambientes de teste, homologação e produção.

<Callout type="warning" title="Versão exacta, nunca ranges">
A versão actual é `0.2.0-beta`. Em SemVer, `0.2.0-beta` é **inferior** a `2.1.0` — um range como `^2.0.0` ou `>=2.0.0` nunca a apanha, e um range aberto puxa a versão errada. Usa sempre a versão exacta.
</Callout>

| | |
| --- | --- |
| Charts | `oci://registry.nosi.cv/igrp-charts` |
| Imagens IGRP | `registry.nosi.cv/igrp/*` (tag `0.2.0-beta`) |
| Imagens de terceiros | `registry.nosi.cv/igrp-mirror/*` — **nada vem do Docker Hub** |

---

## Componentes [#componentes]

<StackArchitecture variant="cluster" />

O diagrama mostra como os serviços se ligam; a tabela abaixo é a **visão de instalação**: os 14 subcharts, todos com `<nome>.enabled`. A mesma plataforma em Docker, no teu computador, está em [Workspace › Visão geral](/docs/developer/workspace/plataforma).

| Subchart | O que é | Onde responde |
| --- | --- | --- |
| `igrp-pg-cluster` | PostgreSQL (operador CloudNative-PG) | `igrp-pg-cluster-rw:5432` |
| `igrp-redis` | Redis — **obrigatório** (sessão do access-management) | `igrp-redis-master:6379` |
| `igrp-minio` | Object storage S3 + consola de administração | `domains3api` · `domain/minio/` |
| `igrp-keycloak` | IdP upstream **opcional** | `domain/auth` |
| `igrp-access-management` | **Authorization Server OAuth2** — emite JWT, gere utilizadores e permissões | via gateway |
| `igrp-gateway-api` | Gateway de APIs com discovery automático | `domainapi` |
| `igrp-app-center` | Frontend principal | `domain/` |
| `igrp-process-studio-api` / `-ui` | Desenho de processos | `domain/apps/igrp-process-studio` |
| `igrp-process-management-api` / `-ui` | Execução de processos | `domain/apps/igrp-process-management` |
| `igrp-notification-temporal` | Temporal — motor de workflows | interno |
| `igrp-notification-api` / `-ui` | Notificações: e-mail, in-app, webhooks | `domain/apps/notifications` |

Três domínios, três entradas: **`domain`** serve as UIs, a consola do MinIO e o Keycloak; **`domainapi`** serve todas as APIs através do gateway; **`domains3api`** é o endpoint S3.

Todos os frontends autenticam no access-management, partilhando um cliente OIDC. O IdP a montante escolhe-se lá: Keycloak, Autentika/WSO2 ou utilizadores locais.

---

## Pré-requisitos [#pré-requisitos]

| Requisito | Verificar com |
| --- | --- |
| Operador CloudNative-PG (≥ 1.25) | `kubectl get pods -n cnpg-system` |
| StorageClass (ex.: `longhorn`) | `kubectl get storageclass` |
| nginx IngressController | `kubectl get ingressclass` |
| DNS dos 3 domínios | `getent hosts <domain>` |
| `helm` ≥ 3.8 e `kubectl` | `helm version` |

---

## Instalação [#instalação]

### 1. Perfil de values [#1-perfil-de-values]

Empilham-se com `-f`, sempre por cima do `values.yaml`:

| IdP | Teste | Produção |
| --- | --- | --- |
| **Keycloak** | `values.yaml` | `+ values.prod.yaml` |
| **Autentika** | `+ values.autentika.yaml` | `+ values.prod.yaml + values.autentika.prod.yaml` |

<Callout type="danger" title="O values.yaml traz passwords de teste públicas">
O `values.yaml` instala sem passos manuais, mas as credenciais que traz são públicas — nunca num ambiente exposto. O `values.prod.yaml` esvazia-as todas e sobe o PostgreSQL para 3 instâncias.
</Callout>

### 2. Domínios [#2-domínios]

```yaml
global:
  domain:      apps.exemplo.cv       # UIs, consola MinIO, Keycloak
  domainapi:   api.exemplo.cv        # APIs, via gateway
  domains3api: s3api.exemplo.cv      # endpoint S3
```

### 3. Instalar [#3-instalar]

**Teste** — um comando:

```bash
helm upgrade --install igrp-stack oci://registry.nosi.cv/igrp-charts/igrp-stack \
  --version 0.2.0-beta -n igrp-stack --create-namespace -f values.yaml
```

**Produção** — os segredos num ficheiro à parte, fora do git:

```yaml
# secrets.yaml
igrp-pg-cluster:
  dbUsers:
    users:
      accessManagement:     {password: "..."}
      keycloak:             {password: "..."}
      processStudio:        {password: "..."}
      processManagement:    {password: "..."}
      notification:         {password: "..."}
      notificationTemporal: {password: "..."}
igrp-access-management:
  oauthClient: {clientSecret: "..."}
  secrets:     {smtp-password: "..."}
igrp-keycloak:
  igrp:      {clientSecrets: {accessManagement: "..."}}
  keycloak:  {auth: {adminPassword: "..."}}
igrp-minio:
  minio: {auth: {rootPassword: "..."}}
igrp-app-center:
  nextauth: {secret: "..."}
```

```bash
helm upgrade --install igrp-stack oci://registry.nosi.cv/igrp-charts/igrp-stack \
  --version 0.2.0-beta -n igrp-stack --create-namespace \
  -f values.yaml -f values.prod.yaml -f secrets.yaml
```

<Callout type="warning" title="As 6 passwords de base de dados são obrigatórias">
Sem elas o render falha de propósito. Em produção, a alternativa recomendada é não ter segredos no git de todo — ver <a href="./producao">Instalação de produção</a>.
</Callout>

### 4. Por ArgoCD [#4-por-argocd]

O repo de deployment tem um `Chart.yaml` que redeclara o umbrella mais o `values.yaml` do ambiente. Duas regras que poupam horas:

1. Um repo que redeclara o umbrella **não herda** o `values.yaml` dele — todo o wiring tem de estar no values do ambiente.
2. Sincronizar pelo botão **SYNC** ou `argocd app sync`. Um `kubectl patch` do `.operation` corre com `skipHooks=true` e **nenhum Job de inicialização corre**.

Ver [Instalação via ArgoCD](./argocd.mdx).

### Não há passos manuais [#não-há-passos-manuais]

15 Jobs criam as bases de dados, o schema do Authorization Server, as chaves RSA, os clientes OAuth, o bucket do MinIO e o namespace do Temporal — por *sync-waves*, de `-5` (chaves) a `5` (API de notificações).

---

## Verificação [#verificação]

```bash
kubectl get pods -n igrp-stack          # nada parado
kubectl get jobs -n igrp-stack          # todos Complete

# nenhuma imagem de fora do nosso registry (só deve sair alpine:3.17, hook de teste)
kubectl get pods -n igrp-stack --field-selector=status.phase=Running \
  -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' \
  | sort -u | grep -v '^registry.nosi.cv/'

curl -sk -o /dev/null -w '%{http_code}\n' https://<domain>/
curl -sk -o /dev/null -w '%{http_code}\n' https://<domainapi>/igrp-access-management/.well-known/openid-configuration
curl -sk -o /dev/null -w '%{http_code}\n' https://<domains3api>/minio/health/live
```

<Callout type="warning" title="A consola do MinIO não se valida por curl">
Devolve `200` em qualquer caminho. Testa o redirect e abre no browser:<br />
`curl -sk -o /dev/null -w '%&#123;http_code&#125; %&#123;redirect_url&#125;\n' https://<domain>/minio` deve dar **301** para `/minio/`.
</Callout>

---

## Por onde seguir [#por-onde-seguir]

| Página | Para quê |
| --- | --- |
| [Configuração](./configuracao.mdx) | Todos os campos de values que se configuram na prática |
| [Instalação de produção](./producao.mdx) | Sem segredos no git — Sealed Secrets ou Secrets directos |
| [ArgoCD](./argocd.mdx) | GitOps, segredos, armadilhas do sync |
| [Clientes OAuth](./clientes-oauth.mdx) | Registar clientes e service accounts por `curl` |


# Instalação de produção

URL: /pt/docs/developer/cloud/producao

Instalar o igrp-stack sem segredos no git — por Sealed Secrets (recomendado) ou por Secrets criados directamente no cluster.

# Instalação de produção [#instalação-de-produção]

O `values.yaml` do chart é o perfil de **teste**: instala sem passos manuais, mas traz passwords públicas. Em produção nenhum valor sensível pode ficar em ficheiros versionados. Há dois caminhos, e ambos partem do mesmo princípio — aplicar sempre `values.prod.yaml` por cima do `values.yaml`, e o ficheiro local de segredos por último.

| Caminho | Quando usar |
| --- | --- |
| [**Sealed Secrets**](#sealed-secrets) | Recomendado. Os segredos ficam cifrados em manifests versionáveis; só o controller do cluster os decifra. |
| [**Secrets directos**](#secrets-directos) | Quando não há controller de Sealed Secrets. Os Secrets criam-se à mão no cluster e nunca passam pelo git. |

<Callout type="warning" title="Ordem dos ficheiros de values">
`--values values.yaml --values values.prod.yaml --values <ficheiro-local>`, sempre por esta ordem. O `values.prod.yaml` esvazia as passwords de teste e sobe o PostgreSQL para 3 instâncias; o ficheiro local sobrepõe-se a tudo.
</Callout>

---

## Sealed Secrets [#sealed-secrets]

Mantém as credenciais cifradas em manifests `SealedSecret`. Só o controller instalado no cluster consegue criar os Kubernetes Secrets reais.

### 1. Controller e namespace [#1-controller-e-namespace]

```bash
export NS=igrp-prod
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm upgrade --install sealed-secrets sealed-secrets/sealed-secrets \
  -n kube-system --set fullnameOverride=sealed-secrets
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml \
  > sealed-secrets-key.backup.yaml
```

<Callout type="danger" title="Guarda o backup da chave fora do Git">
Perder a chave do controller impede a recuperação de todos os dados cifrados existentes. Não há segunda via.
</Callout>

### 2. Gerar e cifrar os Secrets [#2-gerar-e-cifrar-os-secrets]

```bash
export AM_DB_PASSWORD="$(openssl rand -base64 32)"
export KC_DB_PASSWORD="$(openssl rand -base64 32)"
export PM_DB_PASSWORD="$(openssl rand -base64 32)"
export PS_DB_PASSWORD="$(openssl rand -base64 32)"
# --- só com o módulo de notificações activo ---
export NOTIF_DB_PASSWORD="$(openssl rand -base64 32)"
export TEMPORAL_DB_PASSWORD="$(openssl rand -base64 32)"
export OAUTH_CLIENT_SECRET="$(openssl rand -hex 32)"
export MINIO_ROOT_PASSWORD="$(openssl rand -base64 32)"
export REDIS_PASSWORD="$(openssl rand -base64 32)"
export KEYCLOAK_ADMIN_PASSWORD="$(openssl rand -base64 32)"
export NEXTAUTH_SECRET="$(openssl rand -base64 32)"
export SMTP_PASSWORD='<password-ou-token-fornecido-pelo-SMTP>'

./scripts/bootstrap-secrets.sh -n "$NS" --seal --out sealed-secrets/
kubectl apply -f sealed-secrets/
kubectl get sealedsecrets,secrets -n "$NS"
```

Versiona **apenas** os ficheiros cifrados de `sealed-secrets/`. Os valores originais vão para o gestor de passwords.

### 3. SealedSecret das chaves RSA [#3-sealedsecret-das-chaves-rsa]

```bash
workdir=$(mktemp -d)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$workdir/private.pem"
openssl rsa -pubout -in "$workdir/private.pem" -out "$workdir/public.pem"
kubectl create secret generic igrp-access-management-oauth-keys -n "$NS" \
  --from-file=private.pem="$workdir/private.pem" --from-file=public.pem="$workdir/public.pem" \
  --dry-run=client -o yaml |
  kubeseal --controller-name sealed-secrets --controller-namespace kube-system --format yaml \
  > sealed-secrets/igrp-access-management-oauth-keys.yaml
kubectl apply -f sealed-secrets/igrp-access-management-oauth-keys.yaml
rm -rf "$workdir"
```

### 4. Deploy [#4-deploy]

```bash
helm dependency build .
helm upgrade --install igrp-stack . -n "$NS" \
  --values values.yaml --values values.prod.yaml \
  --values values.prod-secrets.local.yaml \
  --wait --timeout 15m
kubectl get sealedsecrets,secrets,pods,jobs -n "$NS"
```

Em modo Autentika, acrescenta `--values values.autentika.prod.yaml` antes do ficheiro local, mais o `--set` dos dois campos da Autentika (ver o cabeçalho desse ficheiro).

---

## Secrets directos [#secrets-directos]

Cria-se tudo à mão no cluster. Os valores nunca chegam ao git.

### 1. Preparação [#1-preparação]

```bash
export NS=igrp-prod
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -

export AM_DB_PASSWORD="$(openssl rand -base64 32)"
export KC_DB_PASSWORD="$(openssl rand -base64 32)"
export PM_DB_PASSWORD="$(openssl rand -base64 32)"
export PS_DB_PASSWORD="$(openssl rand -base64 32)"
export OAUTH_CLIENT_SECRET="$(openssl rand -hex 32)"
export MINIO_ROOT_PASSWORD="$(openssl rand -base64 32)"
export REDIS_PASSWORD="$(openssl rand -base64 32)"
export KEYCLOAK_ADMIN_PASSWORD="$(openssl rand -base64 32)"
export NEXTAUTH_SECRET="$(openssl rand -base64 32)"
export SMTP_PASSWORD='<password-ou-token-fornecido-pelo-SMTP>'
export KEYCLOAK_AM_CLIENT_SECRET="$(openssl rand -hex 32)"
```

Guarda estes valores no gestor de passwords antes de continuar. O cluster deve ter *encryption at rest* para Secrets/etcd e RBAC restrito no namespace.

### 2. Secrets da plataforma [#2-secrets-da-plataforma]

```bash
db_secret() {
  kubectl create secret generic "$1" -n "$NS" --type=kubernetes.io/basic-auth \
    --from-literal=username="$2" --from-literal=password="$3" \
    --dry-run=client -o yaml |
    kubectl label --local -f - cnpg.io/reload=true --dry-run=client -o yaml |
    kubectl apply -f -
}
db_secret igrp-access-management-db-user access_management_user "$AM_DB_PASSWORD"
db_secret igrp-keycloak-db-user keycloak_user "$KC_DB_PASSWORD"
db_secret igrp-process-management-db-user process_management_user "$PM_DB_PASSWORD"
db_secret igrp-process-studio-db-user process_studio_user "$PS_DB_PASSWORD"
# --- só com o módulo de notificações activo ---
db_secret igrp-notification-db-user notification_user "$NOTIF_DB_PASSWORD"
db_secret igrp-notification-temporal-db-user temporaldb_user "$TEMPORAL_DB_PASSWORD"

kubectl create secret generic igrp-access-management-secrets -n "$NS" \
  --from-literal=smtp-password="$SMTP_PASSWORD" --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic igrp-access-management-oauth-client -n "$NS" \
  --from-literal=client-id=igrp-access-management --from-literal=client-secret="$OAUTH_CLIENT_SECRET" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic igrp-minio-creds -n "$NS" \
  --from-literal=MINIO_ROOT_USER=admin --from-literal=MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic igrp-app-center-nextauth -n "$NS" \
  --from-literal=nextauth-secret="$NEXTAUTH_SECRET" --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic igrp-redis-auth -n "$NS" \
  --from-literal=redis-password="$REDIS_PASSWORD" --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic igrp-keycloak-admin -n "$NS" \
  --from-literal=admin-password="$KEYCLOAK_ADMIN_PASSWORD" --dry-run=client -o yaml | kubectl apply -f -
```

<Callout type="note" title="Módulo de notificações">
Vem desligado no `values.prod.yaml`. Com os Secrets pré-criados (`dbUsers.createSecrets: false`, este guia) a validação `validate-db-users` não corre, e os dois `db_secret` de notificações só são precisos se o módulo for activado.<br /><br />
Se for o chart a criar os Secrets (`createSecrets: true`), as entradas `notification` e `notificationTemporal` herdadas do `values.yaml` exigem password por `--set` **mesmo com o módulo desligado** — um `null` não as remove.<br /><br />
Os Secrets `igrp-notification-&#123;api,ui&#125;-oauth-client` **não** se criam aqui: são gerados pelos Jobs `m2m.autoRegister` no primeiro deploy, porque o `clientSecret` só é devolvido na criação do cliente no Access Management.
</Callout>

<Callout type="danger" title="A Web UI do Temporal não tem autenticação">
Em produção mantém `igrp-notification-temporal.webIngress.enabled: false`.
</Callout>

### 3. Chaves RSA e realm Keycloak [#3-chaves-rsa-e-realm-keycloak]

```bash
./scripts/generate-oauth-keys.sh "$NS" igrp-access-management-oauth-keys
```

No ficheiro local de produção — **não versionado**, `*.local.yaml` já está no `.gitignore` — desliga-se a criação de Secrets pelo chart, que passa apenas a referenciar os Secrets criados acima:

```yaml
# values.prod-secrets.local.yaml
igrp-pg-cluster:
  dbUsers:
    createSecrets: false
igrp-access-management:
  secret:
    create: false
  oauthClient:
    create: false
igrp-minio:
  credsSecret:
    create: false
igrp-app-center:
  nextauth:
    create: false
    existingSecret: igrp-app-center-nextauth
igrp-redis:
  authSecret:
    create: false
igrp-keycloak:
  keycloak:
    auth:
      existingSecret: igrp-keycloak-admin
  igrp:
    clientSecrets:
      accessManagement: "<valor de KEYCLOAK_AM_CLIENT_SECRET>"
```

<Callout type="info" title="Porque é que o segredo do Keycloak é a excepção">
Entra no `realm_config.json` como ConfigMap durante o primeiro import do realm. Um Kubernetes Secret não consegue fornecer esse valor ao template Helm, por isso este único segredo tem de estar no ficheiro local de values.
</Callout>

### 4. Deploy e validação [#4-deploy-e-validação]

```bash
helm dependency build .
helm upgrade --install igrp-stack . -n "$NS" \
  --values values.yaml --values values.prod.yaml \
  --values values.prod-secrets.local.yaml \
  --wait --timeout 15m
kubectl get pods,jobs,secrets -n "$NS"
kubectl get cluster,database -n "$NS"
```

Os Jobs do chart criam automaticamente os Secrets OAuth dos quatro serviços `igrp-process-*` quando `m2m.autoRegister.enabled: true`. Não os cries antes.


# Visão geral

URL: /pt/docs/developer/framework/backend

O que uma API Spring Boot gerada pelo Studio usa em runtime — cv.igrp.framework core (CQRS), stereotypes, autenticação, ficheiros e mail — e como a proteger e estender.

# Backend (Spring Boot) [#backend-spring-boot]

Uma API backend iGRP é um projeto **Spring Boot** gerado pelo Studio sobre as bibliotecas partilhadas de `shared/backend`. O Studio modela as entidades e gera o código; o framework fornece a arquitetura (CQRS), a segurança e as integrações com a plataforma.

## Bibliotecas [#bibliotecas]

| Módulo | Papel |
|---|---|
| `cv.igrp.framework:core` | Base de tudo: `CommandBus` / `QueryBus` (CQRS), convenções de erros, paginação, OpenTelemetry |
| `stereotype` | Anotações iGRP, incluindo `@IgrpPermission` (declaração de permissões) |
| `auth-*` (`keycloak`, `permify`, `client`, …) | Autenticação JWT (OAuth2 resource server) e autorização via Access Management, com cache |
| `file/core` (+ `minio`, `awss3`) | Storage de ficheiros, selecionado por `igrp.storage-provider` |
| `mail/core` (+ `smtp`) | Envio de email; delegável ao Notification Service |

<Grid cols={2}>
  <Feature icon="Leaf" color="green" title="Projeto Spring Boot" href="/docs/developer/framework/backend/springboot">
    Stack, estrutura de pastas, o que é gerado e o que é teu, configuração e como correr.
  </Feature>
  <Feature icon="ShieldCheck" color="purple" title="Permissões no backend" href="/docs/developer/framework/backend/permissoes">
    Declarar com `@IgrpPermission`, sincronizar com o Access Management, proteger endpoints com `@PreAuthorize`.
  </Feature>
  <Feature icon="Lock" color="blue" title="Segurança & configuração" href="/docs/developer/framework/backend/permissoes-seguranca">
    Resource server, filtro JWT, CORS e variáveis de ambiente.
  </Feature>
  <Feature icon="ListChecks" color="amber" title="Checklist & troubleshooting" href="/docs/developer/framework/backend/permissoes-referencia">
    Proteger endpoints passo a passo e resolver os erros mais comuns.
  </Feature>
</Grid>

## Como se constrói [#como-se-constrói]

<Steps>
  <Step>
    ### Modelar no Studio [#modelar-no-studio]
    Entidades e relações no [Model Designer](/docs/developer/studio/backend/model-designer) e no [Diagrama ER](/docs/developer/studio/backend/diagrama-er).
  </Step>
  <Step>
    ### Executar e testar [#executar-e-testar]
    [Run Preview](/docs/developer/studio/backend/run-preview) e [Testing](/docs/developer/studio/backend/testing) no Studio; em conjunto com os outros serviços, no [workspace local](/docs/developer/workspace/criar-workspace).
  </Step>
  <Step>
    ### Integrar com a plataforma [#integrar-com-a-plataforma]
    Permissões e menus via [Access Management](/docs/developer/access-management), processos via [BPMN](/docs/developer/process), emails via [Notificações](/docs/developer/notification).
  </Step>
</Steps>

<Callout type="info" title="Outros frameworks">
  .NET, Django e Go estão previstos para versões futuras do Studio; hoje o alvo de geração é Spring Boot.
</Callout>


# Endpoints e resolução de problemas

URL: /pt/docs/developer/framework/backend/permissoes-referencia

Como proteger endpoints com @PreAuthorize, checklist ponta-a-ponta e resolução de problemas comuns.

# Proteger Endpoints, Checklist & Troubleshooting [#proteger-endpoints-checklist--troubleshooting]

## 7. Como funciona — ciclo de vida do pedido [#7-como-funciona--ciclo-de-vida-do-pedido]

Por cada pedido HTTP autenticado, o Spring Security executa este pipeline antes de o controlador ser chamado:

<SpringRequestPipeline />

---

## 8. Proteger endpoints [#8-proteger-endpoints]

Há duas formas equivalentes de aplicar uma permissão a um endpoint. **Escolhe uma por controlador e mantém-te nela** — misturá-las no mesmo handler é receita para confusão.

### 8.1 Com `@PreAuthorize` em controladores escritos à mão [#81-com-preauthorize-em-controladores-escritos-à-mão]

O `IgrpAuthorizationService` está registado como bean com o nome **`igrpAuthorization`**. O SpEL `T(Permission)` resolve porque o `MethodSecurityConfig.IgrpTypeLocator` pré-registra o package `cv.igrp.framework.auth.generated`.

```java
import cv.igrp.framework.auth.generated.PermissionsRegistry.Permission;
import org.springframework.security.access.prepost.PreAuthorize;

@RestController
@RequestMapping("/api/invoices")
public class InvoiceController {

    // Permissão única
    @PostMapping
    @PreAuthorize("@igrpAuthorization.checkPermission(T(Permission).MY_INVOICE_CREATE)")
    public ResponseEntity<InvoiceDTO> create(@RequestBody InvoiceDTO dto) { ... }

    // Todas (o utilizador tem de ter AMBAS)
    @PostMapping("/{id}/approve")
    @PreAuthorize("@igrpAuthorization.checkAllPermissions("
                + "T(Permission).MY_INVOICE_VIEW, "
                + "T(Permission).MY_INVOICE_APPROVE)")
    public ResponseEntity<Void> approve(@PathVariable UUID id) { ... }

    // Qualquer (o utilizador tem de ter PELO MENOS UMA)
    @GetMapping("/{id}")
    @PreAuthorize("@igrpAuthorization.checkAnyPermission("
                + "T(Permission).MY_INVOICE_VIEW, "
                + "T(Permission).MY_INVOICE_APPROVE)")
    public ResponseEntity<InvoiceDTO> get(@PathVariable UUID id) { ... }
}
```

### 8.2 Através do separador Security do iGRP Studio [#82-através-do-separador-security-do-igrp-studio]

Para controladores gerados pelo iGRP Studio, **não anotes o código-fonte** — o Studio sobrepõe-no na próxima regeneração. Abre o endpoint no Studio → separador **Security**:

<StudioPreview imageName="securityTabEndpoint" alt="Separador Security da ação criarPedido, com a permissão pedidos.criar e o operador OR" />

- **Permissões** — em **Adicionar Permissão** escreve o nome de cada permissão (ex.: `pedidos.criar`) e **Adicionar**. Em **Operador de Permissões**, `AND` exige **todas** as permissões listadas e `OR` basta **uma qualquer**.
- **Funções** — em **Adicionar Função** escreve o código de cada perfil (ex.: `ADMIN`, `OPERADOR`). O utilizador tem de ter **pelo menos um** dos perfis listados.

A geração de código do Studio emite então o `@PreAuthorize("@igrpAuthorization.check…")` equivalente para permissões, ou `@PreAuthorize("hasRole('...')")` / `hasAnyRole(...)` para perfis, no método do controlador gerado. O comportamento em runtime é idêntico à forma manual acima.

### 8.3 Bypass de super-admin [#super-admin]

`IgrpAuthorizationService.checkPermission`, `checkAllPermissions` e `checkAnyPermission` fazem todos short-circuit para `true` quando o chamador atual tem a authority `IGRP_SUPER_ADMIN` (da claim `is_super_admin=true` do JWT).

<Callout type="danger" title="Todo o `@PreAuthorize` com `@igrpAuthorization.check…` permite implicitamente super-admins">
Se não quiseres esse comportamento num endpoint específico, protege-o explicitamente contra uma authority que o super-admin não transporte, ou verifica `!@igrpAuthorization.isSuperAdmin()` a par do teu predicado de negócio.
</Callout>

O serviço expõe também `isSuperAdmin()` como predicado SpEL de primeira classe, útil em composições OR:

```java
@PreAuthorize("@igrpAuthorization.isSuperAdmin() "
            + "or @igrpAuthorization.checkPermission(T(Permission).MY_INVOICE_APPROVE)")
```

A revogação é uma alteração de dados no Access Management (remover a flag/perfil de super-admin do utilizador). Só produz efeito no JWT seguinte.

### 8.4 Verificações por perfil (role) [#84-verificações-por-perfil-role]

Se quiseres proteger por **perfil** em vez de permissão, usa os helpers SpEL padrão do Spring — os perfis do JWT chegam como authorities `ROLE_*` através do `IgrpJwtAuthenticationConverter`:

```java
@PreAuthorize("hasRole('INVOICE_APPROVER')")
@PreAuthorize("hasAnyRole('INVOICE_APPROVER','FINANCE_ADMIN')")
```

…ou combina ambos numa única expressão:

```java
@PreAuthorize("hasRole('FINANCE_ADMIN') "
            + "or @igrpAuthorization.checkPermission(T(Permission).MY_INVOICE_APPROVE)")
```

Prefere verificações por permissão para regras de negócio do dia-a-dia — são mais finas e sobrevivem a renomeações de perfis e reorganizações. Reserva as verificações por perfil para portões grosseiros do tipo "esta pessoa está, em termos gerais, dentro da equipa X".

---

## 9. Checklist ponta a ponta [#9-checklist-ponta-a-ponta]

Quando ligas um projeto alvo pela primeira vez, percorre esta lista uma vez:

1. `mvn clean compile` — confirma que `target/generated-sources/annotations/cv/igrp/framework/auth/generated/PermissionsRegistry.java` existe. Se não, ver [§2.4](#processador-anotacoes).
2. Arranca a aplicação. Nos logs deves ver as linhas exatas emitidas pelo `AuthorizationSyncRunner`:
   ```
   [Authorization Sync] Starting authorization synchronization with Access Management API...
   [Authorization Sync] Authenticating to Access Management via client_credentials grant (clientId=...)
   [Authorization Sync] Synchronizing resource for application 'my-invoice-service'
   [Authorization Sync] Resource synchronization completed.
   [Authorization Sync] Synchronizing N permissions for application 'my-invoice-service'
   [Permission Sync] Successfully synchronized N permissions.
   ```
3. Abre a UI do Access Management → **Settings** → separador **Gestão de Acessos** → seleciona um departamento → **Permissions** → **Gerir Recursos** → confirma que o teu `${spring.application.name}` está listado e ativado, e que todas as tuas permissões aparecem para atribuição.
4. Separador **Permissions** → confirma que cada `@IgrpPermission` que definiste aparece com a descrição esperada.
5. Atribui uma permissão a um perfil no Access Management. Faz com que um utilizador nesse perfil obtenha um JWT pelo fluxo normal de login. Descodifica o JWT — o array `permissions` tem de incluir o teu código de permissão.
6. Chama um endpoint protegido com esse JWT. Espera 200 ou 403, conforme a permissão esteja ou não concedida.
7. Remove a permissão do perfil no Access Management. Volta a obter o JWT (um token em cache antigo continua a passar até expirar). Espera 403 no mesmo endpoint.

---

## 10. Resolução de problemas [#10-resolução-de-problemas]

| Sintoma | Causa provável | Onde procurar |
| --- | --- | --- |
| `PermissionsRegistry` não gerado em `target/` | Caminho do processador de anotações em falta | `pom.xml` → `annotationProcessorPaths` ([§2.4](#processador-anotacoes)) |
| `[Permission Sync] Failed to synchronize…` a cada arranque | Credenciais M2M erradas ou em falta | `application.properties` ([§5](#configuracao)); confirma que o cliente M2M está registado no Access Management com `grant_type=client_credentials` |
| `401` em todos os pedidos à tua API | Falha de validação do JWT — issuer não coincide | `spring.security.oauth2.resourceserver.jwt.issuer-uri` tem de ser igual à claim `iss` do JWT, byte a byte |
| `403` num endpoint protegido apesar de o perfil ter a permissão | JWT desatualizado; JWT não apanhado pelo `IgrpJwtAuthenticationConverter`; ou o nome da claim não coincide | Descodifica o bearer → verifica o array `permissions`; confirma que o `IgrpJwtAuthenticationConverter` está no classpath (`core-spring-boot`) |
| SpEL `T(Permission)` não resolve no `@PreAuthorize` | `MethodSecurityConfig` não foi apanhado | Confirma que `core-spring-boot` está no classpath e que `@EnableMethodSecurity` está na tua `SecurityConfig` |
| Utilizador criado no modelo de domínio com NPE em `sub` | `IAMUserProfileSyncFilter` fora da chain | `SecurityConfig` → `addFilterAfter(iamUserProfileSyncFilter, BearerTokenAuthenticationFilter.class)` |
| Permissão renomeada localmente, código antigo continua no JWT | O Access Management ainda tem o `name` antigo e o perfil continua a referenciá-lo | A renomeação tem 3 passos: código → arrancar para registar a nova permissão → o administrador reatribui aos perfis → remover o código antigo no release seguinte |
| A aplicação não arranca: `NoSuchBeanDefinitionException: ApiClient` / NPE dentro de `AuthorizationSyncRunner` | `igrp.access.api.base-url` em falta — não tem default | Define a propriedade (ou `IGRP_ACCESS_API_BASE_URL`) com o URL base do Access Management |
| Todos os `@PreAuthorize` passam para um utilizador específico | Esse utilizador tem `is_super_admin=true` — `checkPermission/…` faz short-circuit | Revoga o super-admin no Access Management e reemite o JWT ([§8.3](#super-admin)) |
| `hasRole('X')` nega apesar de o utilizador ter o perfil X atribuído | O conversor só expõe `ROLE_<selectedRole>` (o perfil ativo), não o conjunto completo | Faz com que o utilizador escolha o perfil X no ecrã de login, ou verifica permissões em vez de perfis |
| O mesmo JWT devolve authorities desatualizadas após alterações no Access Management | As caches do adaptador (`igrp.auth.*Cache`) têm chave na string do JWT — um JWT rotado invalida naturalmente, um inalterado não | Limpa as caches manualmente ou aguarda a rotação do token; as entradas são despejadas com o TTL do token |
| `UUID.fromString(sub)` no `IAMUserProfileSyncFilter` lança exceção | `sub` é a string do id interno do utilizador (pode não ser um UUID em todos os deployments) | Envolve em try/catch e deriva um UUID determinístico, ou torna `t_iam_user_profile.id` um `String` |

Se nenhum dos sintomas encaixar, ativa temporariamente o logging TRACE do Spring Security:

```properties
logging.level.org.springframework.security=TRACE
logging.level.cv.igrp.framework.auth=DEBUG
```

…e procura o filtro/provider exato que correu. O output TRACE nomeia cada filtro e a decisão que devolveu para o pedido.

---

## A seguir [#a-seguir]

- [Frontend — Next.js](../frontend/permissoes.mdx) — como as mesmas permissões protegem páginas, componentes e server actions.
- [Access Manager](../../access-management/index.mdx) — como o Access Management gere recursos, perfis, departamentos e menus.


# Segurança e configuração

URL: /pt/docs/developer/framework/backend/permissoes-seguranca

Variáveis de ambiente do serviço Spring Boot e configuração de segurança — SecurityConfig e IAMUserProfileSyncFilter.

# Segurança & Configuração [#segurança--configuração]

## 5. `application.properties` / variáveis de ambiente [#5-applicationproperties--variáveis-de-ambiente]

Bloco mínimo obrigatório:

```properties
# ─── Identidade deste serviço ───────────────────────────────────────────────
# Usado como nome do Resource no Access Management, como audience do JWT que a
# verificação do resource server espera, e nos logs de auditoria.
spring.application.name=my-invoice-service

# ─── Onde vive o Access Management ──────────────────────────────────────────
# Issuer URI do AS — o resource server usa isto para descoberta de JWKS e
# validação de `iss`. Tem de ser o URL público EXATO (igual à claim iss byte a
# byte) — http vs https, barra final, host, porta, prefixo de caminho.
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://api-demoigrp.nosi.cv/igrp-access-management

# URL base que a biblioteca de cliente M2M usa para /api/m2m/* e /oauth2/token.
# OBRIGATÓRIO — não tem default. Se faltar, o bean ApiClient falha a inicializar
# e o contexto não arranca.
igrp.access.api.base-url=https://api-demoigrp.nosi.cv/igrp-access-management

# ─── Credenciais do cliente M2M (usadas pelo AuthorizationSyncRunner) ───────
# Este cliente está registado no Access Management com
# grant_type=client_credentials e o(s) scope(s) que precisa para chamar
# /api/m2m/*. Nunca faças commit do secret — injeta por variável de ambiente.
igrp.access.m2m.client-id=my-invoice-service-m2m
igrp.access.m2m.client-secret=${IGRP_ACCESS_M2M_CLIENT_SECRET:}
# Opcional — só se o registo do cliente restringir os scopes disponíveis:
igrp.access.m2m.scope=m2m
```

### Mapeamento de variáveis de ambiente [#mapeamento-de-variáveis-de-ambiente]

As propriedades acima são lidas pela via padrão do Spring (`@Value` → `${...}`). Sobrepõe-as em tempo de deploy com variáveis de ambiente em maiúsculas separadas por `_`:

| Propriedade | Variável de ambiente |
| --- | --- |
| `igrp.access.m2m.client-id` | `IGRP_ACCESS_M2M_CLIENT_ID` |
| `igrp.access.m2m.client-secret` | `IGRP_ACCESS_M2M_CLIENT_SECRET` |
| `igrp.access.m2m.scope` | `IGRP_ACCESS_M2M_SCOPE` |
| `igrp.access.m2m.sync-token` | `IGRP_ACCESS_M2M_SYNC_TOKEN` |
| `igrp.access.api.base-url` | `IGRP_ACCESS_API_BASE_URL` |
| `spring.security.oauth2.resourceserver.jwt.issuer-uri` | `SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI` |
| `spring.application.name` | `SPRING_APPLICATION_NAME` |

### Legado: `igrp.access.m2m.sync-token` [#legado-igrpaccessm2msync-token]

O `AuthorizationSyncRunner` ainda lê um valor estático `igrp.access.m2m.sync-token`, já *deprecated*, como fallback quando não há par de client-credentials configurado.

<Callout type="danger" title="Não usar em deployments novos">
Só existe para projetos anteriores à `0.2.0-beta` que ainda não migraram. O caminho do token estático não é protegido por rotação de tokens e será removido numa versão futura da framework.
</Callout>

```properties
# Apenas para projetos legados anteriores à 0.2.0-beta:
igrp.access.m2m.sync-token=${IGRP_ACCESS_M2M_SYNC_TOKEN:}
```

### Parâmetros opcionais da framework [#parâmetros-opcionais-da-framework]

```properties
# Desliga o @EnableMethodSecurity automático da framework (default: true).
# Só desliga isto se registares o teu próprio MethodSecurityConfig.
igrp.framework.auth.method-security.enabled=false

# Troca o adaptador de claims do JWT. `igrp` é o default e lê as claims
# emitidas pelo Access Management (permissions / selectedRole / org /
# is_super_admin). Só muda isto se fornecerdes um bean
# IAuthorizationServiceAdapter personalizado.
igrp.authorization.service.adapter=igrp
```

---

## 6. Classe de configuração de segurança [#6-classe-de-configuração-de-segurança]

Normalmente são precisas duas peças:

1. A **configuração Spring Security** que liga a validação de JWT do resource server e ativa a segurança a nível de método (`@PreAuthorize`).
2. Um **`IAMUserProfileSyncFilter`** que espelha as claims de identidade do JWT numa tabela local `t_iam_user_profile`, para que o modelo de negócio possa fazer join contra FKs de utilizador sem ir ao Access Management a cada pedido.

### 6.1 SecurityConfig [#61-securityconfig]

```java
package cv.igrp.demo.shared.security;

import cv.igrp.framework.auth.core.security.IgrpJwtAuthenticationConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableMethodSecurity   // ← ativa @PreAuthorize / @PostAuthorize
public class SecurityConfig {

    private final IgrpJwtAuthenticationConverter jwtConverter;
    private final IAMUserProfileSyncFilter iamUserProfileSyncFilter;

    public SecurityConfig(IgrpJwtAuthenticationConverter jwtConverter,
                          IAMUserProfileSyncFilter iamUserProfileSyncFilter) {
        this.jwtConverter = jwtConverter;
        this.iamUserProfileSyncFilter = iamUserProfileSyncFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
                    "/actuator/health/**", "/actuator/info"
                ).permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
                jwt.jwtAuthenticationConverter(jwtConverter)
            ))
            // Depois de o JWT ser validado e a Authentication estar no contexto,
            // espelha as claims de identidade na nossa tabela local
            // t_iam_user_profile. Fazê-lo aqui (em vez de por pedido nos
            // controladores) significa que qualquer handler autenticado pode
            // contar com a existência de uma linha local.
            .addFilterAfter(iamUserProfileSyncFilter,
                            org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration c = new CorsConfiguration();
        c.setAllowedOrigins(List.of("https://my-spa.example.com"));   // nunca "*" com credenciais
        c.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        c.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        c.setAllowCredentials(true);
        var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", c);
        return source;
    }
}
```

O que cada peça faz, em uma linha:

- **`@EnableMethodSecurity`** — ativa a avaliação de `@PreAuthorize` / `@PostAuthorize`. Desde o `core-spring-boot` `0.2.0-beta`, o `MethodSecurityEnableConfig` da framework **ativa a segurança de método por default** (`@ConditionalOnProperty(matchIfMissing=true)` sobre `igrp.framework.auth.method-security.enabled`), e o `MethodSecurityConfig` registra automaticamente `IgrpMethodSecurityExpressionHandler` + `IgrpTypeLocator`, para que o SpEL resolva `T(Permission)` sem o caminho completo do package. Manter `@EnableMethodSecurity` na tua `SecurityConfig` é inofensivo e continua a ser legível, mas já não é obrigatório.
- **`IgrpJwtAuthenticationConverter`** (autoconfigurado por `core-spring-boot`) — delega num `IAuthorizationServiceAdapter` (default: `IgrpAuthorizationServiceAdapter`) que lê **quatro** claims do JWT emitido pelo Access Management e converte cada uma em `GrantedAuthority`:
  - `permissions` (`Set<String>`) — cada entrada torna-se um `SimpleGrantedAuthority("<code>")`.
  - `selectedRole` (String — o perfil *atualmente ativo* desta sessão) — torna-se `ROLE_<code>` **e** `GROUP_<code>`.
  - `org` (String — o código do departamento do perfil ativo) — torna-se `<code>` **e** `GROUP_<code>`.
  - `is_super_admin` (boolean) — quando `true`, adiciona a authority marcadora `IGRP_SUPER_ADMIN` (`IgrpAuthorizationService.SUPER_ADMIN_AUTHORITY`).

  O conversor adiciona também sempre `ROLE_ACTIVITI_USER`.
- **`oauth2ResourceServer.jwt(...)`** — ligação padrão do Spring. Usa `spring.security.oauth2.resourceserver.jwt.issuer-uri` para buscar o JWKS e validar assinatura, `exp` e `iss`.
- **`STATELESS`** — APIs não precisam de HttpSession; cada pedido autentica-se por Bearer.

<Callout type="note" title="Cache do adaptador">
Os métodos de `IgrpAuthorizationServiceAdapter` são `@Cacheable` (`igrp.auth.rolesCache`, `permissionsCache`, `departmentsCache`, `superAdminCache`), com chave na string do JWT. Se a aplicação consumidora não configurar um cache manager do Spring (`@EnableCaching` + um provider em `spring.cache.type`), as anotações degradam para no-op — o código continua correto, apenas sem cache.
</Callout>

<Callout type="warning" title="A semântica dos perfis mudou">
Só o perfil *atualmente selecionado* é exposto como `ROLE_<code>`, não a união de todos os perfis atribuídos ao utilizador. `hasRole('X')` avalia portanto contra um perfil de cada vez — o que o utilizador escolheu no login. Desenha as verificações contra permissões, não perfis, sempre que possível.
</Callout>

### 6.2 `IAMUserProfileSyncFilter` [#62-iamuserprofilesyncfilter]

Este filtro corre uma vez por pedido autenticado e espelha as claims de identidade do JWT numa linha local `t_iam_user_profile`. A chave primária da linha é o `sub` do JWT (o id de utilizador emitido pelo Access Management). É assim que o teu modelo de domínio (`Invoice.createdBy`, `Approval.approvedBy`, etc.) pode ter uma verdadeira foreign key em vez de uma string opaca.

<Callout type="warning" title="Duas ressalvas antes de copiares este filtro">
1. **`sub` não é garantidamente um UUID.** Para tokens de utilizador, o Access Management põe em `sub` o `IGRPUserEntity.id` interno (atualmente uma string UUID, mas o tipo da claim é `String`). Para tokens de service account é o UUID de `ServiceAccountEntity`. Se o teu schema exigir que a coluna de id seja `UUID`, envolve o `UUID.fromString(sub)` em try/catch e cai para um UUID determinístico derivado de `sub` — ou muda o tipo da coluna para `String`.
2. **As claims de identidade dependem dos scopes.** `preferred_username`, `email`, `given_name`, `family_name`, `picture` e `locale` só existem quando o token foi emitido com os scopes `openid profile email`. O registo do teu cliente OAuth2 no Access Management tem de os pedir, ou o filtro guarda nulls.
</Callout>

```java
package cv.igrp.demo.shared.security;

import cv.igrp.demo.shared.infrastructure.persistence.entity.IAMUserProfileEntity;
import cv.igrp.demo.shared.infrastructure.persistence.repository.IAMUserProfileEntityRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.UUID;

@Component
public class IAMUserProfileSyncFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(IAMUserProfileSyncFilter.class);

    private final IAMUserProfileEntityRepository repository;

    public IAMUserProfileSyncFilter(IAMUserProfileEntityRepository repository) {
        this.repository = repository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        if (auth instanceof JwtAuthenticationToken jwtAuth) {
            Jwt jwt = jwtAuth.getToken();
            String sub = jwt.getSubject();
            try {
                var existing = repository.findBySub(sub);
                if (existing.isEmpty()) {
                    LOGGER.debug("Creating IAM user profile. sub={}", sub);
                    repository.save(buildEntity(jwt));
                } else {
                    var updated = applyChanges(existing.get(), jwt);
                    if (updated) {
                        LOGGER.debug("Updating IAM user profile. sub={}", sub);
                        repository.save(existing.get());
                    }
                }
            } catch (Exception e) {
                // Nunca falhar o pedido porque o espelho local se portou mal.
                LOGGER.warn("Failed to sync IAM user profile. sub={}, error={}", sub, e.getMessage(), e);
            }
        }

        chain.doFilter(request, response);
    }

    private boolean applyChanges(IAMUserProfileEntity entity, Jwt jwt) {
        String preferredUsername = jwt.getClaimAsString("preferred_username");
        String email             = jwt.getClaimAsString("email");
        String firstName         = jwt.getClaimAsString("given_name");
        String lastName          = jwt.getClaimAsString("family_name");
        String fullName          = buildFullName(firstName, lastName);

        String username = (preferredUsername != null && !preferredUsername.isBlank())
            ? preferredUsername
            : email;

        boolean changed = false;
        if (username != null && !username.equals(entity.getUsername())) { entity.setUsername(username); changed = true; }
        if (!java.util.Objects.equals(email,    entity.getEmail()))     { entity.setEmail(email);       changed = true; }
        if (!java.util.Objects.equals(firstName,entity.getFirstName())) { entity.setFirstName(firstName); changed = true; }
        if (!java.util.Objects.equals(lastName, entity.getLastName()))  { entity.setLastName(lastName); changed = true; }
        if (!java.util.Objects.equals(fullName, entity.getFullName()))  { entity.setFullName(fullName); changed = true; }
        return changed;
    }

    private IAMUserProfileEntity buildEntity(Jwt jwt) {
        String sub               = jwt.getSubject();
        String preferredUsername = jwt.getClaimAsString("preferred_username");
        String email             = jwt.getClaimAsString("email");
        String firstName         = jwt.getClaimAsString("given_name");
        String lastName          = jwt.getClaimAsString("family_name");

        String username = (preferredUsername != null && !preferredUsername.isBlank())
            ? preferredUsername
            : email;
        if (username == null || username.isBlank()) {
            throw new IllegalStateException("Could not determine username from JWT claims. sub=" + sub);
        }

        var entity = new IAMUserProfileEntity();
        entity.setId(UUID.fromString(sub));
        entity.setSub(sub);
        entity.setUsername(username);
        entity.setEmail(email);
        entity.setFirstName(firstName);
        entity.setLastName(lastName);
        entity.setFullName(buildFullName(firstName, lastName));
        return entity;
    }

    private String buildFullName(String firstName, String lastName) {
        String first = StringUtils.hasText(firstName) ? firstName.strip() : null;
        String last  = StringUtils.hasText(lastName)  ? lastName.strip()  : null;
        if (first == null && last == null) return null;
        if (first == null) return last;
        if (last  == null) return first;
        return first + " " + last;
    }
}
```

Entidade e repositório de suporte, para que o filtro compile de forma autónoma no teu projeto:

```java
@Audited
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
@Entity @IgrpEntity
@Table(name = "t_iam_user_profile",
    indexes = {
        @Index(name = "idx_iam_user_profile_username", columnList = "username"),
        @Index(name = "idx_iam_user_profile_full_name", columnList = "full_name"),
        @Index(name = "idx_iam_user_profile_sub",       columnList = "sub")
    }
)
public class IAMUserProfileEntity extends AuditEntity {

    @Id @Column(name = "id", unique = true, nullable = false)
    private UUID id;

    @NotBlank @Column(name = "username", unique = true, nullable = false)
    private String username;

    @Column(name = "email", unique = true)         private String email;
    @Column(name = "first_name")                   private String firstName;
    @Column(name = "last_name")                    private String lastName;
    @Column(name = "full_name")                    private String fullName;

    @NotBlank @Column(name = "sub", unique = true, nullable = false)
    private String sub;
}

@Repository
public interface IAMUserProfileEntityRepository
        extends JpaRepository<IAMUserProfileEntity, UUID>,
                JpaSpecificationExecutor<IAMUserProfileEntity>,
                RevisionRepository<IAMUserProfileEntity, UUID, Integer> {

    default IAMUserProfileEntity findByIdOrThrow(UUID id) {
        return findById(id).orElseThrow(() ->
            IgrpResponseStatusException.notFound("IAMUserProfileEntity not found for id: " + id));
    }

    Optional<IAMUserProfileEntity> findByUsername(String username);
    Optional<IAMUserProfileEntity> findBySub(String sub);
    List<IAMUserProfileEntity> findBySubIn(Collection<String> subs);
}
```

---


# Permissões no Backend

URL: /pt/docs/developer/framework/backend/permissoes

Declarar permissões de negócio com @IgrpPermission, sincronizar com o Access Management e aplicá-las com @PreAuthorize.

# Permissões no Backend (Spring Boot) [#permissões-no-backend-spring-boot]

**Destinatários:** developers a construir um **projeto de API de negócio** — qualquer serviço Spring Boot que precise de declarar as suas próprias permissões e tê-las aplicadas através da API central do iGRP Access Management.

Este guia cobre o ciclo de vida completo do lado do projeto alvo:

1. Declarar as permissões de negócio no código com `@IgrpPermission`.
2. Compilar — o processador de anotações da framework gera `PermissionsRegistry.Permission` em `target/generated-sources/`.
3. Arrancar — a autoconfig `AuthorizationSyncRunner` sincroniza o catálogo de recursos e permissões no Access Management.
4. Em tempo de pedido — os JWTs emitidos pelo Access Management transportam as permissões concedidas como *authorities*; `@PreAuthorize` (ou o separador **Security** do iGRP Studio) aplica-as.

Nada neste guia é interno ao Access Management. Aplica-se a qualquer serviço Spring Boot que inclua `cv.igrp.framework.auth:core-spring-boot`.

---

## 1. O panorama geral [#1-o-panorama-geral]

<SpringPermissionsSync />

Escreves só as anotações. Os JARs da framework fornecem tudo o resto — o processador de anotações, o runner de autoconfig, o conversor de JWT e o serviço SpEL. Só precisas da configuração que este guia detalha. O que acontece a cada pedido está em [Endpoints & Troubleshooting](./permissoes-referencia.mdx).

---

## 2. Configuração Maven [#2-configuração-maven]

### 2.1 Bloco de propriedades [#21-bloco-de-propriedades]

Fixa as versões da framework num único lugar para que todos os módulos do projeto concordem:

```xml
<properties>
    <java.version>26</java.version>
    <framework.auth.version>0.2.0-beta.9</framework.auth.version>
    <!-- Só necessário se mantiveres um bloco <annotationProcessorPaths> explícito (ver §2.4).
         core é lançado independentemente de core-spring-boot e a sua última versão é 0.2.0-beta.8;
         NÃO reutilizes ${framework.auth.version}. -->
    <framework.auth.core.version>0.2.0-beta.8</framework.auth.core.version>
</properties>
```

<Callout type="warning" title="Não fixes as dependências transitivas">
Não precisas de fixar `cv.igrp.framework:stereotype` (que define `@IgrpPermission`) nem `cv.igrp.framework.auth:core` (que contém o processador de anotações) — ambos vêm **transitivamente** por `core-spring-boot`. Declará-los outra vez só arrisca um desalinhamento com as versões contra as quais `core-spring-boot` foi construído. A única razão para `framework.auth.core.version` existir acima é o caminho explícito de processador de anotações da §2.4.
</Callout>

### 2.2 Repositórios [#22-repositórios]

Os JARs da framework vivem no Nexus do iGRP, não no Maven Central. Adiciona os repositórios *e* o espelho `pluginRepository`, para que o processador de anotações possa ser resolvido durante a compilação:

```xml
<repositories>
    <repository>
        <id>igrp-framework-releases</id>
        <url>https://sonatype.nosi.cv/repository/igrp-framework-releases/</url>
    </repository>
    <repository>
        <id>igrp-framework-snapshots</id>
        <url>https://sonatype.nosi.cv/repository/igrp-framework/</url>
        <snapshots><enabled>true</enabled></snapshots>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>igrp-framework-releases</id>
        <url>https://sonatype.nosi.cv/repository/igrp-framework-releases/</url>
    </pluginRepository>
</pluginRepositories>
```

### 2.3 Dependências de runtime [#23-dependências-de-runtime]

Adiciona **uma** dependência iGRP — `core-spring-boot`. Traz transitivamente:

- `cv.igrp.framework.auth:core` — o processador de anotações, `IgrpAuthorizationService`, `IgrpJwtAuthenticationConverter`, `MethodSecurityConfig` e a auto-configuração.
- `cv.igrp.framework:stereotype` — a anotação `@IgrpPermission`.
- `cv.igrp.platform.access:client` — o `ApiClient` / `M2MApi` usado pelo `AuthorizationSyncRunner`.
- `spring-boot-starter-oauth2-resource-server` — validação de JWT.

**Não** redeclares nenhum desses artefactos no teu `pom.xml` — só convida ao desalinhamento de versões.

```xml
<dependencies>
    <!-- Tudo o que este guia descreve vem daqui. -->
    <dependency>
        <groupId>cv.igrp.framework.auth</groupId>
        <artifactId>core-spring-boot</artifactId>
        <version>${framework.auth.version}</version>
    </dependency>

    <!-- Starters Spring Boot que a tua API usa diretamente. -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>
```

Um `pom.xml` mínimo completo — Spring Boot parent, só a dependência iGRP e os dois starters, sem overrides de `<dependencyManagement>` — é a referência por onde todo o projeto novo deve começar:

```xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.14</version>
    <relativePath/>
</parent>

<properties>
    <java.version>26</java.version>
    <framework.auth.version>0.2.0-beta.9</framework.auth.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>cv.igrp.framework.auth</groupId>
        <artifactId>core-spring-boot</artifactId>
        <version>${framework.auth.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
```

<div id="section-2-4"></div>

### 2.4 Processador de anotações [#24-processador-de-anotações]

**Na maioria dos projetos não é necessário nada extra.** `@IgrpPermission` (de `stereotype`) e o processador `PermissionSourceGenerator` (de `core`) vêm ambos transitivamente por `core-spring-boot`, pelo que em tempo de compilação o processador é descoberto automaticamente via `META-INF/services/javax.annotation.processing.Processor`. Corre `mvn clean compile` e confirma que aparece:

```
target/generated-sources/annotations/cv/igrp/framework/auth/generated/PermissionsRegistry.java
```

Se **já** declaras um bloco `<annotationProcessorPaths>` para outros processadores (Lombok, MapStruct, …), o Maven usa só esse bloco e deixa de fazer scan automático do classpath. Nesse caso, adiciona `core` explicitamente (traz `stereotype` transitivamente, logo uma entrada cobre os dois):

```xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>26</source>
                <target>26</target>
                <encoding>UTF-8</encoding>
                <compilerArgs>
                    <arg>-parameters</arg>
                </compilerArgs>

                <annotationProcessorPaths>
                    <!-- Lombok primeiro, se o usares -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </path>

                    <!-- Gera PermissionsRegistry.java.
                         core traz transitivamente cv.igrp.framework:stereotype,
                         que define a própria @IgrpPermission.
                         core é lançado independentemente de core-spring-boot —
                         usa a sua própria propriedade de versão. -->
                    <path>
                        <groupId>cv.igrp.framework.auth</groupId>
                        <artifactId>core</artifactId>
                        <version>${framework.auth.core.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
```

Se `PermissionsRegistry.java` não aparecer após um *clean compile*, a causa é quase sempre: (a) introduziste um bloco `<annotationProcessorPaths>` sem adicionar `core`, ou (b) o compilador tem `<proc>none</proc>`.

#### Opcional — declaração estática do processador [#opcional--declaração-estática-do-processador]

Se preferires fixar os processadores a classes concretas — vários JARs de processadores no path e queres ordenação determinística, ou queres contornar a descoberta por SPI — usa `<annotationProcessors>` a par do bloco de caminhos:

```xml
<configuration>
    <annotationProcessorPaths>
        <!-- ... igual ao anterior ... -->
    </annotationProcessorPaths>

    <annotationProcessors>
        <annotationProcessor>lombok.launch.AnnotationProcessorHider$AnnotationProcessor</annotationProcessor>
        <annotationProcessor>cv.igrp.framework.auth.core.processor.PermissionSourceGenerator</annotationProcessor>
    </annotationProcessors>
</configuration>
```

Prefere a forma implícita (descoberta por SPI) a não ser que tenhas uma razão concreta.

### 2.5 Ligação das generated-sources à IDE [#25-ligação-das-generated-sources-à-ide]

Depois de `mvn compile`, a IDE precisa de tratar `target/generated-sources/annotations` como source root, para que as referências a `PermissionsRegistry.Permission.*` resolvam:

```xml
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>add-generated-source</id>
            <phase>generate-sources</phase>
            <goals><goal>add-source</goal></goals>
            <configuration>
                <sources>
                    <source>${project.build.directory}/generated-sources/annotations</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>
```

O IntelliJ costuma apanhar `annotations` automaticamente; num clone novo faz *Maven → Reload All Projects*, ou clica com o botão direito na pasta `annotations` → *Mark Directory as → Generated Sources Root*.

---

## 3. Declarar permissões com `@IgrpPermission` [#3-declarar-permissões-com-igrppermission]

A anotação vive em `cv.igrp.framework.stereotype` e aplica-se a **campos estáticos**:

```java
// src/main/java/cv/igrp/demo/shared/authorization/InvoicePermissions.java
package cv.igrp.demo.shared.authorization;

import cv.igrp.framework.stereotype.IgrpPermission;

public final class InvoicePermissions {

    private InvoicePermissions() {}

    @IgrpPermission(name = "my.invoice.create", description = "Criar uma fatura")
    public static final String INVOICE_CREATE = "my.invoice.create";

    @IgrpPermission(name = "my.invoice.view", description = "Consultar uma fatura")
    public static final String INVOICE_VIEW = "my.invoice.view";

    @IgrpPermission(name = "my.invoice.approve", description = "Aprovar uma fatura", enabled = true)
    public static final String INVOICE_APPROVE = "my.invoice.approve";

    @IgrpPermission(name = "my.invoice.export", description = "Exportar para CSV/PDF", enabled = false)
    public static final String INVOICE_EXPORT = "my.invoice.export";
}
```

Convenções que compensam:

- **`name`** é o **código canónico da permissão** — sensível a maiúsculas, minúsculas pontuadas, com namespace do teu contexto delimitado. É o que fica guardado no Access Management e o que aparece no JWT.
- **O valor da constante String** deve ser igual ao `name`, byte por byte. A constante dá autocomplete na IDE nos controladores (strings de `@PreAuthorize`) e permite que ferramentas fora do mundo das anotações referenciem o mesmo identificador.
- **`description`** é o que os administradores vêem na UI do Access Management ao atribuir a permissão a um perfil. Escreve-a como uma frase.
- **`enabled = false`** registra a permissão no Access Management como `INACTIVE` — útil para permissões que queres que os administradores descubram mas ainda não concedam.
- **Uma classe por contexto delimitado.** Não despejes todas as permissões de todos os domínios num ficheiro — torna os diffs ilegíveis e os conflitos de merge inevitáveis em equipas grandes.

### O que o processador de anotações gera [#o-que-o-processador-de-anotações-gera]

Em `mvn compile`, o `PermissionSourceGenerator` (de `cv.igrp.framework.auth:core`) percorre todas as `@IgrpPermission` que consegue ver e escreve um único ficheiro:

```
target/generated-sources/annotations/cv/igrp/framework/auth/generated/PermissionsRegistry.java
```

…com esta forma:

```java
package cv.igrp.framework.auth.generated;

public final class PermissionsRegistry {

    public enum Permission {
        MY_INVOICE_CREATE ("my.invoice.create", "Criar uma fatura",     true),
        MY_INVOICE_VIEW   ("my.invoice.view",   "Consultar uma fatura", true),
        MY_INVOICE_APPROVE("my.invoice.approve","Aprovar uma fatura",   true),
        MY_INVOICE_EXPORT ("my.invoice.export", "Exportar para CSV/PDF",false);

        private final String code;
        private final String description;
        private final boolean enabled;

        Permission(String code, String description, boolean enabled) {
            this.code = code;
            this.description = description;
            this.enabled = enabled;
        }

        public String getCode()        { return code; }
        public String getDescription() { return description; }
        public boolean enabled()       { return enabled; }
    }
}
```

O nome da constante do enum deriva de `name()` (maiúsculas, não alfanuméricos → `_`).

<Callout type="warning" title="Nomes duplicados são silenciosamente descartados">
Valores de `name()` duplicados (comparação insensível a maiúsculas) produzem um **aviso** do compilador e a segunda ocorrência é **descartada em silêncio** — só a primeira `@IgrpPermission` com aquele nome sobrevive. Trata duplicados como bugs.
</Callout>

Se `description` ficar vazia, o processador usa o valor de `name` como descrição. O `name()` é obrigatório (a anotação não declara default), pelo que um nome em branco falha em tempo de compilação.

Se o ficheiro não aparecer em `target/generated-sources/annotations/` depois de um *clean compile*, a causa é quase sempre o caminho do processador de anotações da [§2.4](#processador-anotacoes).

---

## 4. Sincronização com o Access Management no arranque [#4-sincronização-com-o-access-management-no-arranque]

O `core-spring-boot` inclui um `AuthorizationSyncRunner` autoconfigurado que dispara em `@PostConstruct` a cada arranque da aplicação. Ele:

1. Autentica-se no Access Management com **OAuth2 `client_credentials`** em `POST /oauth2/token` (com as credenciais da [§5](#configuracao)).
2. Lê todas as constantes do `PermissionsRegistry.Permission` gerado.
3. Faz `POST /api/m2m/sync/resources` para registar o teu serviço como `Resource` (`type=API`, `name=${spring.application.name}`). O cliente `M2MApi` envia `spring.application.name` junto com o recurso, para que o Access Management possa ligar o registo à aplicação chamadora.
4. Faz `POST /api/m2m/sync/permissions` com a lista completa de `PermissionDTO` — `name=code`, `description`, `status=ACTIVE|INACTIVE`.

A sincronização é **idempotente e auto-corretiva**: permissões definidas localmente e ausentes no Access Management são inseridas; permissões apagadas localmente e ainda presentes no Access Management **não** são removidas automaticamente (os administradores podem já as ter atribuído a perfis — a remoção é uma ação administrativa explícita). Texto novo em `description` sobrepõe o valor guardado.

<Callout type="warning" title="Uma falha de sincronização não impede o arranque">
O runner captura o erro e registra em ERROR. É deliberado, para que uma indisponibilidade transitória do Access Management não derrube todos os serviços consumidores. O preço: um cliente M2M mal configurado produz linhas `[Permission Sync] Failed to synchronize…` repetidas até alguém notar. **Liga esse log a alerting.**
</Callout>

<Callout type="note" title="Não há propriedade para desligar o runner">
O runner é um `@Component` simples em `@PostConstruct` — não existe propriedade para o desativar. Se precisares de o saltar em testes, exclui `AuthorizationSyncRunner` via `@SpringBootApplication(exclude = …)` ou usa um *test slice* que não faça scan de `cv.igrp.framework.auth.core.autoconfig`.
</Callout>

### 4.1 Catálogo completo de endpoints M2M [#41-catálogo-completo-de-endpoints-m2m]

Além do trio que o runner usa, o Access Management expõe endpoints M2M adicionais que os projetos alvo podem chamar via `M2MApi`:

| Método + caminho | Propósito |
| --- | --- |
| `POST /api/m2m/sync/permissions` | Sincronizar o catálogo de permissões (usado pelo `AuthorizationSyncRunner`). |
| `POST /api/m2m/sync/resources` | Registar/atualizar esta API como `Resource` (usado pelo runner). |
| `POST /api/m2m/sync/applications` | Registar/atualizar o chamador como `Application`. |
| `POST /api/m2m/sync/applications/{code}/menus?syncRoles=true` | Enviar a árvore de menus do frontend (usado pelo lado Next.js). |
| `POST /api/m2m/users` | Consultar utilizadores de negócio IGRP (filtros no corpo). |
| `POST /api/m2m/departments` | Consultar departamentos de negócio IGRP. |
| `POST /api/m2m/roles` | Consultar perfis de negócio IGRP. |

Todas as chamadas exigem um bearer token obtido por `client_credentials` em `/oauth2/token`, com um scope permitido pelo registo do cliente M2M.

---

<div id="section-5"></div>



---

<Callout type="info" title="Documentação complementar">
- [Configuração de Segurança & Env Vars](./permissoes-seguranca) — `SecurityConfig`, `IAMUserProfileSyncFilter` e variáveis de ambiente
- [Proteger Endpoints, Checklist & Troubleshooting](./permissoes-referencia) — `@PreAuthorize`, checklist ponta-a-ponta e resolução de problemas
</Callout>


# Projeto Spring Boot

URL: /pt/docs/developer/framework/backend/springboot

O que vem num projeto backend gerado pelo Studio — stack, estrutura de pastas, o que é gerado e o que é teu, configuração por variáveis de ambiente e como o correr.

Um projeto backend criado no Studio é uma API **Spring Boot 3.5** já ligada à plataforma: autenticação JWT, permissões do Access Management, PostgreSQL, OpenAPI e descoberta de serviços. Esta página descreve o projeto; para o criar, ver [Primeiro projeto](/docs/developer/workspace/primeiro-projeto), e para o modelar, [Studio · Backend](/docs/developer/studio/backend/model-designer).

## Stack [#stack]

| | |
|---|---|
| **Spring Boot 3.5** + **Java 26** | Web, Data JPA, Data REST, Validation, GraphQL |
| **Framework iGRP** | `cv.igrp.framework:core` e os módulos de autenticação (`@IgrpController`, `@igrpAuthorization`) — ver [Backend](/docs/developer/framework/backend) |
| **Segurança** | Spring Security — OAuth2 resource server (JWT do Keycloak) e client M2M para o Access Management |
| **Dados** | PostgreSQL (H2 nos testes), Hibernate |
| **Plataforma** | Eureka / Kubernetes discovery, Caffeine, springdoc-openapi (Swagger UI) |
| **Build** | Maven, Lombok |

<Callout type="warning" title="O projeto compila com Java 26">
  O `pom.xml` gerado fixa `java.version` em **26** — o artefacto de autenticação do iGRP é bytecode Java 26. Para compilar fora do Docker precisas do JDK 26. A imagem Docker do projeto já usa um JRE recente.
</Callout>

## Estrutura [#estrutura]

O exemplo é o projeto `servico-demo`, com o estilo **Technical** (o predefinido no assistente) e o módulo `pedidos`:

<Files>
  <Folder name="<projeto>" defaultOpen>
    <Folder name=".igrpstudio" defaultOpen>
      <Folder name="pedidos" />
      <Folder name="shared" />
      <File name="baseApi.json" />
    </Folder>
    <Folder name="src/main/java/cv/igrp/servico_demo" defaultOpen>
      <Folder name="config" />
      <Folder name="controllers" defaultOpen>
        <Folder name="pedidos" />
      </Folder>
      <Folder name="dto" />
      <Folder name="exceptions" />
      <Folder name="models" defaultOpen>
        <Folder name="pedido" />
        <Folder name="tipopedido" />
        <Folder name="iamuserprofileentity" />
      </Folder>
      <Folder name="security" />
      <Folder name="services" />
      <File name="ServicodemoApplication.java" />
    </Folder>
    <Folder name="src/main/resources" defaultOpen>
      <File name="application.properties" />
      <File name="application-development.properties" />
      <File name="application-staging.properties" />
      <File name="application-production.properties" />
    </Folder>
    <Folder name="src/test" />
    <Folder name="db" />
    <Folder name="k8s" />
    <File name="pom.xml" />
    <File name="Dockerfile" />
    <File name="docker-compose.yml" />
    <File name="igrp-compose-<projeto>.yaml" />
    <File name=".env.example" />
  </Folder>
</Files>

### O que é do Studio e o que é teu [#o-que-é-do-studio-e-o-que-é-teu]

| Pasta / ficheiro | Quem escreve | Para quê |
|---|---|---|
| `.igrpstudio/` | Studio | Módulos, esquemas, contracts e endpoints em JSON — a fonte do designer de APIs |
| `models/<entidade>/` | Studio | Entidade JPA e repositório Spring Data, por esquema. Com **Gerar APIs CRUD**, o repositório é exposto em REST (`@RepositoryRestResource`) |
| `dto/` | Studio | Os contracts (`PedidoRequestDTO`), com as validações |
| `controllers/<endpoint>/` | Studio | O controlador (`@IgrpController`, `@PreAuthorize` com as permissões do separador Security) e a interface do serviço (`IPedidosService`) |
| `services/` | **Tu** | A implementação do serviço (`PedidosService`) — é aqui que escreves a lógica de cada ação |
| `config/`, `security/`, `exceptions/` | Template | Auditoria, Swagger, segurança, sincronização do perfil do utilizador, tratamento de erros |
| `models/iamuserprofileentity/` | Template | Perfil do utilizador sincronizado a partir do token |
| `src/test/` | Template | Testes de configuração, controladores, repositórios e segurança |

<Callout type="warning" title="O Studio regenera o que gera">
  Entidades, repositórios, DTOs e controladores são reescritos quando gravas no Studio — altera-os no designer de APIs, não no código. A lógica de negócio vai na implementação do serviço.
</Callout>

<Callout type="info" title="Estilo Domain-Driven Design">
  No assistente, **Structure Style → Domain-Driven Design** gera uma organização por domínio em vez de por camada técnica. Esta página descreve o estilo **Technical**.
</Callout>

## Configuração [#configuração]

O `application.properties` lê tudo de variáveis de ambiente, documentadas no `.env.example`. Os perfis `development`, `staging` e `production` (`SPRING_PROFILES_ACTIVE`) acrescentam o que muda entre ambientes. As principais:

| Variável | Por defeito | |
|---|---|---|
| `SERVICE_PORT` | `8080` | Porta da API |
| `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DATABASE`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | | Base de dados |
| `HIBERNATE_DDL` | `update` | Gestão do esquema da base de dados |
| `SECURITY_ENABLED` | `false` | Ativa a validação do JWT e as permissões |
| `AUTH_JWT_ISSUER` | | Issuer do Keycloak (realm) |
| `IGRP_ACCESS_API_BASE_URL` | | API do Access Management |
| `EUREKA_CLIENT_ENABLED` | `false` | Registo no Eureka |
| `ENABLE_SWAGGER` | `true` | Swagger UI |

<Callout type="info" title="No workspace, o Studio trata disto">
  Num projeto criado dentro do workspace, o Studio regista o cliente OAuth, cria a base de dados (`db/init-database.sh`) e injeta estas variáveis ao fazer **Deploy Docker** — ver [Projetos](/docs/developer/workspace/projetos).
</Callout>

Segurança, filtro JWT e permissões estão em [Permissões no backend](/docs/developer/framework/backend/permissoes) e [Segurança & configuração](/docs/developer/framework/backend/permissoes-seguranca).

## Correr [#correr]

<Tabs>
  <TabItem value="studio" label="No workspace">
    No cartão do projeto, **⋮ → Deploy Docker**: a API fica em `http://<slug>:<porta>/gateway-api/<projeto>`, ligada à plataforma local.
  </TabItem>
  <TabItem value="compose" label="Docker sozinho">
    O `docker-compose.yml` do projeto arranca a API com um PostgreSQL próprio, sem a plataforma:

    ```bash
    cp .env.example .env
    docker compose up -d --build
    ```
  </TabItem>
  <TabItem value="maven" label="Maven">
    Com o JDK 26 e um PostgreSQL acessível (variáveis no ambiente):

    ```bash
    mvn spring-boot:run
    ```
  </TabItem>
</Tabs>

Com `ENABLE_SWAGGER=true`, a documentação OpenAPI fica em `/swagger-ui.html`. Para testar a API no Studio, ver [Run Preview](/docs/developer/studio/backend/run-preview).


# Conteúdo e media

URL: /pt/docs/developer/framework/design-system/conteudo

Botões, tipografia, ícones e imagens do Design System — props principais e exemplos em código, mais um resumo dos componentes de media.

Os componentes desta página mostram conteúdo: texto, títulos, ícones, imagens e as ações (botões). Todos são `IGRP*` (Horizon) e precisam de `'use client'`.

<Callout type="info" title="Secção em construção">
  Esta página cobre os componentes principais; o resto do catálogo será completado. Para configurar estes componentes no Studio (sem código), vê [Widgets](/docs/developer/studio/ide/components) do Page Builder.
</Callout>

## Ações [#ações]

### `IGRPButton` [#igrpbutton]

Botão com ícone opcional e estado de carregamento. Dentro de um `IGRPForm`, um botão com `type="submit"` submete o formulário. No Page Builder é o widget **Button**.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `variant` | `"default" \| "outline" \| "secondary" \| "ghost" \| "destructive" \| "link"` | `"default"` | Estilo |
| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"default"` | Tamanho; os `icon*` mostram só o ícone |
| `showIcon` | `boolean` | `false` | Mostra o ícone |
| `iconName` | `string` | `"ArrowLeft"` | Ícone Lucide |
| `iconPlacement` | `"start" \| "end"` | `"start"` | Lado do ícone |
| `loading` | `boolean` | `false` | Mostra um spinner e desativa o botão |
| `loadingText` | `string` | tradução do DS | Texto durante o carregamento |
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Tipo HTML |

```tsx
<IGRPButton showIcon iconName="Save" loading={aGravar} loadingText="A gravar…" type="submit">
  Gravar
</IGRPButton>

{/* só ícone — passa sempre aria-label */}
<IGRPButton size="icon-sm" variant="ghost" iconName="Pencil" aria-label="Editar" />
```

## Tipografia [#tipografia]

### `IGRPHeadline` [#igrpheadline]

Título (`h1` a `h6`) com descrição e ícone opcionais. No Page Builder é o widget **Headline**.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `title` | `string` | — | Texto do título (obrigatório) |
| `variant` | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h3"` | Nível e tamanho |
| `description` | `string` | — | Texto por baixo do título |
| `showIcon` / `iconName` | `boolean` / `string` | `false` / `"Info"` | Ícone ao lado do título |
| `iconPlacement` | `"start" \| "end"` | `"start"` | Lado do ícone |

```tsx
<IGRPHeadline variant="h2" title="Pedidos" description="Pedidos submetidos nos últimos 30 dias." />
```

### `IGRPText` [#igrptext]

Bloco de texto com tamanho, peso, alinhamento, truncagem e realce de termos. No Page Builder é o widget **Text**.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `size` | `"sm" \| "default" \| "lg" \| "xl"` | `"default"` | Tamanho |
| `weight` | `"light" \| "normal" \| "medium" \| "semibold" \| "bold"` | `"normal"` | Peso |
| `align` | `"left" \| "center" \| "right" \| "justify"` | `"left"` | Alinhamento |
| `spacing` | `"none" \| "tight" \| "normal" \| "loose"` | `"tight"` | Margem inferior |
| `truncate` / `maxLines` | `boolean` / `number` | `false` / — | Corta com reticências (numa ou em várias linhas) |
| `highlight` | `string[]` | `[]` | Termos a realçar |
| `as` | `"p" \| "span" \| "div"` | `"div"` | Elemento HTML |

```tsx
<IGRPText size="sm" truncate maxLines={2} highlight={[termo]}>
  {descricao}
</IGRPText>
```

## Ícones [#ícones]

### `IGRPIcon` [#igrpicon]

Mostra um ícone [Lucide](https://lucide.dev/icons) pelo nome. Aceita PascalCase (`"LayoutDashboard"`) ou kebab-case (`"layout-dashboard"`), por isso funciona com nomes que chegam como dados (por exemplo, menus vindos do Access Management). Cada ícone é carregado à parte, não o catálogo inteiro; um nome desconhecido mostra um `AlertCircle` vermelho.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

A mesma convenção repete-se nos componentes Horizon: `showIcon` liga o ícone, `iconName` escolhe-o, `iconPlacement` diz o lado e `iconClassName` estiliza-o.

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `iconName` | `IGRPIconName \| string` | — | Nome do ícone Lucide (obrigatório) |
| `size` | `number \| string` | `16` | Tamanho em px |
| `color` | `string` | `"currentColor"` | Cor do traço |
| `strokeWidth` | `number` | `2` | Espessura do traço |

```tsx
<IGRPIcon iconName="FileText" size={20} />
```

## Imagens [#imagens]

### `IGRPImage` [#igrpimage]

Imagem sobre o `next/image`. Sem `width` e `height`, preenche o contentor numa proporção fixa (`ratio`); com eles, usa o tamanho dado. No Page Builder é o widget **Image**.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `src` / `alt` | `string` | — | Imagem e texto alternativo (do `next/image`) |
| `ratio` | `"1/1" \| "4/3" \| "16/9" \| "21/9"` | `"16/9"` | Proporção quando não há `width`/`height` |
| `rounded` | variante de arredondamento | `"md"` | Raio dos cantos |
| `width` / `height` | `number` | — | Tamanho fixo (desliga o modo `fill`) |

```tsx
<IGRPImage src="/images/capa.jpg" alt="Capa" ratio="21/9" />
```

<Callout type="warning" title="Imagens remotas">
  Como usa o `next/image`, um `src` de outro domínio tem de estar em `images.remotePatterns` no `next.config`.
</Callout>

## Outros componentes [#outros-componentes]

| Componente | Para quê | Widget no Studio |
|---|---|---|
| `IGRPLink` | Link (Next.js ou externo, abre em nova aba) com ícone, `size` e `underline` | — |
| `IGRPTextList` / `IGRPTextListItem` | Listas (`unordered`, `ordered`, `checklist`, `steps`, `features`) com ícones, badges e sub-itens | Text List |
| `IGRPImageCropper` | Recortar uma imagem (`basic`, `circular`, `zoom`, `preview`); devolve um `Blob` em `onCrop` | Image Cropper |
| `IGRPPdfViewer` | Ver PDFs em modal ou inline, a partir de uma lista de documentos | PDF Viewer |
| `IGRPVideoEmbed` | Incorporar vídeo (YouTube, Vimeo…) com proporção e opções de reprodução | Video Embed |
| `IGRPChat` | Interface de chat que envia as mensagens para um `apiEndpoint` | Chat |
| `IGRPCopyTo` | Botão de copiar para a área de transferência, com tooltip e toast | Copy To |
| `IGRPStepperProcess` | Passos de um processo em linha horizontal, com o conteúdo do passo atual | — |


# Dados

URL: /pt/docs/developer/framework/design-system/dados

Componentes do Design System para mostrar dados — IGRPDataTable com células, filtros e ações, gráficos, cards e indicadores.

Componentes para listar, resumir e visualizar dados. Esta página é a vista de código: imports, props e exemplos.

<Callout type="info" title="Secção em construção">
  Esta página cobre os componentes principais; o resto do catálogo será completado. Para configurar estes componentes no Studio (sem código), vê [Widgets](/docs/developer/studio/ide/components) do Page Builder.
</Callout>

## Componentes principais [#componentes-principais]

### `IGRPDataTable` [#igrpdatatable]

Tabela de dados com ordenação, filtros, paginação, linhas expansíveis e ações por linha, construída sobre o TanStack Table.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `columns` | `ColumnDef<TData, TValue>[]` | — | Definição das colunas (TanStack Table). Tem de ser estável: define fora do componente ou com `useMemo`. |
| `data` | `TData[]` | — | Linhas. Também tem de ser estável entre renders. |
| `showFilter` / `clientFilters` | `boolean` / `IGRPDataTableClientFilterListProps<TData>[]` | `false` / — | Mostra a barra de filtros e define um componente de filtro por coluna. |
| `showPagination` / `isNumericPagination` | `boolean` | `false` | Paginação simples (anterior/seguinte) ou numérica. |
| `pageSizePagination` | `number[]` | `[50, 100, 150, 200]` | Tamanhos de página disponíveis; o primeiro é o inicial. |
| `actions` | `IGRPDataTableAction<TData>[]` | — | Ações por linha (`link`, `alert`, `modal`, `custom`). Até 2 aparecem como botões; mais de 2 vão para um menu. |
| `getRowCanExpand` / `renderSubComponent` | `(row) => boolean` / `(row) => ReactElement` | `() => false` | Linhas expansíveis e o conteúdo mostrado ao expandir. |
| `onQueryChange` / `rowCount` | `(query: IGRPDataTableQuery) => void` / `number` | — | Modo servidor: recebe página, tamanho, ordenação e filtros sempre que mudam. |

```tsx
'use client';

import { useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import {
  IGRPDataTable,
  IGRPDataTableHeaderSortToggle,
  IGRPDataTableCellBadge,
  IGRPDataTableCellAmount,
  IGRPDataTableCellDate,
  IGRPDataTableFilterInput,
  IGRPDataTableFilterFaceted,
  IGRPDataTableFacetedFilterFn,
  type IGRPDataTableAction,
} from '@igrp/igrp-framework-react-design-system';

type Pedido = { id: string; cliente: string; estado: string; valor: number; data: string };

export function PedidosTable({ pedidos }: { pedidos: Pedido[] }) {
  const columns = useMemo<ColumnDef<Pedido>[]>(() => [
    {
      accessorKey: 'cliente',
      header: ({ column }) => <IGRPDataTableHeaderSortToggle column={column} title="Cliente" />,
    },
    {
      accessorKey: 'estado',
      header: 'Estado',
      filterFn: IGRPDataTableFacetedFilterFn,
      cell: ({ row }) => (
        <IGRPDataTableCellBadge
          label={row.getValue('estado')}
          color={row.getValue('estado') === 'Pago' ? 'success' : 'warning'}
          variant="soft"
        />
      ),
    },
    {
      accessorKey: 'valor',
      header: 'Valor',
      cell: ({ row }) => <IGRPDataTableCellAmount field={String(row.getValue('valor'))} currency="EUR" />,
    },
    {
      accessorKey: 'data',
      header: 'Data',
      cell: ({ row }) => <IGRPDataTableCellDate date={row.getValue('data')} />,
    },
  ], []);

  const actions: IGRPDataTableAction<Pedido>[] = [
    { type: 'link', label: 'Ver', icon: 'Eye', href: (row) => `/pedidos/${row.original.id}` },
    {
      type: 'alert',
      label: 'Anular',
      icon: 'Trash2',
      title: 'Anular pedido?',
      description: 'Esta ação não pode ser desfeita.',
      onConfirm: async (row) => { /* chamar a API */ },
    },
  ];

  return (
    <IGRPDataTable
      columns={columns}
      data={pedidos}
      actions={actions}
      showFilter
      clientFilters={[
        { columnId: 'cliente', component: ({ column }) => <IGRPDataTableFilterInput column={column} placeholder="Procurar cliente" /> },
        {
          columnId: 'estado',
          component: ({ column }) => (
            <IGRPDataTableFilterFaceted
              column={column}
              placeholder="Estado"
              options={[{ label: 'Pago', value: 'Pago' }, { label: 'Pendente', value: 'Pendente' }]}
            />
          ),
        },
      ]}
      showPagination
    />
  );
}
```

Os componentes auxiliares abaixo são usados dentro de `columns` (cabeçalhos e células), de `clientFilters` (filtros) ou numa coluna de ações. O widget Tabela do Studio gera-os a partir da configuração das colunas — ver [Widget Tabela](/docs/developer/studio/ide/components#widget-tabela) e [Coluna de Ações](/docs/developer/studio/ide/components#coluna-de-ações).

**Cabeçalhos**

| Componente | Para quê | Props principais |
|---|---|---|
| `IGRPDataTableHeaderSortToggle` | Título clicável que alterna a ordenação asc/desc | `column`, `title` |
| `IGRPDataTableHeaderSortDropdown` | Título com menu Asc/Desc | `column`, `title` |
| `IGRPDataTableHeaderRowsSelect` | Checkbox "selecionar todas" da página | `table` |
| `IGRPDataTableHeaderDefault` | Título simples, sem ordenação | `title` |

**Células**

| Componente | Para quê | Props principais |
|---|---|---|
| `IGRPDataTableCellBadge` | Estado como badge | `label`, `color`, `variant`, `size` |
| `IGRPDataTableCellAmount` | Número/moeda formatado com `Intl.NumberFormat` | `field` (texto numérico), `currency` (`"USD"`), `formatStyle` (`"currency"`), `language` |
| `IGRPDataTableCellDate` | Data formatada com `Intl.DateTimeFormat` | `date`, `dateOptions`, `language` (`dateFormat` está descontinuado) |
| `IGRPDataTableCellLink` | Ligação | `href`, `target`, `showIcon`, `iconName` |
| `IGRPDataTableCellTooltip` | Texto truncado com tooltip | `text`, `side`, `align` |
| `IGRPDataTableCellCheckbox` / `IGRPDataTableCellSwitch` | Seleção da linha | `row` |
| `IGRPDataTableCellExpander` | Botão para expandir a linha (com `renderSubComponent`) | `row`, `label` |

**Filtros**

| Componente | Para quê | Props principais |
|---|---|---|
| `IGRPDataTableFilterInput` | Texto livre | `column`, `placeholder`, `iconName` |
| `IGRPDataTableFilterSelect` | Seleção única | `column`, `options`, `placeholder` |
| `IGRPDataTableFilterDropdown` | Seleção única com pesquisa | `column`, `options`, `showFilter`, `notFoundText` |
| `IGRPDataTableFilterFaceted` | Seleção múltipla com contagens | `column`, `options`, `labelFilter`, `labelSearchField` |
| `IGRPDataTableFilterMinMax` | Intervalo numérico | `column`, `placeholder`, `placeholderMax` |
| `IGRPDataTableFilterDate` | Intervalo de datas | `column`, `placeholder`, `clearDates` |
| `IGRPDataTableFacetedFilterFn` | `filterFn` da coluna para o filtro facetado (o valor está numa lista) | — |
| `IGRPDataTableDateRangeFilterFn` | `filterFn` da coluna para o filtro de datas (`from`/`to`) | — |

**Ações por linha**

A forma recomendada é a prop `actions` (exemplo acima). Para montar a coluna de ações à mão:

| Componente | Para quê | Props principais |
|---|---|---|
| `IGRPDataTableRowAction` | Contentor dos botões de ação (com tooltips) | `children` |
| `IGRPDataTableButtonLink` | Botão-ícone que navega ou executa uma ação | `labelTrigger`, `icon`, `href`, `action` |
| `IGRPDataTableButtonAlert` | Botão-ícone que pede confirmação | `labelTrigger`, `icon`, `modalTitle`, `onClickConfirm`, `children` |
| `IGRPDataTableButtonModal` | Botão-ícone que abre um modal | `labelTrigger`, `icon`, `modalTitle`, `render` |
| `IGRPDataTableDropdownMenu` | Menu "⋯" com várias ações | `items` (`{ component, props }`), `iconName` |
| `IGRPDataTableDropdownMenuLink` / `…Alert` / `…Custom` | Itens do menu | `labelTrigger`, `icon`, `href` ou `modalTitle` |

### `IGRPVerticalBarChart` [#igrpverticalbarchart]

Gráfico de barras verticais. Todos os gráficos partilham as mesmas props base (`IGRPChartProps`) e diferem na configuração das séries.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `data` | `IGRPChartDataItem[]` | — | Linhas (`Record<string, string \| number>`). |
| `categoryKey` | `string` | — | Campo usado no eixo das categorias. |
| `bars` | `IGRPBarConfig[]` | — | Uma série por barra: `dataKey`, `name`, `color`, `stackId`, `radius`. |
| `title` / `description` | `string` | — | Cabeçalho do card do gráfico. |
| `size` / `height` | `"sm" \| "md" \| "lg" \| "xl" \| "auto"` / `number \| string` | `"md"` / — | Tamanho do gráfico. |
| `legendPosition` | `"top" \| "right" \| "bottom" \| "left" \| "none"` | `"none"` | Posição da legenda. |
| `showGrid` / `stacked` | `boolean` | `false` | Grelha de fundo; barras empilhadas. |
| `valueFormatter` | `(value: number) => string` | — | Formata os valores no eixo e no tooltip. |

```tsx
<IGRPVerticalBarChart
  title="Pedidos por mês"
  data={[
    { mes: 'Jan', pagos: 120, pendentes: 30 },
    { mes: 'Fev', pagos: 98, pendentes: 41 },
  ]}
  categoryKey="mes"
  bars={[
    { dataKey: 'pagos', name: 'Pagos' },
    { dataKey: 'pendentes', name: 'Pendentes' },
  ]}
  legendPosition="bottom"
  showGrid
/>
```

As outras variantes usam as mesmas props base:

| Componente | Configuração das séries | Props próprias |
|---|---|---|
| `IGRPHorizontalBarChart` | `bars: IGRPBarConfig[]` | `barRadius`, `barGap` |
| `IGRPAreaChart` | `areas: IGRPAreaConfig[]` (`type`, `fillOpacity`, `gradient`) | `expanded` |
| `IGRPLineChart` | `lines: LineConfig[]` | — |
| `IGRPPieChart` | `pies: PieConfig[]` (`innerRadius`, `outerRadius`, `showLabels`, `labelType`…) | `nameKey`, `centerLabel` |
| `IGRPRadarChart` | `radars: IGRPRadarConfig[]` (`fill`, `fillOpacity`, `dot`) | `angleAxisKey`, `polarGridType` |
| `IGRPRadialBarChart` | `bars: RadialBarConfig[]` (`cornerRadius`, `background`, `showLabels`) | `nameKey`, `startAngle` |

As cores das séries vêm dos tokens `--chart-1` … `--chart-8` — ver [Instalação e tema](./instalacao.mdx#tokens).

### `IGRPCard` [#igrpcard]

Superfície de conteúdo com cabeçalho, corpo e rodapé.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `name` / `id` | `string` | id gerado | Identificador do card no DOM. |
| `className` | `string` | — | Classes adicionais. |
| `children` | `ReactNode` | — | Use `IGRPCardHeader`, `IGRPCardTitle`, `IGRPCardDescription`, `IGRPCardAction`, `IGRPCardContent`, `IGRPCardFooter`. |

```tsx
<IGRPCard>
  <IGRPCardHeader>
    <IGRPCardTitle>Resumo</IGRPCardTitle>
    <IGRPCardDescription>Últimos 30 dias</IGRPCardDescription>
  </IGRPCardHeader>
  <IGRPCardContent>…</IGRPCardContent>
  <IGRPCardFooter>
    <IGRPButton variant="outline">Ver tudo</IGRPButton>
  </IGRPCardFooter>
</IGRPCard>
```

### `IGRPStatsCard` [#igrpstatscard]

Card de indicador: um título, um valor e um ícone ou imagem.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `title` | `string` | — | Nome do indicador. |
| `value` | `string \| number` | — | Valor mostrado. |
| `iconName` / `showIcon` | `IGRPIconName \| string` / `boolean` | `"Box"` / `true` | Ícone Lucide. |
| `iconVariant` / `showIconBackground` | `IGRPColorVariants` / `boolean` | `"primary"` / `false` | Cor do ícone e fundo colorido. |
| `cardVariant` / `cardBorderPosition` | `IGRPColorVariants` / `"none" \| "top" \| "bottom" \| "left" \| "right"` | `"primary"` / `"none"` | Cor e posição da borda de destaque. |
| `valueSize` | `"sm" \| "base" \| "lg" \| "xl" \| "2xl"…` | `"2xl"` | Tamanho do valor. |
| `onClick` | `() => void` | — | Torna o card clicável. |

```tsx
<IGRPStatsCard
  title="Pedidos pendentes"
  value={42}
  iconName="Clock"
  iconVariant="warning"
  showIconBackground
  iconBackground="rounded"
  cardVariant="warning"
  cardBorderPosition="left"
/>
```

### `IGRPInfoCard` [#igrpinfocard]

Card de leitura com pares rótulo/valor organizados em secções — típico de páginas de detalhe.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `title` | `string` | — | Título do card. |
| `sections` | `IGRPInfoSection[]` | — | Secções; cada uma com `items: IGRPInfoItem[]` (`label`, `text`, `icon`, `showIcon`, `colorItem`). |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` | Rótulo por cima ou ao lado do valor. |
| `columns` | `1 \| 2 \| 3` | `1` | Colunas da grelha (adaptam-se à largura do card). |
| `variantSection` / `colorSection` | `IGRPColorRole` / `IGRPColorVariants` | — | Cor das secções. |

```tsx
<IGRPInfoCard
  title="Dados do pedido"
  columns={2}
  sections={[
    {
      items: [
        { label: 'Cliente', text: 'Ana Silva', icon: 'User', showIcon: true },
        { label: 'Estado', text: 'Pendente' },
        { label: 'Data', text: '12/03/2026' },
      ],
    },
  ]}
/>
```

### `IGRPBadge` [#igrpbadge]

Etiqueta para estados, categorias e contagens.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `variant` | `"solid" \| "soft" \| "outline"` | `"solid"` | Estilo. |
| `color` | `"primary" \| "secondary" \| "success" \| "destructive" \| "warning" \| "info" \| "indigo"` | `"primary"` | Cor semântica. |
| `size` | `"sm" \| "md" \| "lg"` | `"sm"` | Tamanho. |
| `dot` | `boolean` | `false` | Ponto colorido antes do texto. |
| `showIcon` / `iconName` | `boolean` / `string` | `false` / `"Info"` | Ícone Lucide. |

```tsx
<IGRPBadge color="success" variant="soft" dot>Ativo</IGRPBadge>
```

## Outros componentes [#outros-componentes]

| Componente | Para quê | Widget no Studio |
|---|---|---|
| `IGRPCardDetails` | Card com uma lista de pares `label`/`content`, com botão de copiar opcional (`showCopyTo`) | Card Details |
| `IGRPAvatar` | Imagem de perfil com fallback, estado e badge (`src`, `fallback`, `size`, `hasStatus`, `status`) | Avatar |
| `IGRPStatusBanner` (Custom) | Faixa de estado no topo de uma página ou secção | Status Banner |
| `IGRPStatsCardMini` (Custom) | Indicador compacto | — |
| `IGRPStatsCardTopBorderColored` (Custom) | Indicador com borda superior colorida | — |
| `IGRPUserAvatar` (Custom) | Avatar com nome do utilizador | — |


# Feedback e diálogos

URL: /pt/docs/developer/framework/design-system/feedback

Toasts, alertas, banners de estado, diálogos modais e de confirmação do Design System — props principais e exemplos em código.

Os componentes desta página dão resposta ao utilizador: confirmam uma ação, avisam de um erro ou pedem uma decisão. Todos são `IGRP*` (Horizon) e precisam de `'use client'`.

<Callout type="info" title="Secção em construção">
  Esta página cobre os componentes principais; o resto do catálogo será completado. Para configurar estes componentes no Studio (sem código), vê [Widgets](/docs/developer/studio/ide/components) do Page Builder.
</Callout>

## Toasts [#toasts]

### `useIGRPToast` [#useigrptoast]

Hook que mostra notificações temporárias (toasts, sobre o `sonner`). Precisa de um `IGRPToaster` montado uma vez no layout da aplicação.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

O código gerado declara `const { igrpToast } = useIGRPToast()` e usa-o nas ações (por exemplo, depois de submeter um formulário). O hook devolve `igrpToast(props)` e os atalhos `success`, `error`, `info`, `warning`, `loading` e `dismiss`.

| Prop de `igrpToast` | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `type` | `"default" \| "success" \| "error" \| "info" \| "warning" \| "loading"` | `"default"` | Tipo de toast |
| `title` | `ReactNode` | `"Notification"` | Título |
| `description` | `ReactNode` | — | Texto por baixo do título |
| `duration` | `number` | `4000` (success), `6000` (error) | Tempo visível, em ms |
| `promise` | `Promise<T>` | — | Acompanha uma promessa (loading → success/error) |
| `loading` / `success` / `error` | `ReactNode` ou função | — | Mensagens dos três estados de `promise` |

```tsx
// layout — uma vez
<IGRPToaster richColors position="top-right" />

// componente
const { igrpToast, success } = useIGRPToast();

igrpToast({ type: 'error', title: 'Erro ao gravar', description: 'Tenta de novo.' });
success('Registo gravado');

igrpToast({
  promise: guardar(dados),
  loading: 'A gravar…',
  success: 'Gravado',
  error: (e) => (e instanceof Error ? e.message : 'Falhou'),
});
```

## Alertas [#alertas]

### `IGRPAlert` [#igrpalert]

Caixa de mensagem inline (informação, aviso, erro) com ícone e link opcional. Com `color="destructive"` é anunciada como `role="alert"`; nos restantes casos como `role="status"`.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `variant` | `"solid" \| "soft" \| "outline"` | `"solid"` | Estilo |
| `color` | `"primary" \| "success" \| "destructive" \| "warning" \| "info" \| "secondary" \| "indigo"` | `"primary"` | Cor (escolhe também o ícone por defeito) |
| `showIcon` | `boolean` | `true` | Mostra o ícone |
| `iconName` | `string` | ícone da `color` | Ícone Lucide |
| `showLink` | `boolean` | — | Mostra um link à direita |
| `label` / `linkUrl` | `string` | `"link"` / — | Texto e URL do link |
| `children` | `ReactNode` | — | Conteúdo |

```tsx
<IGRPAlert variant="soft" color="warning">
  O prazo de submissão termina amanhã.
</IGRPAlert>
```

### `IGRPStatusBanner` [#igrpstatusbanner]

Faixa de estado (Custom) que junta um `IGRPAlert` com um `IGRPBadge` — por exemplo, o estado de um pedido. No Page Builder é o widget **Status Banner**.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `text` | `string` | `"Insert our Text here"` | Texto principal |
| `variant` / `color` | `IGRPColorRole` / `IGRPColorVariants` | `"soft"` / `"success"` | Estilo e cor da faixa |
| `badgeText` | `string` | `"Badge Text"` | Texto do badge |
| `badgeVariant` / `badgeColor` | `IGRPColorRole` / `IGRPColorVariants` | `"outline"` / `"primary"` | Estilo e cor do badge |

```tsx
<IGRPStatusBanner text="Pedido em análise" color="info" badgeText="Pendente" badgeColor="warning" />
```

## Diálogos [#diálogos]

### `IGRPModalDialog` [#igrpmodaldialog]

Janela modal para conteúdo ou formulários, composta por partes: `IGRPModalDialog` (raiz, `open`/`onOpenChange`), `IGRPModalDialogTrigger`, `IGRPModalDialogContent`, `IGRPModalDialogHeader`, `IGRPModalDialogTitle`, `IGRPModalDialogDescription`, `IGRPModalDialogFooter` e `IGRPModalDialogClose`. No Studio é o [Widget Modal](/docs/developer/studio/ide/components#widget-modal).

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Em | Tipo | Por defeito | Descrição |
|---|---|---|---|---|
| `open` / `onOpenChange` | raiz | `boolean` / `(open) => void` | — | Controlo do estado (opcional) |
| `size` | `Content` | `"sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Largura máxima |
| `showCloseButton` | `Content` | `boolean` | `true` | Botão de fechar no canto |
| `stickyHeader` | `Header` | `boolean` | — | Cabeçalho fixo ao fazer scroll |
| `stickyFooter` | `Footer` | `boolean` | — | Rodapé fixo ao fazer scroll |

```tsx
<IGRPModalDialog>
  <IGRPModalDialogTrigger asChild>
    <IGRPButton>Ver detalhe</IGRPButton>
  </IGRPModalDialogTrigger>
  <IGRPModalDialogContent size="lg">
    <IGRPModalDialogHeader>
      <IGRPModalDialogTitle>Detalhe do registo</IGRPModalDialogTitle>
      <IGRPModalDialogDescription>Dados do registo selecionado.</IGRPModalDialogDescription>
    </IGRPModalDialogHeader>
    {/* conteúdo */}
    <IGRPModalDialogFooter>
      <IGRPModalDialogClose asChild>
        <IGRPButton variant="outline">Fechar</IGRPButton>
      </IGRPModalDialogClose>
    </IGRPModalDialogFooter>
  </IGRPModalDialogContent>
</IGRPModalDialog>
```

### `IGRPAlertDialog` [#igrpalertdialog]

Diálogo de confirmação (por exemplo, antes de apagar), com ícone, título, descrição e botões de ação e cancelar. Não tem trigger: abre e fecha com `open`/`onOpenChange`.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `open` / `onOpenChange` | `boolean` / `(open) => void` | — | Estado do diálogo |
| `variant` | `IGRPColorVariants` | `"primary"` | Cor do ícone e do botão de ação |
| `title` / `description` | `string` | — | Texto do diálogo |
| `actionLabel` / `cancelLabel` | `string` | traduções do DS | Texto dos botões |
| `onAction` / `onCancel` | `() => void` | — | Chamadas antes de fechar |
| `showCancel` | `boolean` | `true` | Mostra o botão cancelar |
| `iconName` / `iconPlacement` | `string` / `"start" \| "center"` | `"AirVent"` / `"start"` | Ícone e posição — indica sempre o `iconName` |

```tsx
const [open, setOpen] = useState(false);

<IGRPAlertDialog
  open={open}
  onOpenChange={setOpen}
  variant="destructive"
  iconName="Trash2"
  title="Apagar registo?"
  description="Esta ação não pode ser desfeita."
  actionLabel="Apagar"
  onAction={apagar}
/>
```

## Outros componentes [#outros-componentes]

| Componente | Para quê | Widget no Studio |
|---|---|---|
| `IGRPBanner` | Faixa de cookies (`variant="cookie"`, fixa em baixo) ou de anúncio (`variant="announcement"`) | Banner |
| `IGRPNotification` | Notificação inline com ícone por tipo (`default`, `error`, `info`, `success`, `warning`) e link | Notification |
| `IGRPLoadingSpinner` | Spinner centrado para estados de carregamento, com `label` acessível | Loading Spinner |
| `Skeleton` (Primitive) | Marcador de conteúdo enquanto os dados carregam | Skeleton |
| `Progress` (Primitive) | Barra de progresso | Progress |
| `Empty` (Primitive) | Estado vazio (sem resultados, sem dados) | Empty |


# Formulários

URL: /pt/docs/developer/framework/design-system/formularios

IGRPForm com validação Zod e os campos do Design System — texto, seleção, datas, ficheiros e listas repetitivas — vistos pelo código e pelas props.

Um formulário do iGRP é sempre um `IGRPForm` com um esquema **Zod**, e cada campo é um componente `IGRP*` ligado ao formulário pelo `name`. É isto que o Studio gera quando arrastas o widget **Form** e os seus campos no Page Builder; esta página mostra o mesmo do lado do código.

<Callout type="info" title="Secção em construção">
  Esta página cobre os componentes principais; o resto do catálogo será completado. Para configurar estes componentes no Studio (sem código), vê [Widgets](/docs/developer/studio/ide/components) do Page Builder.
</Callout>

## Como funciona um formulário [#como-funciona-um-formulário]

O `IGRPForm` cria o formulário (react-hook-form + `zodResolver`) e partilha-o com os campos que tem dentro. Cada campo funciona num de dois modos:

| Modo | Quando | Como |
|---|---|---|
| **Ligado ao formulário** | Dentro de um `IGRPForm` | Lê e escreve o valor pelo `name`, que tem de existir no esquema; o erro de validação aparece no campo |
| **Controlado** | Fora de um `IGRPForm` | O valor é teu, por `value` / `onChange` (ou `onValueChange`, `onDateChange`…) |

```tsx
'use client';

import { useRef } from 'react';
import { z } from 'zod';
import {
  IGRPButton,
  IGRPForm,
  type IGRPFormHandle,
  IGRPInputText,
  IGRPSelect,
  IGRPDatePickerSingle,
} from '@igrp/igrp-framework-react-design-system';

const schema = z.object({
  nome: z.string().min(1, 'Obrigatório'),
  email: z.string().email('Email inválido'),
  categoria: z.string().min(1, 'Escolhe uma categoria'),
  dataInicio: z.date(),
});

export function RegistoForm() {
  const formRef = useRef<IGRPFormHandle<typeof schema>>(null);

  return (
    <IGRPForm
      schema={schema}
      formRef={formRef}
      defaultValues={{ nome: '', email: '', categoria: '' }}
      onSubmit={async (values) => {
        await guardar(values); // values já validados e tipados pelo esquema
      }}
      showToastOnError
    >
      <IGRPInputText name="nome" label="Nome" required />
      <IGRPInputText name="email" type="email" label="Email" />
      <IGRPSelect
        name="categoria"
        label="Categoria"
        options={[
          { label: 'Geral', value: 'geral' },
          { label: 'Urgente', value: 'urgente' },
        ]}
      />
      <IGRPDatePickerSingle name="dataInicio" label="Data de início" />
      <IGRPButton type="submit">Guardar</IGRPButton>
    </IGRPForm>
  );
}
```

O `formRef` dá acesso ao formulário a partir de fora — por exemplo, um botão no cabeçalho da página que submete: `formRef.current?.submit()`.

## Componentes principais [#componentes-principais]

### `IGRPForm` [#igrpform]

O contentor do formulário: valida com o esquema Zod, chama `onSubmit` só com valores válidos e mostra o erro global se a submissão falhar.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `schema` | `z.ZodType` | — | Esquema Zod de validação (obrigatório) |
| `onSubmit` | `(values) => void \| Promise<void>` | — | Chamado com os valores validados (obrigatório) |
| `formRef` | `RefObject<IGRPFormHandle \| null>` | — | Acesso a `submit()`, `reset()`, `setGlobalError()`, `isSubmitting` e à API do react-hook-form (obrigatório) |
| `defaultValues` | `z.input<schema>` | — | Valores iniciais |
| `validationMode` | `'onSubmit' \| 'onChange' \| 'onBlur' \| 'onTouched' \| 'all'` | `'onSubmit'` | Quando validar |
| `resetAfterSubmit` | `boolean` | `false` | Repõe os `defaultValues` depois de submeter com sucesso |
| `showToastOnError` | `boolean` | `false` | Mostra um toast quando o `onSubmit` lança erro |
| `resetKey` | `React.Key` | — | Muda-o (por exemplo, o id do registo) para recarregar o formulário com novos `defaultValues` |

Se o `onSubmit` lançar uma exceção, a mensagem aparece no topo do formulário; também a podes definir tu com `formRef.current?.setGlobalError('…')`. Para a configuração no Page Builder, ver [Widget Formulário](/docs/developer/studio/ide/components#widget-formulário).

### `IGRPInputText` [#igrpinputtext]

Campo de texto com label, texto de ajuda, ícone opcional e mensagem de erro.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `name` | `string` | — | Nome do campo no esquema |
| `label` | `string` | — | Label do campo |
| `type` | `'text' \| 'email' \| 'number'` | `'text'` | Tipo do input |
| `helperText` | `string` | — | Texto de ajuda por baixo do campo |
| `required` | `boolean` | — | Marca o campo como obrigatório |
| `showIcon` / `iconName` | `boolean` / `string` | — | Ícone Lucide dentro do campo |
| `iconPlacement` | `'start' \| 'end' \| 'center'` | — | Posição do ícone |
| `error` | `string` | — | Mensagem de erro que sobrepõe a do formulário |

```tsx
<IGRPInputText
  name="email"
  type="email"
  label="Email"
  helperText="Usado para notificações"
  showIcon
  iconName="Mail"
/>
```

### `IGRPSelect` [#igrpselect]

Lista de escolha única a partir de um array de opções, com pesquisa e agrupamento opcionais.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `name` | `string` | — | Nome do campo no esquema |
| `options` | `IGRPOptionsProps[]` | — | Opções (`label`, `value` e opcionalmente `group`, `icon`, `status`, `description`, `disabled`) |
| `label` | `string` | — | Label do campo |
| `placeholder` | `string` | — | Texto quando não há valor |
| `showSearch` | `boolean` | — | Campo de pesquisa dentro da lista |
| `showGroup` | `boolean` | — | Agrupa as opções pelo seu `group` |
| `value` / `onValueChange` | `string` / `(value: string) => void` | — | Modo controlado, fora de um `IGRPForm` |

```tsx
<IGRPSelect
  name="distrito"
  label="Distrito"
  placeholder="Escolhe…"
  showSearch
  options={[
    { label: 'Norte', value: 'norte', group: 'Continente' },
    { label: 'Sul', value: 'sul', group: 'Continente' },
  ]}
  showGroup
/>
```

O mesmo formato de opções (`IGRPOptionsProps`) serve `IGRPCombobox`, `IGRPMultiSelect`, `IGRPRadioGroup` e o seletor do `IGRPInputAddOn`.

### `IGRPDatePickerSingle` [#igrpdatepickersingle]

Seleção de uma data num calendário em popover, com botão para limpar.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `name` | `string` | — | Nome do campo no esquema (o valor é um `Date`) |
| `label` | `string` | — | Label do campo |
| `dateFormat` | `string` | `'dd-MM-yyyy'` | Formato de apresentação (date-fns) |
| `placeholder` | `string` | — | Texto quando não há data |
| `disableBefore` / `disableAfter` | `Date` | — | Limita as datas que se podem escolher |
| `disableDayOfWeek` | `number \| number[]` | — | Dias da semana bloqueados (`0` = domingo) |
| `date` / `onDateChange` | `Date` / `(date?: Date) => void` | — | Modo controlado, fora de um `IGRPForm` |

```tsx
<IGRPDatePickerSingle
  name="dataInicio"
  label="Data de início"
  disableBefore={new Date()}
  disableDayOfWeek={[0, 6]}
/>
```

Quando a data é limpa, o valor do campo fica `null` — o esquema deve aceitá-lo se o campo for opcional (`z.date().nullable()`).

### `IGRPFormList` [#igrpformlist]

Lista repetitiva dentro de um formulário: cada item é um cartão colapsável com os seus campos, com botões para adicionar e remover.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `id` | `string` | — | Nome do array no esquema (obrigatório) |
| `renderItem` | `(item, index) => ReactNode` | — | Os campos de cada item (obrigatório) |
| `defaultItem` | `TItem` | — | Valores de um item novo |
| `label` / `description` | `string` | — | Título e descrição da lista |
| `computeLabel` | `(item, index) => string` | — | Título de cada cartão |
| `addButtonLabel` | `string` | — | Texto do botão de adicionar |
| `allowEmpty` | `boolean` | `false` | Permite ficar sem itens (por defeito mantém pelo menos um) |
| `allowMultipleOpen` | `boolean` | `false` | Vários itens abertos ao mesmo tempo |

```tsx
const schema = z.object({
  contactos: z.array(z.object({ nome: z.string().min(1), telefone: z.string() })),
});

<IGRPFormList
  id="contactos"
  label="Contactos"
  addButtonLabel="Adicionar contacto"
  defaultItem={{ nome: '', telefone: '' }}
  computeLabel={(item, i) => item.nome || `Contacto ${i + 1}`}
  renderItem={(_item, index) => (
    <>
      <IGRPInputText name={`contactos.${index}.nome`} label="Nome" />
      <IGRPInputPhone name={`contactos.${index}.telefone`} label="Telefone" />
    </>
  )}
/>
```

Dentro de um `IGRPForm`, cada campo do item escreve no formulário pelo seu próprio `name` (`contactos.0.nome`…). Fora de um formulário, usa `value` / `onChange` e o terceiro argumento de `renderItem`.

## Outros componentes de formulário [#outros-componentes-de-formulário]

Todos seguem o mesmo padrão: `name`, `label`, `helperText`, `required` e ligação automática ao `IGRPForm`.

| Componente | Para quê | Widget no Studio |
|---|---|---|
| `IGRPInputNumber` | Número formatado (`Intl.NumberFormatOptions`), com `min` / `max` | Number |
| `IGRPInputPassword` | Palavra-passe com botão para mostrar (`showPasswordToggle`) | Password |
| `IGRPInputUrl` | URL com seletor de protocolo | URL |
| `IGRPInputSearch` | Pesquisa com debounce e botão de submeter | Input Search |
| `IGRPInputPhone` | Telefone com país e formato internacional | Phone |
| `IGRPInputColor` | Seletor de cor (oklch, hex…) | Color |
| `IGRPInputAddOn` | Campo de texto com um seletor acoplado | Add On |
| `IGRPInputHidden` | Valor escondido que segue no formulário | Input Hidden |
| `IGRPTextarea` | Texto em várias linhas | Textarea |
| `IGRPCombobox` | Escolha com pesquisa sobre muitas opções | Combobox |
| `IGRPMultiSelect` | Escolha múltipla | — |
| `IGRPCheckbox` | Caixa de verificação | Checkbox |
| `IGRPSwitch` | Interruptor ligado/desligado | Switch |
| `IGRPRadioGroup` | Escolha única visível; `variant="card"` mostra cartões de opção | Radio |
| `IGRPInputTime` | Hora (`HH:mm`) | Time |
| `IGRPDateTimeInput` | Data e hora num só campo | — |
| `IGRPDatePickerRange` / `IGRPDatePickerMultiple` | Intervalo de datas / várias datas | Date Range / Date Multiple |
| `IGRPDatePickerInputSingle` | Data escrita à mão, com calendário de apoio | Date Picker Input Single |
| `IGRPCalendarSingle` / `Range` / `Multiple` (+ `Time`) | Calendário sempre visível, com ou sem hora | Calendar Single / Range / Multiple… |
| `IGRPInputFile` | Upload de ficheiros com zona de arrastar | Upload File |
| `IGRPRepetitiveComponent` | Repete um bloco por cada item de um array (só apresentação) | Repetitive Component |
| `IGRPLabel` | Label isolada | Label |
| `IGRPRichTextEditor` / `IGRPRichTextView` | Editor e visualizador de texto rico, importados de `…/rich-text` | — |

<Callout type="warning" title="Componentes descontinuados">
  `IGRPCalendar` e `IGRPDatePicker` ainda existem, mas usa as variantes `Single`, `Range` ou `Multiple` — ver [Instalação e tema](./instalacao.mdx#componentes-descontinuados).
</Callout>


# Visão geral

URL: /pt/docs/developer/framework/design-system

O Design System do iGRP (@igrp/igrp-framework-react-design-system) — os componentes React que as aplicações Next.js e o gerador do Studio usam, organizados em três camadas.

# Design System [#design-system]

O **Design System do iGRP** é o conjunto de componentes React de todas as aplicações frontend da plataforma. Assenta em **Radix UI**, **Tailwind CSS v4** e ícones **Lucide**, segue as convenções do **shadcn/ui** e vive em `shared/frontend/nextjs/packages/design-system`.

```bash
pnpm add @igrp/igrp-framework-react-design-system
```

<Callout type="info" title="É o que o Studio gera">
  Cada widget que arrastas no [Page Builder](/docs/developer/studio/ide/page-builder) é gerado como um componente deste pacote — `IGRPForm`, `IGRPInputText`, `IGRPDataTable`, `IGRPButton`… O código gerado e o código que escreves à mão usam os mesmos componentes, por isso podes editar uma página gerada sem sair do Design System. A configuração de cada widget no Studio está em [Widgets](/docs/developer/studio/ide/components).
</Callout>

<Callout type="info" title="Secção em construção">
  Esta secção cobre a instalação e os componentes principais de cada área. O catálogo completo será acrescentado aos poucos.
</Callout>

## As três camadas [#as-três-camadas]

<Grid cols={3}>
  <Feature icon="Layers" color="primary" title="Horizon" eyebrow="Por defeito">
    Componentes `IGRP*` de alto nível com as convenções iGRP: label, texto de ajuda, ícone, estado de carregamento e ligação ao formulário. São os que o gerador usa.
  </Feature>
  <Feature icon="Box" color="neutral" title="Primitives" eyebrow="Quando é preciso">
    Blocos de base (`Button`, `Dialog`, `Table`…), alinhados com o shadcn. Para composições próprias, quando um Horizon é demasiado opinativo.
  </Feature>
  <Feature icon="Sparkles" color="purple" title="Custom" eyebrow="Domínio iGRP">
    Composições específicas da plataforma: `IGRPStatsCardMini`, `IGRPStatusBanner`, `IGRPUserAvatar`…
  </Feature>
</Grid>

O mesmo campo nas duas camadas:

```tsx
// Primitive — um input estilizado; tudo o resto é contigo
<Input placeholder="Email" />

// Horizon — label, ajuda, ícone, ligação ao formulário e erro de validação
<IGRPInputText
  name="email"
  label="Email"
  helperText="Nunca partilhamos o teu email"
  showIcon
  iconName="Mail"
/>
```

## Regras [#regras]

<Checklist>
  <Item title="Prefere sempre o Horizon">Desce para os Primitives só quando o Horizon não serve; usa Custom para composições iGRP.</Item>
  <Item title="Os IGRP* são componentes de cliente">Um ficheiro que importa da raiz do pacote precisa de `'use client'`.</Item>
  <Item title="Formulários são sempre IGRPForm + Zod">Nunca `<form>` cru nem `react-hook-form` diretamente — ver [Formulários](./formularios.mdx).</Item>
  <Item title="Cores por tokens semânticos">`bg-background`, `text-foreground`, `bg-primary`… nunca cores Tailwind cruas — ver [Instalação e tema](./instalacao.mdx).</Item>
  <Item title="cn() para juntar classes">`size-*` para larguras e alturas iguais; `flex gap-*` para espaçamentos.</Item>
</Checklist>

## Componentes por área [#componentes-por-área]

<Cards>
  <Card title="Formulários" href="/docs/developer/framework/design-system/formularios" description="IGRPForm, inputs, seleção, datas, ficheiros, texto rico, listas repetitivas." />
  <Card title="Dados" href="/docs/developer/framework/design-system/dados" description="IGRPDataTable (células, filtros, ações), gráficos, cards e indicadores." />
  <Card title="Layout e navegação" href="/docs/developer/framework/design-system/layout-navegacao" description="Cabeçalho e rodapé de página, menus, tabs, accordion, dropdowns, command." />
  <Card title="Feedback e diálogos" href="/docs/developer/framework/design-system/feedback" description="Toasts, alertas, banners, diálogos modais e de confirmação, notificações." />
  <Card title="Conteúdo e media" href="/docs/developer/framework/design-system/conteudo" description="Tipografia, ícones, imagens, PDF, vídeo, chat, processos." />
  <Card title="Primitives e Custom" href="/docs/developer/framework/design-system/primitives" description="Os blocos de base e as composições iGRP, e quando usá-los." />
</Cards>


# Instalação e tema

URL: /pt/docs/developer/framework/design-system/instalacao

Instalar o Design System numa aplicação Next.js, configurar o Tailwind v4 com os tokens, ativar o dark mode e personalizar o tema.

Num projeto gerado pelo Studio, tudo isto já vem configurado. Esta página é para quando adicionas o Design System a uma aplicação existente, ou queres perceber e personalizar a configuração.

## Requisitos [#requisitos]

| | Versão |
|---|---|
| Node.js | `22+` |
| React | `^19.2` |
| Next.js | `^15.5` |
| Tailwind CSS | `v4` |

## Instalar [#instalar]

```bash
pnpm add @igrp/igrp-framework-react-design-system
```

O pacote vive no registry da NOSi — ver a nota sobre o `--registry` em [CLI](/docs/developer/studio/cli#instalação).

| Entrada | Conteúdo |
|---|---|
| `@igrp/igrp-framework-react-design-system` | Componentes, hooks e utilitários |
| `…/tokens` | Variáveis CSS do tema (tokens) |
| `…/cn` | O utilitário `cn()` para juntar classes |
| `…/rich-text` | Editor e visualizador de texto rico (entrada separada, mais pesada) |

## Configurar o CSS [#configurar-o-css]

O Design System **não traz CSS compilado**: a tua aplicação compila o Tailwind e lê as classes usadas pelos componentes. No CSS principal (por exemplo `src/styles/globals.css`):

```css
@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

/* lê a tua aplicação e o Design System */
@source "../**/*.{ts,tsx,js,jsx}";
@source "../../node_modules/@igrp/igrp-framework-react-design-system/dist/**/*.{js,jsx,ts,tsx,mjs,cjs}";

/* tokens do tema */
@import "@igrp/igrp-framework-react-design-system/tokens";

/* opcional: os teus overrides */
@import "./themes.css";
```

<Callout type="warning" title="Não importes styles.css">
  Versões antigas importavam `@igrp/…/styles.css`. Remove esse import: causa conflitos de cascata e utilitários em falta. Bastam o `@source` e os tokens.
</Callout>

## Usar os componentes [#usar-os-componentes]

```tsx
'use client';

import { IGRPButton, IGRPCard, IGRPInputText, useIGRPToast } from '@igrp/igrp-framework-react-design-system';
```

Os componentes `IGRP*` são componentes de cliente — o ficheiro que os importa precisa de `'use client'`.

## Tokens [#tokens]

As cores, raios e estados vêm de variáveis CSS definidas em `:root` e `.dark`. Usa sempre as classes semânticas (`bg-primary`, `text-muted-foreground`, `border-border`…), nunca cores Tailwind cruas — é isso que faz o dark mode e os temas funcionarem.

| Token | Para quê |
|---|---|
| `--background` / `--foreground` | Fundo e texto da página |
| `--primary` / `--primary-foreground` | Ações principais |
| `--secondary`, `--muted`, `--accent` (+ `-foreground`) | Elementos secundários, fundos discretos, hover |
| `--destructive` | Ações destrutivas |
| `--success`, `--warning`, `--info`, `--highlight` (+ `-foreground`) | Estados e realces |
| `--border`, `--input`, `--ring` | Bordas, campos, foco |
| `--card`, `--popover` (+ `-foreground`) | Superfícies |
| `--radius` | Raio base dos cantos |
| `--sidebar-*` | Barra lateral |
| `--chart-1` … `--chart-8` | Cores dos gráficos |
| `--process-completed`, `--process-active` | Estados dos passos de processo |

## Dark mode [#dark-mode]

O dark mode é a classe `.dark` num elemento pai (normalmente `<html>`). Com `next-themes`:

```tsx
import { ThemeProvider } from 'next-themes';

<ThemeProvider attribute="class" defaultTheme="system">
  {children}
</ThemeProvider>
```

Para a barra do browser em mobile acompanhar o tema, usa `useIGRPMetaColor()` no layout.

## Personalizar o tema [#personalizar-o-tema]

Sobrepõe os tokens num ficheiro teu, carregado **depois** do import dos tokens:

```css title="themes.css"
:root {
  --primary: oklch(0.62 0.13 230);
  --primary-foreground: oklch(0.985 0 0);
}

.dark {
  --primary: oklch(0.72 0.12 230);
  --primary-foreground: oklch(0.205 0 0);
}

/* variantes de tema ativáveis por classe */
.theme-green {
  --primary: var(--color-green-600);
}
```

## Componentes descontinuados [#componentes-descontinuados]

| Descontinuado | Usa |
|---|---|
| `IGRPCalendar` | `IGRPCalendarSingle`, `IGRPCalendarRange` ou `IGRPCalendarMultiple` |
| `IGRPDatePicker` | `IGRPDatePickerSingle`, `IGRPDatePickerRange` ou `IGRPDatePickerMultiple` |


# Layout e navegação

URL: /pt/docs/developer/framework/design-system/layout-navegacao

Componentes do Design System para estruturar páginas e navegar — cabeçalho de página, menu de secções, tabs, accordion, dropdowns e barra lateral.

Componentes que dão estrutura à página e permitem navegar entre secções e ações. Esta página é a vista de código: imports, props e exemplos.

<Callout type="info" title="Secção em construção">
  Esta página cobre os componentes principais; o resto do catálogo será completado. Para configurar estes componentes no Studio (sem código), vê [Widgets](/docs/developer/studio/ide/components) do Page Builder.
</Callout>

## Componentes principais [#componentes-principais]

### `IGRPPageHeader` [#igrppageheader]

Cabeçalho de página com título, descrição, botão de voltar opcional e uma zona à direita para ações.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `title` | `string` | — | Título da página. |
| `description` | `string` | — | Subtítulo. |
| `variant` | `"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h3"` | Nível e tamanho do título. |
| `children` | `ReactNode` | — | Ações à direita (botões, dropdowns). |
| `isSticky` | `boolean` | — | Fixa o cabeçalho no topo ao fazer scroll. |
| `showBackButton` | `boolean` | — | Mostra o botão de voltar. |
| `urlBackButton` / `backButtonUseBrowserBack` | `string` / `boolean` | — | Destino do botão de voltar, ou usa o histórico do browser. |

```tsx
<IGRPPageHeader
  title="Pedidos"
  description="Gestão dos pedidos recebidos"
  showBackButton
  urlBackButton="/"
>
  <IGRPButton showIcon iconName="Plus">Novo pedido</IGRPButton>
</IGRPPageHeader>
```

### `IGRPMenuNavigation` [#igrpmenunavigation]

Menu lateral de secções da própria página: ao clicar, faz scroll até à secção e marca-a como ativa.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `sections` | `IGRPMenuNavigationItem[]` | — | Itens: `id`, `label`, `icon`, `disabled`. |
| `title` | `string` | `"Navigation"` | Título do menu. |
| `activeSection` / `onSectionChange` | `string` / `(sectionId: string) => void` | — | Controlo externo da secção ativa (opcional). |
| `isStickyTop` | `boolean` | `false` | Mantém o menu visível ao fazer scroll. |
| `showChevron` | `boolean` | `true` | Seta à direita de cada item. |
| `badgeContent` / `badgeColor` | `string` / `IGRPColorVariants` | — | Badge junto ao título. |

As secções de destino ligam-se pelo atributo `data-section-id`, ou pelo `ref` devolvido por `useIGRPMenuNavigation().getSectionRef(id)` quando a página está dentro de um `IGRPMenuNavigationProvider` — é o que o código gerado pelo Studio faz quando um widget tem a referência ativa.

```tsx
<div className="flex gap-6">
  <IGRPMenuNavigation
    title="Pedido"
    isStickyTop
    sections={[
      { id: 'dados', label: 'Dados', icon: 'FileText' },
      { id: 'pagamento', label: 'Pagamento', icon: 'CreditCard' },
    ]}
  />
  <div className="flex flex-col gap-6">
    <section data-section-id="dados">…</section>
    <section data-section-id="pagamento">…</section>
  </div>
</div>
```

### `IGRPTabs` [#igrptabs]

Separadores definidos por uma lista de itens, com ícones, badges e scroll horizontal quando não cabem.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `items` | `IGRPTabItem[]` | — | Cada item: `value`, `label`, `content`, `icon`, `disabled`, `badgeContent`, `keepMounted`. |
| `defaultValue` / `value` / `onValueChange` | `string` | — | Separador inicial, ou controlo externo. |
| `variant` | `"default" \| "outline" \| "pills" \| "underline" \| "cards"` | `"default"` | Estilo da barra de separadores. |
| `showIcon` / `iconPlacement` | `boolean` / `"start" \| "end" \| "top"` | `false` | Mostra o ícone de cada item. |
| `showBadge` | `boolean` | — | Mostra o `badgeContent` de cada item. |
| `fullWidth` / `contentBorder` | `boolean` | `false` | Barra a toda a largura; borda à volta do conteúdo. |

```tsx
<IGRPTabs
  defaultValue="geral"
  variant="underline"
  showIcon
  items={[
    { value: 'geral', label: 'Geral', icon: 'Info', content: <DadosGerais /> },
    { value: 'historico', label: 'Histórico', icon: 'History', content: <Historico />, keepMounted: true },
  ]}
/>
```

`keepMounted` mantém o conteúdo montado quando o separador está inativo — útil para não perder o estado de um formulário.

### `IGRPAccordion` [#igrpaccordion]

Lista de painéis colapsáveis; abre um de cada vez e começa com o primeiro aberto.

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `items` | `IGRPAccordionItem[]` | — | Cada item: `title`, `content`, e opcionalmente `iconName`, `showIcon`, `iconPlacement`. |
| `defaultValue` | `string` | título do primeiro item | Título do painel aberto inicialmente. |
| `showIcon` / `iconName` / `iconPlacement` | `boolean` / `string` / `"start" \| "end"` | `true` / `"ChevronDown"` / `"end"` | Ícone do gatilho, para todos os itens. |
| `classNameTrigger` / `classNameContent` | `string` | — | Classes do gatilho e do conteúdo. |

```tsx
<IGRPAccordion
  items={[
    { title: 'Dados pessoais', content: <DadosPessoais /> },
    { title: 'Morada', content: <Morada /> },
  ]}
/>
```

### `IGRPDropdownMenu` [#igrpdropdownmenu]

Menu de ações que abre a partir de um botão. É o primitive de dropdown com prefixo `IGRP`, composto por partes.

| Prop | Tipo | Por defeito | Descrição |
|---|---|---|---|
| `IGRPDropdownMenuTrigger` | `asChild?: boolean` | — | Elemento que abre o menu (normalmente um `IGRPButton` com `asChild`). |
| `IGRPDropdownMenuContent` | `align?: "start" \| "center" \| "end"` | `"start"` | Painel do menu. |
| `IGRPDropdownMenuItem` | `onSelect`, `disabled`, `variant?: "default" \| "destructive"` | — | Uma ação. |
| `IGRPDropdownMenuLabel` / `IGRPDropdownMenuSeparator` | — | — | Título e separador de grupos. |
| `IGRPDropdownMenuCheckboxItem` / `IGRPDropdownMenuRadioItem` / `IGRPDropdownMenuSub` | — | — | Opções marcáveis e submenus. |

```tsx
<IGRPDropdownMenu>
  <IGRPDropdownMenuTrigger asChild>
    <IGRPButton variant="outline" showIcon iconName="Ellipsis">Mais</IGRPButton>
  </IGRPDropdownMenuTrigger>
  <IGRPDropdownMenuContent align="end">
    <IGRPDropdownMenuLabel>Pedido</IGRPDropdownMenuLabel>
    <IGRPDropdownMenuItem onSelect={exportar}>Exportar</IGRPDropdownMenuItem>
    <IGRPDropdownMenuSeparator />
    <IGRPDropdownMenuItem variant="destructive" onSelect={anular}>Anular</IGRPDropdownMenuItem>
  </IGRPDropdownMenuContent>
</IGRPDropdownMenu>
```

## Outros componentes [#outros-componentes]

| Componente | Para quê | Widget no Studio |
|---|---|---|
| `IGRPPageFooter` | Rodapé da página, normalmente com os botões de submeter (`isSticky` fixa-o em baixo) | — |
| `IGRPContainer` | `div` com `id` para agrupar conteúdo | Container (o Studio gera um contentor de layout) |
| `IGRPSeparator` | Linha divisória horizontal ou vertical | Separator |
| `IGRPSidebarProvider`, `IGRPSidebar`, `IGRPSidebarMenu`… | Barra lateral da aplicação (o layout das apps iGRP já a inclui) | — |
| `IGRPMenubar` (+ `…Menu`, `…Trigger`, `…Item`) | Barra de menus horizontal | — |
| `IGRPCommand` / `IGRPCommandDialog` | Paleta de comandos e pesquisa rápida (estilo ⌘K) | — |
| `Breadcrumb`, `Pagination` (Primitives) | Migalhas de pão e paginação manual, quando o Horizon não cobre o caso — ver [Primitives e Custom](./primitives.mdx) | — |


# Primitives e Custom

URL: /pt/docs/developer/framework/design-system/primitives

Os 56 blocos de base do Design System, alinhados com o shadcn/ui, e as 4 composições iGRP da camada Custom — quando usá-los e como acrescentar um componente.

Além dos componentes `IGRP*` da camada Horizon, o Design System exporta duas camadas: os **Primitives**, os blocos de base, e os **Custom**, composições próprias da plataforma. Todos saem da raiz do pacote:

```tsx
'use client';

import { Button, Dialog, DialogContent, DialogTitle } from '@igrp/igrp-framework-react-design-system';
import { IGRPStatsCardMini } from '@igrp/igrp-framework-react-design-system';
```

Os Primitives são exportados com o **nome do shadcn, sem prefixo nem sufixo**: `Button`, `Dialog`, `Table`, `Sheet`… Os Horizon e os Custom usam o prefixo `IGRP`.

<Callout type="info" title="Secção em construção">Esta página dá a visão geral; o detalhe de cada componente será completado.</Callout>

## Primitive ou Horizon [#primitive-ou-horizon]

| | Primitives | Horizon |
|---|---|---|
| **Para quê** | Blocos de base: estilo mínimo, máxima flexibilidade | Componentes opinativos para os padrões comuns do iGRP |
| **Assenta em** | Radix UI e variantes CVA | Nos Primitives (são wrappers) |
| **Props** | Props HTML/React + variantes (`variant`, `size`, `asChild`) | Props iGRP (`label`, `helperText`, `showIcon`, `iconName`, `loading`…) |
| **Formulários** | Nenhuma ligação | Ligados ao `IGRPForm` e ao `react-hook-form` |
| **Usa quando** | Precisas de controlo total ou de uma composição própria | Queres formulários, botões com ícone/carregamento, campos com label… |

<Callout type="info" title="Prefere sempre o Horizon">
  Desce para um Primitive só quando o componente Horizon é demasiado opinativo para o caso. O gerador do Studio emite componentes Horizon (e o `IGRPStatusBanner` da camada Custom), nunca Primitives.
</Callout>

```tsx
// Primitive — um botão estilizado; tudo o resto é contigo
<Button variant="outline" size="sm">Guardar</Button>

// Horizon — ícone, estado de carregamento e atributos iGRP
<IGRPButton showIcon iconName="ArrowRight" loading={isSubmitting} loadingText="A guardar...">
  Guardar
</IGRPButton>
```

## Catálogo dos Primitives [#catálogo-dos-primitives]

Cada linha é um ficheiro em `src/components/primitives/`. A coluna **Exports** lista o componente raiz; as partes (`DialogTitle`, `TableRow`…) seguem a nomenclatura do shadcn.

### Formulário [#formulário]

| Exports | Para quê | Horizon equivalente |
|---|---|---|
| `Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormMessage`, `useFormField` | Contexto `react-hook-form` (sem submissão nem Zod) | `IGRPForm` |
| `Field`, `FieldLabel`, `FieldError`, `FieldSet`… | Estrutura de campo: label, descrição, erro, grupos | — |
| `Input` | Campo de texto | `IGRPInputText`, `IGRPInputNumber`, `IGRPInputPassword`, `IGRPInputUrl`, `IGRPInputSearch` |
| `InputGroup`, `InputGroupInput`, `InputGroupAddon`… | Campo com ícone, botão ou prefixo | `IGRPInputAddOn` |
| `InputOTP`, `InputOTPSlot`… | Código de uso único (OTP) | — |
| `Textarea` | Texto multilinha | `IGRPTextarea` |
| `Label` | Label acessível | `IGRPLabel` |
| `Checkbox` | Caixa de seleção | `IGRPCheckbox` |
| `RadioGroup`, `RadioGroupItem` | Escolha única | `IGRPRadioGroup` |
| `Switch` | Interruptor on/off | `IGRPSwitch` |
| `Select`, `SelectTrigger`, `SelectItem`… | Lista de opções | `IGRPSelect` |
| `NativeSelect`, `NativeSelectOption` | `<select>` nativo estilizado | — |
| `Slider` | Valor num intervalo | — |
| `Calendar` | Calendário | `IGRPCalendarSingle`, `IGRPCalendarRange`, `IGRPCalendarMultiple` |
| `Toggle`, `ToggleGroup` | Botões de alternar | — |
| `Button` | Botão | `IGRPButton` |
| `ButtonGroup` | Grupo de botões encostados | — |

### Overlays [#overlays]

| Exports | Para quê | Horizon equivalente |
|---|---|---|
| `Dialog`, `DialogContent`, `DialogTitle`… | Diálogo modal (`DialogTitle` obrigatório) | `IGRPModalDialog` |
| `AlertDialog`… | Confirmação | `IGRPAlertDialog` |
| `Sheet`, `SheetContent`, `SheetTitle`… | Painel lateral | — |
| `Drawer`, `DrawerContent`, `DrawerTitle`… | Painel inferior (bottom sheet) | — |
| `Popover`, `PopoverContent`… | Conteúdo flutuante ancorado | — |
| `HoverCard`… | Cartão ao passar o rato | — |
| `Tooltip`, `TooltipProvider`… | Dica curta | — |
| `DropdownMenu`, `DropdownMenuItem`… | Menu de ações | `IGRPDropdownMenu` |
| `ContextMenu`… | Menu do botão direito | — |
| `Command`, `CommandDialog`, `CommandInput`… | Lista pesquisável, paleta de comandos | `IGRPCommand`, `IGRPCombobox` |

### Navegação [#navegação]

| Exports | Para quê | Horizon equivalente |
|---|---|---|
| `NavigationMenu`… | Menu de navegação | `IGRPMenuNavigation` |
| `Menubar`, `MenubarMenu`… | Barra de menus | `IGRPMenubar` |
| `Sidebar`, `SidebarMenu`, `SidebarProvider`… | Barra lateral da aplicação | — |
| `Breadcrumb`, `BreadcrumbList`… | Trilho de navegação | — |
| `Pagination`… | Paginação | — |
| `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | Separadores | `IGRPTabs` |
| `Stepper`, `StepperItem`… | Passos numerados | — (para processos, `IGRPStepperProcess`) |

### Dados e layout [#dados-e-layout]

| Exports | Para quê | Horizon equivalente |
|---|---|---|
| `Table`, `TableRow`, `TableCell`… | Tabela HTML estilizada | `IGRPDataTable` |
| `Card`, `CardHeader`, `CardContent`… | Superfície em cartão | `IGRPCard`, `IGRPCardDetails`, `IGRPInfoCard`, `IGRPStatsCard` |
| `Item`, `ItemContent`, `ItemActions`… | Linha de lista com media e ações | — |
| `ChartContainer`, `ChartTooltip`, `ChartLegend`… | Base para gráficos Recharts | `IGRPAreaChart`, `IGRPLineChart`, `IGRPPieChart`… |
| `Accordion`, `AccordionItem`… | Secções colapsáveis | `IGRPAccordion` |
| `Collapsible`… | Um bloco que abre e fecha | — |
| `Carousel`, `CarouselItem`… | Carrossel | — |
| `ScrollArea` | Área com scroll estilizado | — |
| `ResizablePanelGroup`, `ResizablePanel`, `ResizableHandle` | Painéis redimensionáveis | — |
| `Separator` | Linha divisória | `IGRPSeparator` |
| `AspectRatio` | Proporção fixa | — |
| `Avatar`, `AvatarImage`, `AvatarFallback` | Avatar (fallback obrigatório) | `IGRPAvatar` |
| `Badge` | Etiqueta | `IGRPBadge` |
| `Kbd`, `KbdGroup` | Tecla de atalho | — |

### Feedback [#feedback]

| Exports | Para quê | Horizon equivalente |
|---|---|---|
| `Alert`, `AlertTitle`, `AlertDescription` | Mensagem em bloco | `IGRPAlert`, `IGRPBanner` |
| `Empty`, `EmptyTitle`… | Estado vazio | — |
| `Toaster` | Toasts (com `toast()` do `sonner`) | `IGRPToaster` / `useIGRPToast` |
| `Progress` | Barra de progresso | — |
| `Skeleton` | Placeholder de carregamento | — |
| `Spinner` | Indicador de carregamento | `IGRPLoadingSpinner` |

### Outros [#outros]

| Exports | Para quê | Horizon equivalente |
|---|---|---|
| `Cropper`, `CropperImage`, `CropperCropArea` | Recorte de imagem | `IGRPImageCropper` |

## Diferenças face ao shadcn [#diferenças-face-ao-shadcn]

Estes Primitives divergem de propósito do shadcn — tem-nas em conta antes de copiar código do site do shadcn.

| Primitive | Diferença |
|---|---|
| `Button` | Tamanhos extra `xs`, `icon-xs`, `icon-sm`, `icon-lg`, além de `sm`, `default`, `lg`, `icon` |
| `Accordion` | `AccordionTrigger` aceita `iconName`, `showIcon` e `iconPlacement` (o indicador pode ser escondido, trocado ou ir para o início); respeita `prefers-reduced-motion` |
| `Form` | `FormItem` usa `flex flex-col gap-2` em vez de `grid`; `FormMessage` tem `role="alert"` para os leitores de ecrã anunciarem os erros |
| `Popover` | `PopoverContent` aceita `container`, para abrir dentro de um `Dialog` ou `Drawer` sem partir o foco |
| `RadioGroup` | `RadioGroupItem` tem `variant` (`default`, `outline`, `soft`) e `size` (`sm`, `md`, `lg`) |

## Componentes Custom [#componentes-custom]

Composições iGRP sobre os Horizon e os Primitives, em `src/components/custom/`. As cores aceitam `primary`, `secondary`, `success`, `warning`, `info`, `destructive` e `indigo`; os estilos aceitam `solid`, `soft` e `outline`.

### IGRPStatsCardMini [#igrpstatscardmini]

Cartão de indicador compacto: título, valor e ícone com contorno, numa só cor. Aceita as props de conteúdo do `IGRPStatsCard`, mas a aparência resume-se a `variant`.

| Prop | Tipo | Descrição |
|---|---|---|
| `title` | `string` | Título |
| `value` | `string \| number` | Valor em destaque |
| `iconName` | `string` | Ícone Lucide |
| `variant` | cor | Cor do valor e do ícone (por defeito `primary`) |
| `titleSize` / `valueSize` | tamanho | Por defeito `sm` / `2xl` |
| `onClick` | `() => void` | Torna o cartão clicável |

```tsx
<IGRPStatsCardMini title="Pedidos abertos" value={128} iconName="Inbox" variant="info" />
```

### IGRPStatsCardTopBorderColored [#igrpstatscardtopbordercolored]

Cartão de indicador com uma borda colorida no topo e o ícone à direita.

| Prop | Tipo | Descrição |
|---|---|---|
| `title` | `string` | Título |
| `value` | `string \| number` | Valor em destaque |
| `iconName` | `string` | Ícone Lucide |
| `cardVariant` | cor | Cor da borda e do ícone |

```tsx
<IGRPStatsCardTopBorderColored title="Concluídos" value="94%" iconName="CircleCheck" cardVariant="success" />
```

### IGRPStatusBanner [#igrpstatusbanner]

<Badge color="green" icon="Wand2">Gerado pelo Studio</Badge>

Faixa de estado: um `IGRPAlert` com um ponto colorido e o texto, mais um `IGRPBadge` à direita.

| Prop | Tipo | Descrição |
|---|---|---|
| `text` | `string` | Texto do estado |
| `color` / `variant` | cor / estilo | Cor e estilo da faixa (por defeito `success` / `soft`) |
| `badgeText` | `string` | Texto do badge |
| `badgeColor` / `badgeVariant` | cor / estilo | Cor e estilo do badge (por defeito `primary` / `outline`) |

```tsx
<IGRPStatusBanner text="Pedido em análise" color="warning" badgeText="Em curso" badgeColor="warning" />
```

### IGRPUserAvatar [#igrpuseravatar]

Avatar de utilizador: mostra a imagem e, se não houver ou falhar, o conteúdo de fallback.

| Prop | Tipo | Descrição |
|---|---|---|
| `image` | `string \| null` | URL da imagem |
| `alt` | `string` | Texto alternativo (por defeito `Current User`) |
| `fallbackContent` | `ReactNode` | **Obrigatório.** Normalmente as iniciais |
| `fallbackClass` | `string` | Classes do fallback |

```tsx
<IGRPUserAvatar image={user.avatarUrl} alt={user.name} fallbackContent="AS" />
```

## Acrescentar um componente [#acrescentar-um-componente]

<Steps>

### Existe no shadcn? Primeiro um Primitive [#existe-no-shadcn-primeiro-um-primitive]

Copia-o do shadcn para `src/components/primitives/`, comparando com o upstream (o fluxo `--diff` do CLI do shadcn, num diretório temporário).

### Todas as aplicações vão querer o mesmo? Um wrapper Horizon [#todas-as-aplicações-vão-querer-o-mesmo-um-wrapper-horizon]

Se todas as aplicações precisam das mesmas labels, ícones, estado de carregamento ou ligação ao formulário, acrescenta um componente `IGRP*` em `src/components/horizon/` por cima do Primitive.

### É específico do domínio iGRP? Custom [#é-específico-do-domínio-igrp-custom]

Variantes de cartões de indicadores, avatares, faixas de estado e afins vão para `src/components/custom/`.

</Steps>

Cada Primitive guarda no comentário inicial a data da última sincronização com o shadcn (por exemplo `/* shadcn: 2026-09-15 */`; `// shadcn: …` também é aceite). Para saber se o upstream mudou desde então, a partir de `packages/design-system/`:

```bash
pnpm drift:shadcn          # node scripts/check-shadcn-drift.mjs
pnpm drift:shadcn:update   # novo baseline, depois de rever as mudanças
```

Se o upstream mudou algo de que divergimos de propósito, regista a diferença em `COMPONENTS.md` antes de atualizar o baseline.

## Explorar no Storybook [#explorar-no-storybook]

O monorepo tem um Storybook com os componentes, no pacote `packages/design-system-storybook`:

```bash
cd shared/frontend/nextjs/packages/design-system-storybook
pnpm storybook   # http://localhost:6006
```


# Frontend (Next.js)

URL: /pt/docs/developer/framework/frontend

O que uma aplicação Next.js gerada pelo Studio usa em runtime — @igrp/framework-next, autenticação, componentes de UI e design system — e como a estender.

Uma aplicação frontend iGRP é um projeto **Next.js (App Router)** gerado pelo Studio sobre as bibliotecas partilhadas de `shared/frontend/nextjs`. O Studio desenha as páginas; o framework trata de autenticação, layout, permissões e componentes.

## Bibliotecas [#bibliotecas]

| Pacote | Papel |
|---|---|
| `@igrp/framework-next` | Layout (`IGRPLayoutFull`), navegação, menus sincronizados com o Access Management, helpers de autorização (`igrpAuthorize`, `igrpGetClaims`) |
| `@igrp/framework-next-auth` | Sessão e tokens via Keycloak (NextAuth) |
| `@igrp/framework-next-ui` | Componentes de aplicação: header, notificações, `IGRPAuthorization`, `IGRPForbidden` |
| `@igrp/igrp-framework-react-design-system` | Design system React (botões, formulários, tabelas, …) — ver [Design System](/docs/developer/framework/design-system) |
| `@igrp/framework-next-types` | Tipos partilhados |

<Grid cols={2}>
  <Feature icon="Triangle" color="green" title="Projeto Next.js" href="/docs/developer/framework/frontend/nextjs">
    Stack, estrutura de pastas, o que é gerado e o que é teu, configuração e como correr.
  </Feature>
  <Feature icon="ShieldCheck" color="purple" title="Permissões no frontend" href="/docs/developer/framework/frontend/permissoes">
    Proteger páginas, componentes e server actions a partir das claims do token.
  </Feature>
</Grid>

## Como se constrói [#como-se-constrói]

<Steps>
  <Step>
    ### Desenhar no Studio [#desenhar-no-studio]
    Páginas e componentes nascem no [Page Builder](/docs/developer/studio/ide/page-builder) e no [Page Manager](/docs/developer/studio/ide/page-manager); lógica própria entra via [Custom Code](/docs/developer/studio/ide/customCode) e [Custom Components](/docs/developer/studio/ide/customComponents).
  </Step>
  <Step>
    ### Correr no workspace [#correr-no-workspace]
    O projeto corre ao lado dos serviços da plataforma no [workspace local](/docs/developer/workspace/criar-workspace), já com cliente OAuth e menus registados.
  </Step>
  <Step>
    ### Estender com o framework [#estender-com-o-framework]
    Slots do header, notificações e autorização ficam a cargo das bibliotecas acima — ver [Header Slots](/docs/developer/studio/ide/header-slots) e a [migração de template](/docs/developer/studio/cli/migrate) para atualizar apps existentes.
  </Step>
</Steps>

<Callout type="info" title="Outros frameworks">
  Angular e Vue estão previstos para versões futuras do Studio; hoje o alvo de geração é Next.js.
</Callout>


# Projeto Next.js

URL: /pt/docs/developer/framework/frontend/nextjs

O que vem num projeto frontend gerado pelo Studio — stack, estrutura de pastas, o que é gerado e o que é teu, configuração por variáveis de ambiente e como o correr.

Um projeto frontend criado no Studio parte do template **`@igrp/framework-next-template`**: uma aplicação **Next.js 15 (App Router)** já ligada à plataforma — autenticação, layout, menus e permissões do Access Management, i18n e o Design System. Esta página descreve o projeto; para o criar, ver [Primeiro projeto](/docs/developer/workspace/primeiro-projeto), e para desenhar páginas, [Studio · Frontend](/docs/developer/studio/ide/page-manager).

## Stack [#stack]

| | |
|---|---|
| **Next.js 15** + **React 19** | App Router, Turbopack em dev e em build |
| **Framework iGRP** | `@igrp/framework-next`, `-next-auth`, `-next-ui`, `-next-types` — ver [Frontend](/docs/developer/framework/frontend) |
| **Design System** | `@igrp/igrp-framework-react-design-system` — ver [Design System](/docs/developer/framework/design-system) |
| **Autenticação** | NextAuth v4 com OIDC (Keycloak), ou sem autenticação para pré-visualização |
| **Dados e formulários** | React Query, React Hook Form + Zod |
| **i18n** | `next-intl` (pt, en, fr) |
| **Qualidade** | TypeScript, Tailwind CSS v4, Biome |

## Estrutura [#estrutura]

<Files>
  <Folder name="<projeto>" defaultOpen>
    <Folder name=".igrpstudio" defaultOpen>
      <Folder name="pages" />
      <File name="permissions.json" />
      <File name="baseApp.json" />
    </Folder>
    <Folder name="src" defaultOpen>
      <Folder name="app" defaultOpen>
        <Folder name="(auth)" />
        <Folder name="(igrp)" defaultOpen>
          <Folder name="(generated)" />
          <Folder name="(demo)" />
          <File name="layout.tsx" />
        </Folder>
        <Folder name="(myapp)" />
        <Folder name="api" />
        <File name="igrp.config.ts" />
        <File name="layout.tsx" />
      </Folder>
      <Folder name="actions" />
      <Folder name="components" />
      <Folder name="config" />
      <Folder name="i18n" />
      <Folder name="lib" />
      <Folder name="providers" />
      <Folder name="temp" />
      <File name="igrp.template.config.ts" />
      <File name="middleware.ts" />
    </Folder>
    <Folder name="docs" />
    <Folder name="docker" />
    <Folder name="k8s" />
    <File name="igrp-compose-<projeto>.yaml" />
    <File name=".env.example" />
  </Folder>
</Files>

### O que é do Studio e o que é teu [#o-que-é-do-studio-e-o-que-é-teu]

| Pasta / ficheiro | Quem escreve | Para quê |
|---|---|---|
| `.igrpstudio/` | Studio | Definição das páginas, permissões e app em JSON — a fonte do que o Page Builder mostra |
| `src/app/(igrp)/(generated)/` | Studio | Código das páginas desenhadas no Page Builder. **Não edites à mão** — é regenerado ao gravar |
| `src/app/(myapp)/` | Tu | Componentes, funções, tipos e actions próprios, registados em `igrp.config.ts` — ver [Custom Components](/docs/developer/studio/ide/customComponents) |
| `src/app/igrp.config.ts` | Tu | Registo do que está em `(myapp)` para o Studio o oferecer |
| `src/igrp.template.config.ts` | Template | Configuração do layout: header, sidebar, menus, Access Management, toasts |
| `src/app/(auth)/`, `src/middleware.ts`, `src/lib/` | Template | Login, sessão, proteção de rotas e utilitários |
| `src/app/(igrp)/(demo)/` | Template | Páginas de exemplo (dashboard, permissões…) — podes removê-las |
| `src/temp/` | Template | Utilizador, menus e apps de exemplo para a pré-visualização sem autenticação |
| `src/i18n/messages/` | Tu | Textos em pt, en e fr |

<Callout type="warning" title="Não edites (generated)">
  O que escreveres em `src/app/(igrp)/(generated)/` perde-se na próxima gravação no Studio. Para lógica própria usa o [Custom Code](/docs/developer/studio/ide/customCode) da página ou ficheiros em `(myapp)`.
</Callout>

## Configuração [#configuração]

Tudo se configura por variáveis de ambiente — o `.env.example` documenta cada uma. As principais:

| Variável | |
|---|---|
| `AUTH_PROVIDER` | `igrp-auth` (Keycloak) ou `none` (sem autenticação) |
| `IGRP_AUTH_ISSUER`, `IGRP_AUTH_CLIENT_ID`, `IGRP_AUTH_CLIENT_SECRET` | Cliente OIDC da aplicação |
| `NEXTAUTH_URL`, `NEXTAUTH_SECRET` | Sessão NextAuth |
| `IGRP_APP_CODE` | Código da aplicação no Access Management |
| `IGRP_ACCESS_MANAGEMENT_API` | API do Access Management |
| `IGRP_SYNC_ACCESS`, `IGRP_SYNC_ON_CODE_MENUS`, `IGRP_SYNC_PERMISSIONS` | Sincronizar menus e permissões declarados no código |
| `IGRP_PREVIEW_MODE` | Pré-visualização com dados de exemplo, sem login |
| `NEXT_PUBLIC_BASE_PATH` | Prefixo da app quando servida num subcaminho (ex.: `/apps/<projeto>`) |

<Callout type="info" title="No workspace, o Studio trata disto">
  Num projeto criado dentro do workspace, o Studio regista o cliente OAuth e injeta estas variáveis ao fazer **Deploy Docker** — ver [Projetos](/docs/developer/workspace/projetos).
</Callout>

## Correr [#correr]

<Tabs>
  <TabItem value="studio" label="No workspace">
    No cartão do projeto, **⋮ → Deploy Docker** e depois **Abrir no Navegador**: a app fica em `http://<slug>:<porta>/apps/<projeto>`, ligada à plataforma local.
  </TabItem>
  <TabItem value="local" label="Localmente">
    ```bash
    pnpm install
    cp .env.example .env.local   # preencher as variáveis
    pnpm dev
    ```
    Para correr sem a plataforma, `AUTH_PROVIDER=none` com `IGRP_PREVIEW_MODE=true`.
  </TabItem>
</Tabs>

| Script | |
|---|---|
| `pnpm dev` | Servidor de desenvolvimento |
| `pnpm build` | Lint (Biome e i18n) e build de produção |
| `pnpm start` | Servir o build |
| `pnpm lint` / `pnpm format` | Biome |

## Documentação incluída [#documentação-incluída]

A pasta `docs/` do projeto traz guias sobre a arquitetura, autenticação, Access Management, permissões, design system, tokens, i18n, header slots, Docker, variáveis de ambiente e migração.

## Atualizar o template [#atualizar-o-template]

Projetos criados com versões antigas do template atualizam-se com o [`@igrp/template-migrator`](/docs/developer/studio/cli/migrate).


# Permissões no Frontend

URL: /pt/docs/developer/framework/frontend/permissoes

Proteger páginas, componentes, menus e server actions pelas permissões do utilizador, a partir das claims do token.

# Permissões no Frontend (Next.js) [#permissões-no-frontend-nextjs]

**Destinatários:** developers a construir uma aplicação Next.js sobre o template iGRP que precisam de proteger **páginas**, **componentes**, **menus** e **server actions** pelas permissões do utilizador atual.

O gating lê as **claims do access token do IGRP** — **zero rede**. A API do Access Management continua a ser a aplicação *real* em cada chamada de dados; estes gates apenas moldam **aquilo que o utilizador vê**.

---

## O modelo [#o-modelo]

O access token transporta (descodificado do payload do JWT):

```jsonc
{
  "org": "DEPT_IGRP",                 // departamento ativo
  "selectedRole": "DEPT_IGRP.superadmin",
  "is_super_admin": true,             // contorna todas as verificações
  "resource_access": { "igrp-access-management": { "roles": ["DEPT_IGRP.superadmin"] } },
  "permissions": ["DEPT_IGRP.manage_access"]  // todos os departamentos; `${dept}.${sufixo}`
}
```

**Regra de correspondência** (`claimsAllow`):

1. `is_super_admin` → permite tudo.
2. Um `name` que **contém** `.` corresponde literalmente.
3. Um `name` **simples** é qualificado com o `org` ativo → `` `${org}.${name}` ``.
4. Um `name` simples **sem** `org` → **nega** (fail-closed).

---

## Como funciona (ciclo de vida do pedido) [#como-funciona-ciclo-de-vida-do-pedido]

<NextRequestPipeline />

Tudo lê daquele **único** objeto de claims descodificado — sem chamadas de rede extra. `igrpGetClaims()` é deduplicado por pedido (via `cache` do React).

<Callout type="note" title="Falha de descodificação não é «sem permissões»">
Em caso de falha na descodificação, `igrpGetClaims()` devolve um estado de **erro** distinguível — nunca um silencioso "este utilizador não tem permissões". É o que permite não confundir uma indisponibilidade com um 403 legítimo.
</Callout>

---

## Ligação inicial (uma só vez) [#ligação-inicial-uma-só-vez]

Uma aplicação consumidora configura isto **uma vez** — no template iGRP já está feito:

**1. Ativar `forbidden()`** em `next.config.ts`:

```ts
experimental: { authInterrupts: true }
```

**2. Semear o provider** no layout autenticado (`src/app/(igrp)/layout.tsx`):

```tsx
import { IGRPLayoutFull, igrpGetClaims } from "@igrp/framework-next";
import { IGRPSectionPermissions } from "@igrp/framework-next-ui";

await verifySession();                  // semeia o access token do pedido
const claims = await igrpGetClaims();
return (
  <IGRPSectionPermissions state={claims}>
    <IGRPLayoutFull config={config}>{children}</IGRPLayoutFull>
  </IGRPSectionPermissions>
);
```

<Callout type="warning" title="A ordem importa">
`verifySession()` tem de correr **antes** de `igrpGetClaims()` — é o que semeia o token de access management do pedido que `igrpGetClaims()` vai ler. Sem essa ordem, as claims resolvem para um estado de erro fora do modo preview.
</Callout>

**3. Fronteira de 403** em `src/app/(igrp)/forbidden.tsx`:

```tsx
import { IGRPForbidden } from "@igrp/framework-next-ui";
export default function Forbidden() { return <IGRPForbidden />; }
```

Depois disto, no dia-a-dia só escreves os gates abaixo.

---

## Proteger uma página (servidor — autoritativo) [#proteger-uma-página-servidor--autoritativo]

```tsx
// app/(igrp)/(demo)/invoices/page.tsx — server component
import { igrpAssertAuthorize } from "@igrp/framework-next";

export default async function InvoicesPage() {
  await igrpAssertAuthorize("manage_access"); // sem permissão → forbidden() → 403
  return <InvoicesScreen />;
}
```

Este é o verdadeiro gate de render: corre antes de o conteúdo ser transmitido e não pode ser contornado do lado do cliente.

- Permissão genuinamente ausente → `forbidden()` (403, dentro do chrome da aplicação).
- **Erro** de descodificação das claims → lança exceção (→ `error.tsx`, 5xx), para que uma indisponibilidade nunca seja rotulada como 403.

---

## Proteger um componente (cliente) [#proteger-um-componente-cliente]

### Envolver (padrão) [#envolver-padrão]

```tsx
"use client";
import { IGRPAuthorization } from "@igrp/framework-next-ui";
import { IGRPButton } from "@igrp/igrp-framework-react-design-system";

<IGRPAuthorization permission="delete_invoice" fallback={<IGRPButton disabled>Eliminar</IGRPButton>}>
  <IGRPButton variant="destructive">Eliminar</IGRPButton>
</IGRPAuthorization>
```

Negado → renderiza o `fallback` (default: nada — desmonta). A prop `permission` aceita um array com `mode="all"` (default) ou `mode="any"`.

### Hook [#hook]

Quando precisas de *desativar* em vez de *esconder*, ou de várias verificações:

```tsx
"use client";
import { usePermissions } from "@igrp/framework-next-ui";

const { isAllowed } = usePermissions();
<IGRPButton disabled={!isAllowed("read_invoice")}>Ver</IGRPButton>
```

<Callout type="tip" title="O design system é agnóstico de autenticação">
Nunca adiciones uma prop `permission` a um componente do design system. Protege-o a partir do consumidor, com `<IGRPAuthorization>` ou `usePermissions()`.
</Callout>

---

## Proteger uma server action [#proteger-uma-server-action]

O botão é cosmético — protege a operação real na sua server action:

```ts
"use server";
import { igrpAuthorize } from "@igrp/framework-next";

export async function deleteInvoice(id: string) {
  if (!(await igrpAuthorize("delete_invoice"))) return { ok: false, code: "forbidden" };
  // … e depois a API do Access Management aplica outra vez na chamada real
}
```

---

## Através do editor visual (Page Builder) [#através-do-editor-visual-page-builder]

Tudo o que foi descrito acima também está disponível **sem código**, diretamente no Page Builder do iGRP Studio. Seleciona qualquer nó na tela → separador **Interações** → **Regras** → `+` → escolhe **Permissão** (a par de **Visibilidade**, que é um gate por expressão booleana simples como `!isLoading` — não relacionado com permissões):

<StudioPreview imageName="frontendRulesMenu" alt="Separador Interações de uma tabela, com o menu de Regras a mostrar Visibilidade e Permissão" />

Isto abre o modal **Regra de permissão**: escolhes uma ou mais permissões em **Escolher permissões…** (bare suffix ou `dept.suffix`, a mesma regra da secção [Nomes de permissões](#nomes) acima) e escolhes a **Ação**:

<StudioPreview imageName="frontendPermissionRuleTable" alt="Regra de permissão sobre uma tabela, com a lista Ação aberta e Assert (só raiz) desativado" />

| Action no Studio | Equivalente em código | Comportamento |
| --- | --- | --- |
| **Ocultar** | `<IGRPAuthorization>` sem `fallback` | Nega → desmonta o nó (o default do componente) |
| **Desativar** | `usePermissions().isAllowed()` a controlar `disabled` | Nega → o nó permanece visível mas inativo |
| **Substituir por…** | `<IGRPAuthorization fallback={...}>` | Nega → renderiza um nó alternativo em vez de desmontar |
| **Assert (só raiz)** | `igrpAssertAuthorize()` | Nega → `forbidden()` → 403 |

<Callout type="note" title="Assert só existe na raiz">
No screenshot acima, sobre um componente de tabela, a opção **Assert (só raiz)** aparece desativada — cinzenta, sem ser clicável. Só é selecionável no **nó raiz** de uma página, de um componente ou de um passo de processo (o modal avisa: *Assert só está disponível na raiz de page, component ou processStep*), porque é aí que corresponde a `igrpAssertAuthorize()`: um gate de render que decide se a página inteira arranca ou devolve 403. Num componente aninhado (uma tabela, um botão) não faz sentido "abortar" só esse nó — por isso as únicas ações disponíveis são Ocultar, Desativar e Substituir por….
</Callout>

Ao selecionar o nó de página (não um componente lá dentro), a mesma ação já aparece disponível:

<StudioPreview imageName="frontendPermissionRuleAssert" alt="Regra de permissão sobre o nó da página, com a ação Assert (só raiz) selecionada" />

O Studio gera o código equivalente ao que descrevemos manualmente nas secções anteriores — a escolha entre editor visual e código é de conveniência, não de comportamento em runtime.

---

## Nomes de permissões e o departamento ativo [#nomes-de-permissões-e-o-departamento-ativo]

Passa o **sufixo simples** (`"manage_access"`); é auto-qualificado com o departamento ativo do utilizador (`org`), lido do token. Passa um nome totalmente qualificado (`"DEPT_X.manage_access"`) para uma verificação cruzada entre departamentos.

Um nome simples sem `org` no token é negado — *fail-closed*. `is_super_admin` contorna todas as verificações.

---

## Camadas de aplicação [#camadas-de-aplicação]

| Âmbito | Gate | Força |
| --- | --- | --- |
| Componente | `<IGRPAuthorization>` / `usePermissions` (token, cliente) | Cosmético — esconde/mostra UI |
| Menu | Scoping server-side do `getCurrentUserApplicationMenus` no Access Management | Navegação de confiança |
| Página | `igrpAssertAuthorize` → `forbidden()` (token, servidor) | Gate de render aplicado |
| Ação atrás de um controlo | `igrpAuthorize` na server action **+ a API do Access Management** | **Aplicação real** |

O gating por claims do token é rápido e conveniente; a API do Access Management é a fonte de verdade.

<Callout type="danger">
Um botão protegido com uma server action desprotegida atrás não é seguro.
</Callout>

---

## Modo preview [#modo-preview]

Com `IGRP_PREVIEW_MODE=true` (ou `AUTH_PROVIDER=none`), as claims passam a ser **super-admin**, pelo que todos os gates passam — ideal para construir UI sem backend.

Para ver um estado **negado** (o fallback desativado) ou a página **403**, corre com o preview **desligado** contra um backend real, com um utilizador que não tenha a permissão.

---

## Checklist por página (não há default-deny) [#checklist-por-página-não-há-default-deny]

- [ ] Esta página precisa de permissão? Se sim, chama `await igrpAssertAuthorize("<perm>")` no topo do server component.
- [ ] Esquecê-lo deixa a página **aberta** — um item de menu escondido é UX de navegação, **não** aplicação de segurança (o utilizador pode entrar por deep-link).
- [ ] Para mutações sensíveis, protege também a **server action** (`igrpAuthorize`) — o gate do componente é cosmético.
- [ ] Nome da permissão: passa o **sufixo simples** (`"manage_access"`); usa `dept.sufixo` só para uma verificação cruzada explícita entre departamentos.

---

## Referência de API [#referência-de-api]

| Símbolo | Package | Uso |
| --- | --- | --- |
| `igrpGetClaims()` | `@igrp/framework-next` | Resolve as claims do pedido (`IGRPClaimsState`); semeia o provider |
| `igrpAuthorize(name)` | `@igrp/framework-next` | Verificação booleana (server actions / lógica) |
| `igrpAssertAuthorize(name)` | `@igrp/framework-next` | Gate de página → `forbidden()` se negado |
| `IGRPSectionPermissions` | `@igrp/framework-next-ui` | Provider; semear com o resultado de `igrpGetClaims()` |
| `usePermissions()` | `@igrp/framework-next-ui` | `{ isAllowed, permissions, roles, selectedRole, isSuperAdmin, status, error }` |
| `<IGRPAuthorization>` | `@igrp/framework-next-ui` | Envolve um componente para mostrar/esconder por permissão |
| `<IGRPGuardPage>` | `@igrp/framework-next-ui` | Gate de página do lado do cliente (conveniência; o gate de servidor é o autoritativo) |
| `IGRPForbidden` | `@igrp/framework-next-ui` | UI de 403 para `forbidden.tsx` |
| `IGRPAccessClaims`, `IGRPClaimsState` | `@igrp/framework-next-types` | Tipos das claims (reexportados de `@igrp/framework-next-auth/claims`) |

---

## Limitações [#limitações]

- **Latência de revogação.** Os gates leem claims de um token em cache, pelo que uma *revogação* de permissão do lado do servidor só se reflete quando o token refresca (TTL curto) ou o utilizador volta a autenticar-se. Para uma operação que tenha de trancar no instante em que o acesso é retirado, aplica-a na API do Access Management, que está sempre fresca.
- **Não há default-deny.** Os gates por página são opt-in (ver a [checklist](#checklist)). Um `igrpAssertAuthorize` esquecido deixa a página aberta.
- **Troca de perfil ativo** (mudar de departamento/perfil a meio da sessão) ainda não está ligada no template — o perfil ativo é o que o token trazia no login.

---

## A seguir [#a-seguir]

- [Backend — Spring Boot](../backend/permissoes.mdx) — onde as permissões são declaradas e realmente aplicadas.
- [Access Manager](../../access-management/index.mdx) — gestão de recursos, perfis, departamentos e menus.


# Visão geral

URL: /pt/docs/developer/framework

O que um projeto gerado pelo Studio usa em runtime — as bibliotecas partilhadas de frontend (Next.js) e backend (Spring Boot) e o modelo de permissões comum.

O Studio gera o código; o **framework** é o que esse código usa quando corre. Vive em `shared/` no workspace e divide-se em dois lados — frontend e backend —, mais o Design System e um modelo de permissões transversal a ambos.

<Grid cols={2}>
  <Feature icon="Layout" color="green" title="Frontend (Next.js)" href="/docs/developer/framework/frontend">
    `@igrp/framework-next`, autenticação, UI e design system — a base de cada aplicação Next.js gerada.
  </Feature>
  <Feature icon="Server" color="blue" title="Backend (Spring Boot)" href="/docs/developer/framework/backend">
    `cv.igrp.framework:core` (CQRS, `CommandBus`/`QueryBus`), stereotypes, módulos de autenticação, ficheiros e mail.
  </Feature>
</Grid>

<Grid cols={2}>
  <Feature icon="Palette" color="purple" title="Design System" href="/docs/developer/framework/design-system">
    Os componentes React (`IGRP*`) que o Studio gera e que usas no código das aplicações.
  </Feature>
  <Feature icon="ShieldCheck" color="amber" title="Permissões" href="/docs/developer/framework/permissoes">
    Um único modelo de permissões, declarado no backend e consumido pelo frontend através das claims do token.
  </Feature>
</Grid>

<Callout type="info" title="Onde está o quê">
  Para **criar** projetos e páginas usa o [Studio](/docs/developer/studio/ide/page-manager); para os **executar** localmente usa o [Workspace](/docs/developer/workspace/criar-workspace). Esta secção descreve apenas o que o projeto gerado contém e como se estende.
</Callout>


# Modelo de permissões

URL: /pt/docs/developer/framework/permissoes

Como as permissões de negócio são declaradas, sincronizadas e aplicadas — do código Spring Boot ao gating da interface Next.js.

# Permissões no iGRP 3.0 [#permissões-no-igrp-30]

**Destinatários:** developers que precisam de proteger funcionalidades de negócio — no backend (API Spring Boot), no frontend (aplicação Next.js), ou em ambos.

O iGRP tem **um único modelo de permissões** partilhado por toda a plataforma. O Access Management é a fonte de verdade; o teu código declara as permissões que existem, e o JWT transporta as que foram concedidas ao utilizador.

Esta secção divide-se em dois guias, um por cada lado da stack:

| Guia | O que cobre |
| --- | --- |
| [Backend — Spring Boot](./backend/permissoes.mdx) | Declarar permissões com `@IgrpPermission`, sincronizar com o Access Management, proteger endpoints com `@PreAuthorize`. |
| [Frontend — Next.js](./frontend/permissoes.mdx) | Proteger páginas, componentes e server actions a partir das claims do token. |

---

## O ciclo de vida completo [#o-ciclo-de-vida-completo]

<PermissionsLifecycle />

Só a anotação é escrita à mão: o registo e a sincronização são automáticos. A concessão é uma decisão do administrador no Access Management, e os gates correm a cada pedido.

<Callout type="tip" title="O ponto essencial">
Só declaras permissões **num** lugar: o código Java do backend. O frontend consome os mesmos códigos de permissão pelas claims do token — não os redeclara.
</Callout>

---

## Camadas de aplicação [#camadas-de-aplicação]

Nem todas as verificações têm a mesma força. Esta tabela é a que mais interessa perceber antes de escrever qualquer gate:

| Âmbito | Mecanismo | Força |
| --- | --- | --- |
| Componente (frontend) | `<IGRPAuthorization>` / `usePermissions()` | **Cosmético** — mostra/esconde UI |
| Menu | Scoping server-side do Access Management | Navegação de confiança |
| Página (frontend) | `igrpAssertAuthorize()` → `forbidden()` | Gate de render aplicado |
| Server action (frontend) | `igrpAuthorize()` **+** API do Access Management | **Aplicação real** |
| Endpoint (backend) | `@PreAuthorize` + `IgrpAuthorizationService` | **Aplicação real** |

<Callout type="danger" title="Um botão escondido não é segurança">
Esconder um botão no frontend impede o utilizador de o *ver*, não de invocar a operação. Um controlo protegido com uma server action desprotegida atrás **não é seguro**. A aplicação real vive no backend e na API do Access Management.
</Callout>

---

## Anatomia do JWT [#anatomia-do-jwt]

Ambos os lados leem o mesmo token. As claims relevantes:

```jsonc
{
  "sub": "…",                          // id interno do utilizador (String, não garantidamente UUID)
  "org": "DEPT_IGRP",                  // departamento ativo
  "selectedRole": "DEPT_IGRP.superadmin",  // perfil ATIVO (não a união de todos)
  "is_super_admin": true,              // contorna todas as verificações
  "permissions": ["DEPT_IGRP.manage_access"]  // permissões concedidas, `${dept}.${sufixo}`
}
```

Duas consequências que causam confusão com frequência:

**`selectedRole` é singular.** Só o perfil *atualmente selecionado* é exposto como `ROLE_<code>`, não todos os perfis atribuídos ao utilizador. `hasRole('X')` avalia contra um perfil de cada vez — o que o utilizador escolheu no login. Sempre que possível, desenha as verificações contra **permissões**, não perfis: são mais finas e sobrevivem a renomeações e reorganizações.

**`is_super_admin` contorna tudo.** Quando é `true`, todas as verificações de permissão passam — nos dois lados. Ver [backend §8.3](./backend/permissoes-referencia.mdx#super-admin) para o detalhe e para saber como excluir um endpoint específico desse bypass.

---

## Nomenclatura das permissões [#nomenclatura-das-permissões]

O código canónico é uma string pontuada em minúsculas, com namespace do teu contexto de negócio:

```
my.invoice.create
hr.employee.view
```

No backend declaras o código completo. No frontend passas normalmente apenas o **sufixo** (`"manage_access"`), que é auto-qualificado com o departamento ativo (`org`) do token. Um nome sem ponto e sem `org` no token é **negado** — fail-closed.

---

## Latência de revogação [#latência-de-revogação]

Os gates leem claims de um token já emitido. Uma revogação no Access Management **não tem efeito imediato**:

- Backend: o `@PreAuthorize` continua a passar até o utilizador obter um novo JWT.
- Frontend: idem, até o token refrescar (TTL curto) ou novo login.

Para uma operação que tem de trancar no instante em que o acesso é retirado, a verificação tem de ser feita contra a API do Access Management, que está sempre fresca.

---

## Por onde começar [#por-onde-começar]

- Vais construir uma **API de negócio** que define as suas próprias permissões? → [Backend — Spring Boot](./backend/permissoes.mdx)
- Vais proteger **páginas e componentes** numa aplicação Next.js? → [Frontend — Next.js](./frontend/permissoes.mdx)
- Queres entender como o Access Management gere recursos, perfis e menus? → [Access Manager](../access-management/index.mdx)


# Configurações do Studio

URL: /pt/docs/developer/getting-started/configuracoes

Preferências da própria aplicação IGRP Studio — aparência, idioma, contas conectadas, atalhos e notificações. A configuração do workspace é tratada na secção Workspace.

Estas são as preferências da **aplicação IGRP Studio** (o IDE onde o workspace vive) — aparência, idioma, contas conectadas, atalhos e notificações. Não confundir com a configuração do **workspace** (serviços, portas, ambiente), descrita em [Workspace](/docs/developer/workspace/criar-workspace).

## Como aceder [#como-aceder]

Clica no ícone de **engrenagem** no canto superior direito do Studio (atalho `Ctrl+,`, ou `⌘ ,` no macOS).

<StudioPreview imageName="configStudio" alt="Ícone de engrenagem das Configurações na barra superior do IGRP Studio" />

## Opções disponíveis [#opções-disponíveis]

<Grid cols={3}>
  <Feature icon="Info" color="purple" title="Sobre">Versão instalada, atualizações e canal de atualizações</Feature>
  <Feature icon="Sparkles" color="cyan" title="Provedores de IA">Chaves de API e CLIs locais usados pelo assistente de IA</Feature>
  <Feature icon="Globe" color="blue" title="Idioma">Idioma da interface — Português ou English</Feature>
  <Feature icon="Link" color="green" title="Contas conectadas">Ligação ao GitHub e ao GitLab</Feature>
  <Feature icon="Palette" color="orange" title="Aparência">Cor de destaque da interface</Feature>
  <Feature icon="Keyboard" color="neutral" title="Atalhos">Atalhos de teclado, editáveis</Feature>
</Grid>

### Sobre [#sobre]

<Checklist>
  <Item title="Versão atual do IGRP Studio instalada" />
  <Item title="Verificar Atualização — procura uma versão mais recente" />
  <Item title="Canal de atualizações — Estável (só versões estáveis) ou Beta (inclui pré-lançamentos)" />
  <Item title="Ligações para obter a versão mais recente, termos de serviço e política de privacidade" />
</Checklist>

<StudioPreview imageName="sobreConfigStudio" alt="Separador Sobre das Configurações do IGRP Studio" />

### Provedores de IA [#provedores-de-ia]

Chaves e ferramentas usadas pelo assistente de IA do Studio. As chaves ficam cifradas no keychain do sistema operativo; variáveis de ambiente sobrepõem-se aos valores guardados (útil em desenvolvimento).

| Provedor | Para quê |
|---|---|
| **OpenRouter** | LLMs na cloud (Claude, GPT, Gemini, Llama…) usados pelo assistente |
| **OpenAI** | Embeddings semânticos da Knowledge Base |
| **Voyage** | Alternativa opcional para os embeddings |
| **CLIs locais** (Claude, Ollama) | Detetados automaticamente no `PATH`; define um caminho próprio se o binário estiver noutro sítio |

Cada chave tem **Save** e **Test connection**.

<StudioPreview imageName="iaConfigStudio" alt="Separador Provedores de IA das Configurações do IGRP Studio" />

### Idioma [#idioma]

Idioma da interface: <Badge color="blue" icon="Check">Português</Badge> <Badge outline>English</Badge>

<Callout type="info" title="Alguns ecrãs ainda só em inglês">
  O onboarding, o Doctor e partes dos assistentes ainda não estão traduzidos e aparecem em inglês mesmo com o Português selecionado.
</Callout>

<StudioPreview imageName="idiomaConfigStudio" alt="Separador Idioma das Configurações do IGRP Studio" />

### Contas conectadas [#contas-conectadas]

Liga o Studio aos teus repositórios Git:

| Serviço | |
|---|---|
| **GitHub** | **Conectar** a uma conta; **Adicionar GitHub** para mais contas |
| **GitLab NOSi** (padrão) | **Conectar**; **Adicionar GitLab** para outra instância |

<StudioPreview imageName="contasConfigStudio" alt="Separador Contas Conectadas das Configurações do IGRP Studio" />

### Notificações [#notificações]

Reservado para as preferências de notificações da aplicação — ainda sem opções configuráveis.

<StudioPreview imageName="notifyConfigStudio" alt="Separador Notificações das Configurações do IGRP Studio" />

### Aparência [#aparência]

Cor de destaque da interface — `default`, Red, Rose, Orange, Igrp, Green, Blue, Yellow ou Violet. A cor aplica-se de imediato. O tema claro/escuro alterna-se no ícone de sol/lua da barra superior.

<StudioPreview imageName="aparenciaConfigStudio" alt="Separador Aparência das Configurações do IGRP Studio" />

### Atalhos de teclado [#atalhos-de-teclado]

Os atalhos usam `Ctrl` no Windows e Linux e `⌘` no macOS. Clica num atalho para o editar; **Redefinir para o padrão** repõe todos.

| Endpoints | Atalho |
|---|---|
| Novo Endpoint | `Ctrl+E` |
| Novo Esquema | `Ctrl+M` |
| Novo Contract | `Ctrl+O` |
| Novo Projeto | `Ctrl+N` |
| Salvar | `Ctrl+S` |
| Eliminar | `Delete` |

| Abas | Atalho |
|---|---|
| Fechar aba | `Ctrl+W` |
| Forçar saída da aba | `Ctrl+Alt+W` |
| Próxima aba | `Ctrl+Alt+→` ou `Ctrl+Shift+]` |
| Aba anterior | `Ctrl+Alt+←` ou `Ctrl+Shift+[` |
| Ir para aba específica | `Ctrl+1–8` |
| Ir para a última aba | `Ctrl+9` |

| Editor de código | Atalho |
|---|---|
| Localizar | `Ctrl+F` |
| Substituir | `Ctrl+Alt+F` |

| Geral | Atalho |
|---|---|
| Alternar barra lateral | `Ctrl+B` |
| Configurações | `Ctrl+,` |
| Quebra de linha | `↵` ou `⇧↵` |
| Aumentar / reduzir zoom | `Ctrl+=` / `Ctrl+-` |
| Abrir ajuda de atalhos | `Ctrl+/` |

<StudioPreview imageName="atalhoConfigStudio" alt="Separador Atalhos das Configurações do IGRP Studio" />


# Instalar o Studio

URL: /pt/docs/developer/getting-started/instalacaoStudio

Instala o IGRP Studio com o instalador do teu sistema operativo ou corre-o a partir do código-fonte no GitHub (open source).

O **IGRP Studio** é o IDE onde se criam e gerem os projetos e onde vive o workspace. Há dois caminhos para o ter — escolhe um:

<Grid cols={2}>
  <Feature icon="Download" color="green" title="Instalador" eyebrow="Recomendado">
    Executável para o teu sistema operativo. Atualiza-se sozinho.
  </Feature>
  <Feature icon="Github" color="neutral" title="A partir do código-fonte" eyebrow="Open source">
    Clona o repositório e corre o Studio em modo de desenvolvimento — para contribuir ou personalizar.
  </Feature>
</Grid>

<Callout type="info" title="Antes de começar">
  Confirma os [pré-requisitos](/docs/developer/getting-started/pre-requisitos) — sobretudo a versão do Node.js.
</Callout>

## Instalador [#instalador]

<DownloadsSection />

## A partir do código-fonte [#a-partir-do-código-fonte]

O Studio é open source: [NOSiCode-CV/igrp-studio-ide](https://github.com/NOSiCode-CV/igrp-studio-ide).

<Callout type="warning" title="Node.js">
  O modo de desenvolvimento exige Node.js `^20.19`, `^22.13`, `^23.5` ou `≥ 24` — por exemplo, `22.12` é recusado. Confirma com `node -v`.
</Callout>

<Steps>
  <Step>
    **Clona o repositório**

    ```bash
    git clone https://github.com/NOSiCode-CV/igrp-studio-ide
    cd igrp-studio-ide
    ```
  </Step>
  <Step>
    **Instala as dependências**

    ```bash
    yarn
    ```

    A primeira execução demora alguns minutos; avisos durante a instalação podem ser ignorados.
  </Step>
  <Step>
    **Corre o Studio**

    ```bash
    yarn dev
    ```
  </Step>
</Steps>

<Callout type="info" title="Vais contribuir? Faz um fork">
  Para propor alterações, cria primeiro um **fork** de [NOSiCode-CV/igrp-studio-ide](https://github.com/NOSiCode-CV/igrp-studio-ide) na tua conta GitHub e clona o fork em vez do repositório original. Mantém o original como `upstream` para receberes as atualizações:

  ```bash
  git clone https://github.com/<o-teu-utilizador>/igrp-studio-ide
  cd igrp-studio-ide
  git remote add upstream https://github.com/NOSiCode-CV/igrp-studio-ide
  ```

  Trabalha numa branch, faz push para o teu fork e abre um pull request para o repositório original — ver o [Guia de Contribuição](/docs/developer/reference/contributing).
</Callout>

<NextStep href="/docs/developer/getting-started/primeiro-arranque" title="Primeiro arranque">
  Abre o Studio pela primeira vez: verificação do ambiente e instalação do CLI.
</NextStep>


# Pré-requisitos

URL: /pt/docs/developer/getting-started/pre-requisitos

As ferramentas, versões e acessos de rede que o teu computador precisa antes de instalar o IGRP Studio, e como confirmar cada um.

Antes de instalar o Studio, confirma que o teu computador tem estas ferramentas. Não precisas de verificar tudo à mão: no [primeiro arranque](/docs/developer/getting-started/primeiro-arranque) o **Doctor** do Studio analisa o ambiente e mostra o que está instalado, o que falta e o link de download de cada ferramenta.

<Callout type="objective" title="No fim desta página">
  Tens o Node.js, os gestores de pacotes, o JDK 26, o Docker a correr e o Git — e sabes confirmar cada um no terminal.
</Callout>

## Ferramentas [#ferramentas]

| Ferramenta | | Versão | Para quê |
|---|---|---|---|
| Node.js | <Badge color="red">Obrigatório</Badge> | `22.13+` ou `24+` | Studio, CLI e projetos frontend |
| yarn | <Badge color="red">Obrigatório</Badge> | — | Gestor de pacotes que o Doctor do Studio exige |
| pnpm | <Badge color="red">Obrigatório</Badge> | — | Gestor de pacotes que o `igrp doctor` (CLI) exige |
| Java (JDK) | <Badge color="red">Obrigatório</Badge> | `26` | Projetos backend (Spring Boot) — o `pom.xml` gerado compila para Java 26 |
| Docker | <Badge color="red">Obrigatório</Badge> | Compose v2 | Serviços do workspace (`docker compose`); o daemon tem de estar a correr |
| Git | <Badge color="red">Obrigatório</Badge> | — | Controlo de versões |
| `@igrp/cli` | <Badge color="red">Obrigatório</Badge> | — | Instala-se no [primeiro arranque](/docs/developer/getting-started/primeiro-arranque), não agora |
| npm | <Badge>Incluído</Badge> | — | Vem com o Node.js; é com ele que se instala o CLI |
| Python + MarkItDown | <Badge>Opcional</Badge> | `3.10+` | Conversor de documentos para Markdown do Studio |

Não precisas de instalar o **Maven**: os projetos backend gerados trazem o *Maven Wrapper* (`mvnw`), que descarrega a versão certa na primeira compilação.

## Confirmar no terminal [#confirmar-no-terminal]

O Doctor usa estes mesmos comandos. Se algum falhar no terminal, também falha no Studio.

```bash
node -v              # v22.13 ou superior (ou v24+)
yarn -v
pnpm -v
java --version       # 26
docker info          # sem erro = daemon a correr
docker compose version
git --version
python3 --version    # opcional — 3.10 ou superior
```

<Callout type="warning" title="Abre um terminal novo depois de instalar">
  O Studio e o terminal só veem uma ferramenta acabada de instalar depois de reabertos (o `PATH` é lido no arranque). Se o Doctor continuar a dar uma ferramenta como em falta, fecha e volta a abrir o Studio.
</Callout>

## Notas por ferramenta [#notas-por-ferramenta]

<Accordions>
  <Accordion title="Node.js — uma única versão para tudo">
    `22.13+` (ou `24+`) satisfaz o Studio e o CLI (`@igrp/cli` exige `22+`). Evita `22.12` ou anterior: o Studio em modo de desenvolvimento recusa-a. Um gestor de versões (nvm, nvm-windows) facilita ter a versão certa e trocar entre projetos.
  </Accordion>
  <Accordion title="yarn e pnpm — instala os dois">
    O Doctor do Studio exige o **yarn** e o `igrp doctor` exige o **pnpm**; com os dois instalados, ambas as verificações passam. Com o Node.js já instalado:

    ```bash
    npm install -g yarn pnpm
    ```
  </Accordion>
  <Accordion title="Java — exatamente o JDK 26">
    Os projetos gerados usam Spring Boot 3.5 e compilam para Java 26. Instala o **JDK 26** (não chega o JRE) e confirma que `java --version` mostra 26 — se tiveres várias versões, o `JAVA_HOME` e o `PATH` têm de apontar para o 26. Não uses ainda o JDK 27: o Lombok incluído no Spring Boot 3.5 não compila nele.
  </Accordion>
  <Accordion title="Docker — tem de estar a correr">
    O Studio levanta os serviços do workspace com `docker compose` (Compose v2, o plugin — não o antigo `docker-compose`). Não basta estar instalado: o Doctor corre `docker info`, que só responde com o Docker Desktop (Windows e macOS) ou o serviço do Docker Engine (Linux) a correr.
  </Accordion>
  <Accordion title="Python + MarkItDown — só para converter documentos">
    Só é preciso para o conversor de Markdown do Studio. Com o Python 3.10+ instalado (comando `python3`):

    ```bash
    pip install "markitdown[all]"
    ```
  </Accordion>
</Accordions>

## Acesso à rede [#acesso-à-rede]

O Studio e os projetos gerados descarregam pacotes e templates dos repositórios da plataforma. O teu computador tem de chegar a:

| Endereço | Para quê |
|---|---|
| `sonatype.nosi.cv` | Pacotes `@igrp` (npm), o CLI, templates do workspace e dependências Maven do framework |
| `storage-api.nosi.cv` | Atualizações automáticas do Studio |
| `repo.maven.apache.org` | Maven, descarregado pelo `mvnw` na primeira compilação |

Não precisas de credenciais para nenhum destes endereços. Se estiveres atrás de um proxy, configura-o no npm e no Maven antes de continuar — o Studio não tem configuração de proxy própria.

## Por sistema operativo [#por-sistema-operativo]

<OSTabs />

<NextStep href="/docs/developer/getting-started/instalacaoStudio" title="Instalar o Studio">
  Com as ferramentas instaladas, instala o IGRP Studio.
</NextStep>


# Primeiro arranque

URL: /pt/docs/developer/getting-started/primeiro-arranque

Abrir o IGRP Studio pela primeira vez — verificar o ambiente com o Doctor e instalar o CLI (@igrp/cli) a partir do próprio Studio ou do terminal.

Na primeira vez que abres o Studio, o ecrã de boas-vindas termina no passo **Prepare your environment**. A caixa **Setup Requirements** tem duas ações: **Validate with Doctor** e **Install `@igrp/cli`**. No fim, **Enter Studio Workspace** leva-te ao Studio — e, se ainda não tiveres nenhum workspace, abre o assistente para [criar o primeiro](/docs/developer/workspace/criar-workspace).

<StudioPreview imageName="onboardingStudio" alt="Passo Prepare your environment do onboarding, com Setup Requirements e Enter Studio Workspace" />

## Verificar o ambiente (Doctor) [#verificar-o-ambiente-doctor]

**Validate with Doctor** abre o *System Health Check*, integrado no Studio — não precisa do CLI. Verifica as ferramentas dos [pré-requisitos](/docs/developer/getting-started/pre-requisitos) e mostra, por categoria, o que está instalado, em falta ou com erro, com a versão detetada e o link de download do que faltar.

| Categoria | Verifica |
|---|---|
| Frontend | Node.js, yarn (obrigatórios); npm, pnpm (opcionais) |
| Backend | Java (obrigatório) |
| Infraestrutura | Docker, Git, `@igrp/cli` (obrigatórios); Python + MarkItDown (opcionais) |

<Callout type="warning" title="Studio 0.3.0-beta.4: o Doctor marca o Java 26 como erro">
  Os projetos backend gerados compilam para **Java 26**, por isso é essa a versão a instalar. No Studio **0.3.0-beta.4** o Doctor ainda só aceita Java `23` e `24` e mostra o JDK 26 como erro — podes ignorar esse aviso. A próxima versão do Studio já aceita Java 26 ou superior.
</Callout>

<StudioPreview imageName="doctorStudio" alt="System Health Check do IGRP Studio com as ferramentas de Frontend Development" />

Depois do primeiro arranque, o Doctor continua disponível no ícone de estetoscópio da barra inferior do Studio.

## Instalar o CLI [#instalar-o-cli]

O **`@igrp/cli`** (comando `igrp`) cria projetos, gera artefactos e abre o Studio a partir do terminal. Escolhe uma das formas:

<Tabs>
  <TabItem value="studio" label="No Studio">
    No passo **Prepare your environment**, clica em **Install `@igrp/cli`** — o Studio corre a instalação e mostra *CLI installed successfully* (ou **Retry**, se falhar). Se ignorares este passo, o Studio volta a sugerir a instalação com a notificação *IGRP CLI não instalado* → **Instalar** — e, mais tarde, avisa quando houver uma versão nova (*Atualização do IGRP CLI disponível* → **Atualizar**).
  </TabItem>
  <TabItem value="terminal" label="No terminal">
    O Studio executa exatamente este comando:

    ```bash
    npm install -g @igrp/cli --registry=https://sonatype.nosi.cv/repository/npm-group/
    ```
  </TabItem>
</Tabs>

Confirma a instalação:

```bash
igrp --version
igrp doctor
```

O `igrp doctor` faz no terminal a mesma verificação do Doctor do Studio. Os restantes comandos estão em [CLI](/docs/developer/studio/cli).

<NextStep href="/docs/developer/workspace/criar-workspace" title="Criar workspace">
  Com o ambiente pronto, cria o workspace onde vão viver a plataforma e os teus projetos.
</NextStep>

As preferências do próprio Studio (idioma, aparência, atalhos) estão em [Configurações do Studio](/docs/developer/getting-started/configuracoes).


# Visão geral

URL: /pt/docs/developer/introducao

O que é o iGRP 3.0 (Horizon) — a arquitetura, o padrão BFF e os valores que orientam a plataforma — e o percurso do primeiro dia.

<VideoCard youtubeUrl="https://www.youtube.com/watch?v=tqJN0u9T98g" />

O **iGRP 3.0 — Horizon** é a plataforma *low-code / no-code* para construir aplicações digitais robustas, pensada sobretudo para governação digital e sistemas empresariais. O **Studio** gera o código; o **framework** e os serviços da plataforma (acessos, processos, notificações) fazem-no correr.

<Grid cols={3}>
  <Feature icon="Rocket" color="green" title="Como começar" href="#como-começar">
    Pré-requisitos, instalação do Studio e primeiro workspace — o caminho do primeiro dia.
  </Feature>
  <Feature icon="Blocks" color="blue" title="Studio" href="/docs/developer/studio/ide/page-builder">
    Page Builder, Model Designer e CLI: a ferramenta que gera as aplicações.
  </Feature>
  <Feature icon="Layers" color="purple" title="Framework" href="/docs/developer/framework">
    O que uma aplicação gerada usa em runtime, no frontend e no backend.
  </Feature>
</Grid>

## Como começar [#como-começar]

Tudo começa no **IGRP Studio**: é nele que vive o workspace, e é a partir dele que se instala o CLI. O Studio corre localmente no teu computador — ambiente completo, debugging real e funciona offline.

<Steps>
  <Step>
    ### Pré-requisitos [#pré-requisitos]
    Node.js, yarn, Java, Docker e Git nas versões certas — [ver pré-requisitos](/docs/developer/getting-started/pre-requisitos).
  </Step>
  <Step>
    ### Instalar o Studio [#instalar-o-studio]
    Com o instalador do teu sistema ou a partir do código-fonte no GitHub — [ver instalação](/docs/developer/getting-started/instalacaoStudio).
  </Step>
  <Step>
    ### Primeiro arranque [#primeiro-arranque]
    O Studio verifica o ambiente com o Doctor e instala o CLI (`@igrp/cli`) com um clique — [ver primeiro arranque](/docs/developer/getting-started/primeiro-arranque).
  </Step>
  <Step>
    ### Configurações do Studio [#configurações-do-studio]
    Idioma, aparência, contas ligadas e atalhos — [ver configurações](/docs/developer/getting-started/configuracoes).
  </Step>
  <Step>
    ### Criar o primeiro workspace [#criar-o-primeiro-workspace]
    Levanta a plataforma localmente e cria o teu primeiro projeto — [criar workspace](/docs/developer/workspace/criar-workspace).
  </Step>
</Steps>

## Arquitetura Horizon [#arquitetura-horizon]

Quatro princípios definem a forma como a plataforma é construída e como as aplicações geradas se organizam:

<Grid cols={4}>
  <Feature icon="Boxes" color="blue" title="Microservices">
    As funcionalidades de negócio vivem em serviços independentes, desenvolvidos, atualizados e escalados de forma autónoma.
  </Feature>
  <Feature icon="Plug" color="green" title="API-First">
    Tudo comunica por APIs, o que torna simples integrar novos serviços ou canais — web, mobile, bots.
  </Feature>
  <Feature icon="Cloud" color="orange" title="Cloud-Native">
    Serviços desenhados para a nuvem: escalabilidade automática, tolerância a falhas e aprovisionamento flexível.
  </Feature>
  <Feature icon="LayoutTemplate" color="purple" title="Headless">
    A apresentação está desacoplada do negócio, permitindo experiências diferentes por canal e dispositivo.
  </Feature>
</Grid>

Em conjunto, dão um sistema flexível e preparado para evoluir sem o bloqueio de uma plataforma monolítica. A implementação concreta — serviços, stacks e fluxos de pedidos — está descrita em [Plataforma em Docker](/docs/developer/workspace/plataforma) e em [Stack em Kubernetes](/docs/developer/cloud).

## Backend for Frontend (BFF) [#backend-for-frontend-bff]

Cada tipo de frontend tem uma camada de backend própria, que agrega as respostas dos microserviços e as adapta a esse canal.

<Grid cols={3}>
  <Feature eyebrow="1" title="Foco no frontend">
    Cada BFF responde às necessidades de uma aplicação web, móvel ou de outro canal específico.
  </Feature>
  <Feature eyebrow="2" title="Menor acoplamento">
    O BFF é a ponte entre o frontend e os microserviços; o frontend não precisa de os conhecer.
  </Feature>
  <Feature eyebrow="3" title="Escalabilidade">
    Novos canais ganham BFFs dedicados sem alterar as outras interfaces.
  </Feature>
</Grid>

<Callout type="tip" title="Na prática">
  Menos complexidade nas equipas de frontend e menos *overfetching* / *underfetching* de dados.
</Callout>

## Valores do manifesto [#valores-do-manifesto]

<Grid cols={4}>
  <Feature icon="Lock" color="red" title="No lock-in">
    Aplicações abertas, sem dependências tecnológicas excessivas — migráveis e evolutivas.
  </Feature>
  <Feature icon="Zap" color="amber" title="Não opinativo">
    Liberdade técnica e metodológica para a abordagem que melhor serve cada projeto.
  </Feature>
  <Feature icon="Code2" color="green" title="Geração de código">
    O Studio gera a estrutura e o código repetitivo; a equipa concentra-se no negócio.
  </Feature>
  <Feature icon="Share2" color="purple" title="Composição e reutilização">
    Componentes modulares partilháveis entre projetos.
  </Feature>
  <Feature icon="Layers" color="teal" title="Low-code / no-code">
    Interfaces visuais que aproximam analistas e developers.
  </Feature>
  <Feature icon="Workflow" color="cyan" title="Flexibilidade">
    Acompanha o crescimento e a mudança sem grandes refatorações.
  </Feature>
  <Feature icon="Gauge" color="orange" title="Performance">
    Aplicações rápidas e leves, com boa experiência de utilizador.
  </Feature>
  <Feature icon="Shield" color="neutral" title="Segurança">
    Conformidade com padrões regulatórios e requisitos institucionais, por defeito.
  </Feature>
</Grid>

<NextStep href="/docs/developer/getting-started/pre-requisitos" title="Pré-requisitos">
  Começa por confirmar as ferramentas do teu computador.
</NextStep>


# Monitorização — Notifications UI

URL: /pt/docs/developer/notification/admin-ui

A app igrp-notifications-ui — definições por aplicação, templates, histórico de envios e inbox; o que configurar no Access Management para a pôr a funcionar.

# Monitorização — Notifications UI [#monitorização--notifications-ui]

A `igrp-notifications-ui` é uma app Next.js do iGRP Framework para gerir o Notification Service sem chamar a API à mão. Cada página trabalha sobre uma **aplicação** do Access Management, escolhida no campo *Aplicação* (e guardada em `?applicationCode=` no URL).

| Menu | Rota | Para quê |
|---|---|---|
| Inbox | `/inbox`, `/inbox/[id]` | Notificações IN_APP do utilizador autenticado |
| Envios | `/notifications`, `/notifications/[id]` | Histórico de envios da aplicação e estado de cada entrega |
| Templates | `/templates`, `/templates/novo`, `/templates/[id]`, `/templates/[id]/editor` | Criar, editar, publicar e arquivar templates |
| Definições | `/settings` | Canais, callbacks, retenção, remetentes e quotas da aplicação |

---

## Pôr a consola a funcionar [#pôr-a-consola-a-funcionar]

1. **Registar a app no Access Management.** Cria uma aplicação (por exemplo `IGRP_NOTIFICATIONS`, slug `/apps/notifications`) e usa esse código em `IGRP_APP_CODE`.
2. **Sincronizar os menus.** Os menus (Inbox, Envios, Templates, Definições) estão no código. Com `IGRP_SYNC_ACCESS=true` e `IGRP_SYNC_ON_CODE_MENUS=true` a consola cria-os na aplicação no arranque; sem isso a barra lateral mostra *Aplicação sem menus*.
3. **Registar o callback de login.** O cliente OAuth usado no login tem de aceitar `<origem><basePath>/api/auth/callback/igrp-auth`; senão o Access Management responde 401 no `/oauth2/authorize`.
4. **Dar permissões a quem a usa.** Templates, definições, destinatários e recorrências exigem `igrp.notification.admin`; o histórico de envios, `igrp.notification.read`; a inbox, `igrp.notification.inbox`. O serviço regista estas permissões no Access Management quando arranca.
5. **Ligar o serviço ao Access Management.** Para verificar se o utilizador tem acesso à aplicação escolhida, o Notification Service precisa de `notification.access-management.enabled=true` (com `base-url`, `client-id`, `client-secret`). Sem isso as páginas mostram `ACCESS_MANAGER_UNAVAILABLE`.

---

## Definições [#definições]

Por aplicação: canais permitidos e ordem preferida (para `channelStrategy=PREFERRED`), URL e domínios de callback, retenção (ISO 8601, por exemplo `P90D`), remetentes e quotas (`limit` por `HOUR`, `DAY` ou `MONTH`, opcionalmente por canal). Um canal fora de *Canais permitidos* é recusado no envio (`CHANNEL_NOT_ENABLED`).

<StudioPreview imageName="ntDefinicoes" alt="Definições da aplicação registos: canais permitidos EMAIL e IN_APP, ordem preferida EMAIL e IN_APP, callbacks vazios, retenção PT2160H e um sender EMAIL noreply@registos.gov.cv" />

<Callout type="warning" title="Remetentes ainda não chegam ao email">
Os remetentes ficam guardados, mas o provider SMTP envia sempre com o `from` da sua configuração (`notification.providers.email-smtp.from`). A retenção é guardada como duração normalizada (`P90D` aparece como `PT2160H`).
</Callout>

---

## Templates [#templates]

A lista mostra os templates da aplicação com código, idioma, versão e estado. *Novo template* abre o assistente; o lápis abre o template e o ícone de arquivo arquiva-o.

<StudioPreview imageName="ntTemplatesLista" alt="Lista de templates da aplicação registos com pedido-confirmacao, pt-CV, versão 1, Publicado" />

O passo a passo de criação (metadados → ponto de partida → editor → publicar) está em [Templates › Criar na consola](./templates#criar-na-consola).

Na página do template editas os metadados e o corpo em três vistas: **Código** (HTML), **WYSIWYG** e **Visual** (o editor por blocos):

<StudioPreview imageName="ntTemplateDetalhe" alt="Página Editar template de pedido-confirmacao com código, canal EMAIL, idioma Português (Cabo Verde), assunto e título, e o corpo aberto na vista Visual" />

| Ação | O que faz |
|---|---|
| **Guardar** num `DRAFT` | Atualiza o rascunho (`PUT /templates/{id}`) |
| **Guardar** num `PUBLISHED`/`ARCHIVED` | Cria um `DRAFT` na versão seguinte (`POST /templates`) |
| **Publicar** | Publica o rascunho e arquiva as outras versões publicadas do mesmo código, canal e locale |
| **Arquivar** | `POST /templates/{id}/archive` |

---

## Envios [#envios]

O histórico da aplicação, com pesquisa por `businessKey` e filtros (categoria, prioridade, canal, estado, datas). Cada linha abre o detalhe ou reenvia.

<StudioPreview imageName="ntEnviosLista" alt="Histórico de envios da aplicação registos com duas notificações do pedido PED-2026-0456: pedido-analise Entregue e pedido-confirmacao Enviado" />

O detalhe mostra os dados do pedido (aplicação, estratégia, template, locale, chave de idempotência), uma linha por **entrega** com o estado, destinatário, tentativas e erro, e o conteúdo renderizado. *Reenviar* chama `POST /notifications/{id}/resend`.

<StudioPreview imageName="ntEnvioDetalhe" alt="Detalhe do envio PED-2026-0456: aplicação REGISTOS, canal EMAIL, template pedido-confirmacao pt-CV, entrega para maria.silva@exemplo.cv com estado Enviado e assunto Pedido PED-2026-0456 recebido" />

O email do exemplo, tal como chega ao destinatário (aqui, numa caixa de testes):

<StudioPreview imageName="ntMailpit" alt="Email Pedido PED-2026-0456 recebido no Mailpit, com o texto Olá Maria Silva e o número do pedido a negrito" />

---

## Inbox [#inbox]

As notificações IN_APP do **utilizador autenticado** (lidas com o token dele, não M2M): pesquisa, filtros, marcar como lida, marcar todas e arquivar.

<StudioPreview imageName="ntInbox" alt="Inbox com 1 não lida: Pedido PED-2026-0456, assunto Novo pedido para analisar, categoria pedido-analise, estado Não lida" />

---

## Variáveis de ambiente [#variáveis-de-ambiente]

```bash
# Login (igrp-auth)
AUTH_PROVIDER=igrp-auth
IGRP_AUTH_ISSUER=https://<gateway>/igrp-access-management
IGRP_AUTH_CLIENT_ID=<cliente OAuth>
IGRP_AUTH_CLIENT_SECRET=<segredo>
NEXTAUTH_URL=https://<host>/apps/notifications/api/auth
NEXTAUTH_SECRET=<segredo>

# Aplicação e Access Management
IGRP_APP_CODE=IGRP_NOTIFICATIONS
NEXT_PUBLIC_IGRP_APP_CODE=IGRP_NOTIFICATIONS
IGRP_ACCESS_MANAGEMENT_API=https://<gateway>/igrp-access-management
IGRP_M2M_CLIENT_ID=<cliente M2M>
IGRP_M2M_CLIENT_SECRET=<segredo>
IGRP_SYNC_ACCESS=true
IGRP_SYNC_ON_CODE_MENUS=true

# Notification Service (sem /api/v1)
IGRP_NOTIFICATION_API=https://<gateway>/igrp-notification-service

# Base path
NEXT_PUBLIC_BASE_PATH=/apps/notifications
```

`NOTIFICATION_SERVICE_BASE_URL` também é aceite em vez de `IGRP_NOTIFICATION_API`. `NEXTAUTH_URL` inclui o base path, e o callback registado no cliente OAuth é `<NEXTAUTH_URL>/callback/igrp-auth`.

Com `IGRP_PREVIEW_MODE=true` a consola arranca sem login real, com menus e utilizador de exemplo.


# API REST

URL: /pt/docs/developer/notification/api

Endpoints REST do iGRP Notification Service — envio, inbox, anexos, templates, destinatários, recorrências, configuração e administração, com as permissões de cada um.

# API REST [#api-rest]

| | |
|---|---|
| **Base** | `<base do serviço>/api/v1` — no workspace do Studio: `http://<slug>:<porta>/gateway-api/igrp-notification-service/api/v1` |
| **Referência** | [Gerada do OpenAPI do serviço](./referencia-api/notificacoes-e-lotes.mdx) (`/v3/api-docs` · Swagger UI em `/swagger-ui.html`) |
| **Autenticação** | `Authorization: Bearer <jwt>` emitido pelo Access Management, exceto health/info, OpenAPI, Swagger e `/webhooks/**` |
| **GraphQL** | também disponível (consultas de notificações e templates) |

Os clientes [TypeScript](./frontend-client) e [Java](./java-client) cobrem todos estes endpoints.

---

## Permissões e aplicação [#permissões-e-aplicação]

Cada endpoint exige uma permissão, lida das claims do token:

| Permissão | Para quê |
|---|---|
| `igrp.notification.send` | Enviar, reenviar, anexar, cancelar lotes |
| `igrp.notification.read` | Consultar notificações, lotes, anexos, estatísticas |
| `igrp.notification.inbox` | Inbox do próprio utilizador |
| `igrp.notification.admin` | Templates, destinatários, grupos, recorrências, configuração, RGPD |
| `igrp.notification.manual_status` | Mudar à mão o estado de uma entrega OFFLINE |

Os pedidos pertencem a uma **aplicação**. O serviço usa o claim `application_code` do token; sem ele, usa o `applicationCode` do pedido (query nas leituras, corpo nas escritas). Erros: `400 APPLICATION_CODE_REQUIRED`, `403 APPLICATION_ACCESS_DENIED`, `422 ACCESS_MANAGER_UNAVAILABLE`.

Os envios (`POST /notifications`, bulk e CSV) são **M2M**: token `client_credentials`.

---

## Referência [#referência]

Cada endpoint, com a permissão, os parâmetros, o corpo do pedido, as respostas e exemplos em várias linguagens, está na referência gerada a partir do OpenAPI do serviço:

<Cards>
  <Card title="Notificações e lotes" href="/docs/developer/notification/referencia-api/notificacoes-e-lotes">Enviar, listar, reenviar, envios em massa e lotes.</Card>
  <Card title="Anexos" href="/docs/developer/notification/referencia-api/anexos">Carregar e descarregar anexos.</Card>
  <Card title="Inbox" href="/docs/developer/notification/referencia-api/inbox">Notificações IN_APP do utilizador autenticado.</Card>
  <Card title="Templates" href="/docs/developer/notification/referencia-api/templates">Criar, publicar, arquivar e pré-visualizar.</Card>
  <Card title="Destinatários e grupos" href="/docs/developer/notification/referencia-api/destinatarios-e-grupos">Contactos, preferências, opt-outs e grupos.</Card>
  <Card title="Recorrências" href="/docs/developer/notification/referencia-api/recorrencias">Envios periódicos e execuções.</Card>
  <Card title="Entregas e estatísticas" href="/docs/developer/notification/referencia-api/entregas-e-estatisticas">Estado manual OFFLINE e contagens.</Card>
  <Card title="Configuração e administração" href="/docs/developer/notification/referencia-api/configuracao-e-administracao">Definições da aplicação, RGPD e retenção.</Card>
  <Card title="Webhooks dos providers" href="/docs/developer/notification/referencia-api/webhooks-dos-providers">Recibos dos providers, sem token.</Card>
</Cards>

<Callout type="info" title="Testar no playground">
O botão **Testar no playground** envia o pedido a partir do browser para o servidor indicado (por omissão, o gateway do workspace em `localhost:2575`). Preenche o `host` do teu ambiente e um token em **Autorização**. O gateway tem de aceitar pedidos (CORS) vindos da origem da documentação; se não aceitar, usa os exemplos `curl`.
</Callout>

---

## Corpo do envio [#corpo-do-envio]

O corpo de `POST /notifications` (e dos envios em massa):


| Campo | Descrição |
|---|---|
| `channelStrategy` | **Obrigatório.** `SPECIFIC`, `ALL` ou `PREFERRED` |
| `channels` | `EMAIL`, `SMS`, `WHATSAPP`, `PUSH`, `IN_APP`, `OFFLINE` |
| `recipients` | `[{ userId?, email?, phone?, pushToken? }]` |
| `groups` | `[{ groupId \| departmentCode \| roleCode \| permissionName }]` — resolvidos no envio |
| `template` | `{ code, locale? }` — template publicado |
| `inline` | `{ subject?, title?, body? }` — em vez de `template` |
| `variables` | Valores das variáveis do template |
| `priority` | `LOW`, `NORMAL`, `HIGH`, `URGENT` |
| `attachments` | `[{ filename, contentRef }]` |
| `idempotencyKey` | Evita envios duplicados |
| `businessKey`, `category`, `correlationId` | Rastreio e agrupamento |
| `locale`, `scheduledAt`, `expiresAt` | Opcionais |
| `callbackUrl` | HTTPS, num domínio registado nas definições da aplicação |

---

## Exemplo: email com template [#exemplo-email-com-template]

<Callout type="warning" title="EMAIL precisa de SMTP configurado">
Para enviar por **EMAIL**, o provider SMTP tem de estar configurado no Notification Service (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`: servidor, porta, credenciais e remetente). Sem ele, o envio é recusado com `422 CHANNEL_NOT_ENABLED` e nenhum email sai — ver [Configurar os canais › EMAIL (SMTP)](/docs/developer/notification#email-smtp). O canal também tem de estar ativo nas [Definições da aplicação](/docs/developer/notification/admin-ui#definições).
</Callout>

```bash
curl -X POST "$NOTIFICATION_SERVICE_BASE_URL/api/v1/notifications" \
  -H "Authorization: Bearer $TOKEN_M2M" \
  -H "Content-Type: application/json" \
  -d '{
    "applicationCode": "MINHA_APP",
    "channelStrategy": "SPECIFIC",
    "channels": ["EMAIL"],
    "recipients": [{ "email": "utente@exemplo.cv" }],
    "template": { "code": "pedido-confirmacao", "locale": "pt-CV" },
    "variables": { "nome": "Maria Silva", "numeroPedido": "PED-2026-456" },
    "category": "pedido-confirmacao",
    "businessKey": "PED-2026-456",
    "idempotencyKey": "pedido-confirmacao-PED-2026-456"
  }'
```

Resposta `202 Accepted`: a entrega é assíncrona. O estado de cada entrega consulta-se em `GET /notifications/{id}`.

---

## Erros [#erros]

Corpo de erro: `{ "code": "…", "message": "…" }`.

| HTTP | Códigos |
|---|---|
| 400 | `VALIDATION_ERROR`, `INVALID_REQUEST`, `APPLICATION_CODE_REQUIRED` |
| 403 | `APPLICATION_ACCESS_DENIED`, `ATTACHMENT_ACCESS_DENIED`, `MISSING_USER_ID` |
| 404 | `TEMPLATE_NOT_FOUND`, `NOTIFICATION_NOT_FOUND`, `RECIPIENT_NOT_FOUND`, `GROUP_NOT_FOUND`, `BATCH_NOT_FOUND`, `DELIVERY_NOT_FOUND`, `INBOX_ITEM_NOT_FOUND`, `ATTACHMENT_NOT_FOUND`, … |
| 413 | `ATTACHMENT_TOO_LARGE` |
| 422 | `TEMPLATE_ERROR`, `CHANNEL_NOT_ENABLED`, `CHANNEL_NOT_ALLOWED`, `CSV_INVALID`, `CALLBACK_DOMAIN_NOT_ALLOWED`, `SCHEDULING_NOT_SUPPORTED`, `INVALID_STATE_TRANSITION`, `ACCESS_MANAGER_UNAVAILABLE` |
| 429 | `QUOTA_EXCEEDED` |

---

## Estados de entrega (`DeliveryStatus`) [#estados-de-entrega-deliverystatus]

| Estado | Descrição |
|---|---|
| `PENDING` | Aceite, à espera (ou agendada para `scheduledAt`) |
| `QUEUED` | Na fila de processamento |
| `SENDING` | Envio em curso |
| `SENT` | Aceite pelo provider |
| `DELIVERED` | Entrega confirmada (recibo do provider, IN_APP guardada, OFFLINE entregue à mão) |
| `READ` | Leitura confirmada (recibo de leitura, IN_APP aberta, marcação manual) |
| `FAILED` | Falha definitiva depois das tentativas |
| `CANCELLED` | Cancelada antes do envio |
| `EXPIRED` | `expiresAt` passou sem envio |


# SDK TypeScript

URL: /pt/docs/developer/notification/frontend-client

Cliente TypeScript do Notification Service (envio M2M, inbox, anexos, templates, erros) e editor visual de templates de email.

# SDK TypeScript [#sdk-typescript]

Dois pacotes npm para o frontend:

| Pacote | Versão | Para quê |
|---|---|---|
| `@igrp/platform-notification-client-ts` | `0.2.0-beta.4` | Cliente REST do Notification Service: envio, inbox, anexos, templates |
| `@igrp/notification-template-editor` | `0.1.0-beta.5` | Editor visual de templates de email (React) |

Os pacotes estão no registry do iGRP:

```bash
# .npmrc
@igrp:registry=https://sonatype.nosi.cv/repository/igrp/
```

O passo a passo de uma app Next.js completa (factories, Server Actions, sino) está em [Envio — SDKs e API](./integracao#frontend-nextjs). Esta página é a referência do SDK.

---

## `@igrp/platform-notification-client-ts` [#igrpplatform-notification-client-ts]

```bash
pnpm add @igrp/platform-notification-client-ts
```

`NotificationClient` agrupa um cliente por domínio da API, todos com a mesma configuração:

| Propriedade | Endpoint | Uso típico |
|---|---|---|
| `notifications` | `/api/v1/notifications` | `send`, `list`, `get_`, `resend` |
| `inbox` | `/api/v1/inbox` | Caixa de entrada IN_APP do utilizador |
| `attachments` | `/api/v1/attachments` | `upload` / `download` de anexos |
| `templates` | `/api/v1/templates` | `list`, `create`, `update`, `publish`, `archive`, `preview` |
| `bulk`, `batches` | | Envios em massa e o seu estado |
| `recipients`, `groups` | | Destinatários e grupos guardados |
| `recurrences` | | Envios recorrentes |
| `deliveries`, `stats` | | Entregas por canal e estatísticas |
| `settings`, `webhooks`, `admin` | | Configuração da aplicação e administração |

Todos os métodos devolvem `ApiResponse<T>`: o corpo está em `.data` (e há `status` e `statusText`).

### Autenticação [#autenticação]

O serviço aceita dois tipos de token, e cada um serve para operações diferentes:

| Token | Para quê | Como criar o cliente |
|---|---|---|
| **Utilizador** (sessão) | Inbox do próprio utilizador: o `userId` sai do token | `headers.Authorization` com o token da sessão |
| **M2M** (`client_credentials`) | Enviar, anexos, templates, administração | `m2mConfig` com `clientId` / `clientSecret` / `authBaseUrl` |

#### Utilizador final [#utilizador-final]

```ts
import { NotificationClient } from '@igrp/platform-notification-client-ts';
import { auth } from '@/lib/auth';

const token = await auth.getAccessToken(); // sessão do template iGRP

const client = NotificationClient.create({
  baseUrl: process.env.NOTIFICATION_SERVICE_BASE_URL!,
  timeout: 30000,
  headers: { Authorization: `Bearer ${token?.accessToken}` },
});
```

#### M2M [#m2m]

```ts
import { NotificationClient } from '@igrp/platform-notification-client-ts';

const client = NotificationClient.create(
  { baseUrl: process.env.NOTIFICATION_SERVICE_BASE_URL! },
  {
    clientId: process.env.IGRP_M2M_CLIENT_ID!,
    clientSecret: process.env.IGRP_M2M_CLIENT_SECRET!,
    authBaseUrl: process.env.IGRP_ACCESS_MANAGEMENT_API!, // Access Management, não o Notification Service
    serviceId: process.env.IGRP_SERVICE_ID, // opcional: header X-Machine-Service-ID
  },
);
```

O SDK pede o token em `POST {authBaseUrl}/oauth2/token`, guarda-o em cache e renova-o 30 s antes de expirar. `scope` é opcional: só o passes se o cliente OAuth2 o restringir.

<Callout type="warning" title="authBaseUrl é o Access Management">
Se `authBaseUrl` faltar, o SDK pede o token ao `baseUrl` do Notification Service, que responde 401. Aponta-o para a base do Access Management (ou do IdP).
</Callout>

<Callout type="info" title="Cria o cliente M2M uma vez">
Cada cliente de domínio tem a sua cache de token. Guarda o `NotificationClient` num módulo (singleton) em vez de o criar a cada pedido. Se precisares do token em bruto, usa `ClientCredentialsTokenProvider` (exportado pelo pacote) e passa-o em `headers.Authorization`.
</Callout>

Todas as variáveis de ambiente são lidas no servidor (Server Actions, Route Handlers). O `clientSecret` nunca pode chegar ao bundle do browser.

### `applicationCode` [#applicationcode]

Cada pedido pertence a uma aplicação do Access Management. O serviço lê-a do claim `application_code` do token e, se o token não a tiver, do `applicationCode` do pedido. Os tokens `client_credentials` normalmente não trazem esse claim (o erro é `APPLICATION_CODE_REQUIRED`), por isso passa sempre o código da app (`IGRP_APP_CODE`):

| Operação | Onde vai |
|---|---|
| `notifications.send(request)` | `request.applicationCode` |
| `templates.list(filters, pageable, applicationCode)` | 3.º argumento |
| `attachments.upload(file, filename, applicationCode)` | 3.º argumento |
| `notifications.list` / `get_` / `resend`, `templates.get_` / `publish` / `archive` | último argumento |

---

### Enviar com template [#enviar-com-template]

<Callout type="warning" title="EMAIL precisa de SMTP configurado">
Para enviar por **EMAIL**, o provider SMTP tem de estar configurado no Notification Service (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`: servidor, porta, credenciais e remetente). Sem ele, o envio é recusado com `422 CHANNEL_NOT_ENABLED` e nenhum email sai — ver [Configurar os canais › EMAIL (SMTP)](/docs/developer/notification#email-smtp). O canal também tem de estar ativo nas [Definições da aplicação](/docs/developer/notification/admin-ui#definições).
</Callout>

```ts
'use server';

import { Channel, ChannelStrategy } from '@igrp/platform-notification-client-ts';
import { getNotificationM2MClient } from '@/lib/notification-client';

export async function enviarConfirmacao(input: { email: string; nome: string; referencia: string }) {
  const client = getNotificationM2MClient();
  const { data } = await client.notifications.send({
    applicationCode: process.env.IGRP_APP_CODE!,
    channelStrategy: ChannelStrategy.SPECIFIC,
    channels: [Channel.EMAIL],
    recipients: [{ email: input.email }],
    template: { code: 'pedido-confirmacao', locale: 'pt-CV' },
    variables: { nome: input.nome, numeroPedido: input.referencia },
    category: 'pedido-confirmacao',
    businessKey: input.referencia,
    idempotencyKey: `pedido-confirmacao-${input.referencia}`,
  });
  return data.id;
}
```

Campos de `SendNotificationRequest`:

| Campo | Descrição |
|---|---|
| `channelStrategy` | **Obrigatório.** `SPECIFIC` (só os `channels` indicados), `ALL` ou `PREFERRED` (preferências do destinatário). |
| `channels` | `EMAIL`, `SMS`, `WHATSAPP`, `PUSH`, `IN_APP`, `OFFLINE`. |
| `recipients` | `{ userId }`, `{ email }`, `{ phone }` ou `{ pushToken }` — endereços explícitos por canal. |
| `groups` | Um de: `groupId` (grupo guardado), `departmentCode`, `roleCode` ou `permissionName` do Access Management, resolvidos na hora do envio. |
| `template` | `{ code, locale? }` — template **publicado**. |
| `inline` | `{ subject?, title?, body? }` — conteúdo sem template. Usa `template` ou `inline`. |
| `variables` | Valores para as variáveis Handlebars do template (`{{nome}}`). |
| `priority` | `LOW`, `NORMAL`, `HIGH`, `URGENT`. |
| `attachments` | `[{ filename, contentRef }]` — ver [Anexos](#anexos). |
| `category`, `businessKey`, `correlationId` | Agrupamento e rastreio (o `businessKey` aparece na inbox). |
| `idempotencyKey` | Repetir o pedido com a mesma chave não duplica o envio. |
| `locale`, `scheduledAt`, `expiresAt`, `callbackUrl` | Opcionais. |

### Template ou conteúdo inline [#template-ou-conteúdo-inline]

Não há endpoint para obter um template pelo código. Para saber se está publicado, lista com filtro:

```ts
import { Channel, TemplateStatus } from '@igrp/platform-notification-client-ts';

const { data } = await client.templates.list(
  { code: 'pedido-confirmacao', status: TemplateStatus.PUBLISHED, locale: 'pt-CV', channel: Channel.EMAIL },
  { page: 0, size: 5 },
  process.env.IGRP_APP_CODE,
);
const publicado = data.content?.[0];
```

Um padrão útil é enviar com o template e, se ele não existir ou falhar a renderização, repetir com conteúdo `inline`:

```ts
import { ApiClientError, IgrpNotificationErrorCode } from '@igrp/platform-notification-client-ts';

try {
  await client.notifications.send({ ...base, template: { code: 'pedido-confirmacao', locale: 'pt-CV' }, variables });
} catch (error) {
  const semTemplate =
    error instanceof ApiClientError &&
    (error.is(IgrpNotificationErrorCode.TEMPLATE_NOT_FOUND) || error.is(IgrpNotificationErrorCode.TEMPLATE_ERROR));
  if (!semTemplate) throw error;
  await client.notifications.send({
    ...base,
    inline: { subject: `Pedido ${referencia}`, title: 'Pedido recebido', body: `Olá ${nome}, recebemos o seu pedido.` },
  });
}
```

### Anexos [#anexos]

O anexo é enviado primeiro e o envio refere-o pelo `contentRef`:

```ts
const pdf = new Blob([bytes], { type: 'application/pdf' }); // bytes: ArrayBuffer | Uint8Array

const { data: anexo } = await client.attachments.upload(pdf, 'recibo.pdf', process.env.IGRP_APP_CODE);

await client.notifications.send({
  ...base,
  attachments: [{ filename: 'recibo.pdf', contentRef: anexo.contentRef }],
});
```

`upload` recebe um `Blob` (ou `File`). Erros possíveis: `ATTACHMENT_TOO_LARGE`, `ATTACHMENT_INVALID`.

---

### Inbox (IN_APP) [#inbox-in_app]

Com o cliente de **utilizador**:

| Método | Endpoint |
|---|---|
| `inbox.list({ unreadOnly?, includeArchived? }, { page, size })` | `GET /api/v1/inbox` |
| `inbox.unreadCount()` | `GET /api/v1/inbox/unread-count` |
| `inbox.markRead(deliveryId)` | `POST /api/v1/inbox/{deliveryId}/read` |
| `inbox.markAllRead()` | `POST /api/v1/inbox/read` |
| `inbox.archive(deliveryId)` | `POST /api/v1/inbox/{deliveryId}/archive` |

Cada item (`InboxItemResponse`) tem `deliveryId`, `subject`, `title`, `body`, `category`, `businessKey`, `status`, `read`, `archived` e `createdAt`. O `body` pode trazer HTML (templates de email): limpa-o antes de o mostrar como texto.

#### Sino da header [#sino-da-header]

A header do framework mostra o que `getHeaderData()` devolver em `notifications` (`IGRPNotificationArgs[]`, de `@igrp/framework-next-types`). Converte cada item da inbox:

```ts
// src/lib/map-inbox-notification.ts
import type { IGRPNotificationArgs } from '@igrp/framework-next-types';
import type { InboxItemResponse } from '@igrp/platform-notification-client-ts';

const texto = (html?: string) => (html ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();

export function mapInboxItem(item: InboxItemResponse): IGRPNotificationArgs {
  return {
    id: item.deliveryId ?? '',
    title: texto(item.subject || item.title) || 'Notificação',
    message: texto(item.body) || item.businessKey || '',
    type: 'info',
    timestamp: item.createdAt ?? new Date().toISOString(),
    isRead: Boolean(item.read),
  };
}
```

A ligação ao `getHeaderData()` está em [Envio — SDKs e API](./integracao#5-sino-de-notificações-na-header).

---

### Erros [#erros]

Os erros HTTP chegam como `ApiClientError`:

| Campo | Conteúdo |
|---|---|
| `status` | Código HTTP |
| `errorCode` | Código do serviço (`IgrpNotificationErrorCode`), por exemplo `TEMPLATE_NOT_FOUND`, `APPLICATION_CODE_REQUIRED`, `CHANNEL_NOT_ENABLED`, `QUOTA_EXCEEDED`, `VALIDATION_ERROR` |
| `serverMessage` | Mensagem do serviço (envelope `{ code, message }`) |
| `details` | Corpo da resposta |
| `is(code)` | `true` se `errorCode` for esse código |

<Callout type="warning" title="Server Actions perdem os detalhes">
Uma Server Action só passa `message` ao browser: `errorCode`, `serverMessage` e `details` perdem-se. Para mostrar o motivo num toast, apanha o `ApiClientError` no servidor e lança um `Error` cuja mensagem já inclua `serverMessage` e `errorCode`.
</Callout>

---

### Variáveis de ambiente [#variáveis-de-ambiente]

| Variável | Uso |
|---|---|
| `NOTIFICATION_SERVICE_BASE_URL` | Base do Notification Service, sem `/api/v1` (no workspace do Studio: `http://<slug>:<porta>/gateway-api/igrp-notification-service`) |
| `IGRP_APP_CODE` | Código da aplicação → `applicationCode` |
| `IGRP_M2M_CLIENT_ID`, `IGRP_M2M_CLIENT_SECRET` | Cliente OAuth2 M2M — só servidor |
| `IGRP_ACCESS_MANAGEMENT_API` (ou `IGRP_AUTH_ISSUER`) | `authBaseUrl` do token M2M |
| `IGRP_SERVICE_ID` | Opcional — header `X-Machine-Service-ID` |
| `NEXT_PUBLIC_IGRP_NOTIFICATION_URL` | Página "ver todas" do sino (`notificationsUrl`) |

---

## `@igrp/notification-template-editor` [#igrpnotification-template-editor]

Editor visual React para compor templates de email por linhas e colunas. É o editor usado na [Notifications UI](./admin-ui). Precisa de React 19 e do Design System (`@igrp/igrp-framework-react-design-system` ≥ 0.2.0-beta.3).

```bash
pnpm add @igrp/notification-template-editor
```

Estilos, no CSS global (Tailwind v4):

```css
@source "../../node_modules/@igrp/notification-template-editor/dist/**/*.{js,jsx,ts,tsx,mjs,cjs}";
@import "@igrp/notification-template-editor/styles";
```

```tsx
'use client';

import { EmailTemplateRowEditor, type EmailTemplate } from '@igrp/notification-template-editor';

export function EditorTemplate({ onSave }: { onSave: (html: string) => void }) {
  return (
    <EmailTemplateRowEditor
      tagSyntax="handlebars"
      layout="fill"
      embedded
      persistLocal={false}
      onSave={(template: EmailTemplate, html: string) => onSave(html)}
    />
  );
}
```

| Prop | Descrição |
|---|---|
| `tagSyntax` | `"handlebars"` (`{{nome}}`) ou `"go"` (por omissão). O Notification Service renderiza **Handlebars**: usa `"handlebars"`. |
| `initialTemplate`, `initialDynamicTags` | Estado inicial (`EmailTemplate`, `DynamicTag[]`). |
| `onChange`, `onSave` | Recebem `(template, html)`: o HTML gerado vai para o corpo do template no serviço. |
| `onDynamicTagsChange` | Aviso quando as variáveis disponíveis mudam. |
| `embedded`, `layout` | Embutir numa página (`layout="fill"` ocupa a altura do contentor). |
| `persistLocal` | Guardar o rascunho no `localStorage` do browser. |
| `insertTokenRef` | Ref para o host inserir uma variável no bloco selecionado. |

Também exporta `generateEmailHtml(template)` (gera o HTML fora do editor) e os dados iniciais `INITIAL_TEMPLATE`, `DEFAULT_DYNAMIC_TAGS` e `SAMPLE_PRESETS`.


# Visão geral

URL: /pt/docs/developer/notification

Hub centralizado de notificações do ecossistema iGRP — email, SMS, WhatsApp, push, in-app e offline, com suporte a templates, envios em massa e recorrência.

# Notification Service [#notification-service]

O **iGRP Notification Service** é o microserviço de notificações do iGRP 3.0. Cada aplicação do [Access Management](/docs/developer/access-management) envia notificações por um SDK ([TypeScript](./frontend-client) ou [Java](./java-client)) ou pela [API REST](./api); o serviço trata de canais, templates, retentativas, auditoria e RGPD.

Para ligar uma aplicação, segue a página [Envio — SDKs e API](./integracao). Templates e definições geram-se na [Notifications UI](./admin-ui), a consola de administração.

---

<Callout type="info" title="Já integrado nos processos">
O runtime dos processos BPMN avisa por IN_APP quem tem acesso à etapa seguinte de cada processo — ver [Notificações de tarefas](/docs/developer/process/management#notificações-de-tarefas).
</Callout>

## Canais suportados [#canais-suportados]

| Canal | Provider | Caso de uso típico |
|---|---|---|
| `EMAIL` | SMTP | Recibos, confirmações, convites |
| `SMS` | Agregador HTTP | Alertas urgentes, OTP |
| `WHATSAPP` | Meta Cloud API | Mensagens com templates aprovados |
| `PUSH` | Firebase Cloud Messaging | Aplicações móveis |
| `IN_APP` | Caixa de entrada na base de dados | Sino de notificações na UI web |
| `OFFLINE` | Sem transporte — fica registado para entrega manual | Balcão, carta |

Todos os providers vêm **desligados**: cada canal só envia depois de configurado — ver [Configurar os canais](#configurar-os-canais).

---

## Modos de envio [#modos-de-envio]

| Modo | Descrição |
|---|---|
| **Recipient único** | Um destinatário, um ou mais canais |
| **Multi-destinatário** | Lista de destinatários numa única chamada |
| **Bulk** | Lista grande ou ficheiro CSV; resposta com `batchId` |
| **Grupos** | Grupos internos ou sincronizados com o Access Manager |
| **Recorrente** | Cron ou intervalo via Temporal Schedules |

---

## Envio de notificações [#envio-de-notificações]

Antes do primeiro envio, confirma estes quatro pontos. Cada um, se faltar, faz o pedido falhar com um erro diferente:

| # | Requisito | Onde se configura | Se faltar |
|---|---|---|---|
| 1 | **Cliente M2M** associado à aplicação, com a permissão `igrp.notification.send` | Access Management (conta de serviço do cliente) | `403`, ou `422 ACCESS_MANAGER_UNAVAILABLE` se o token não trouxer `application_code` |
| 2 | **Provider do canal ligado no serviço** — para EMAIL, o **SMTP** | Variáveis do Notification Service — [Configurar os canais](#configurar-os-canais) | `422 CHANNEL_NOT_ENABLED` |
| 3 | **Canal ativo na aplicação** | Consola › [Definições › Canais permitidos](./admin-ui#definições) | `422 CHANNEL_NOT_ENABLED` |
| 4 | **Template publicado** no código, canal e locale do envio (ou conteúdo `inline`) | Consola › [Templates](./templates) | `422 TEMPLATE_ERROR` |

<Callout type="warning" title="EMAIL precisa de SMTP configurado">
Sem o provider SMTP configurado no Notification Service (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`: servidor, porta, credenciais e remetente), **nenhum email é enviado**: a API recusa o pedido com `422 CHANNEL_NOT_ENABLED`. Ver [EMAIL (SMTP)](#email-smtp).
</Callout>

Com os requisitos cumpridos, o envio é um pedido M2M:

```bash
curl -X POST "$NOTIFICATION_SERVICE_BASE_URL/api/v1/notifications" \
  -H "Authorization: Bearer $TOKEN_M2M" \
  -H "Content-Type: application/json" \
  -d '{
    "channelStrategy": "SPECIFIC",
    "channels": ["EMAIL"],
    "recipients": [{ "email": "utente@exemplo.cv" }],
    "template": { "code": "pedido-confirmacao", "locale": "pt-CV" },
    "variables": { "nome": "Maria Silva", "numeroPedido": "PED-2026-0456" },
    "businessKey": "PED-2026-0456",
    "idempotencyKey": "pedido-confirmacao-PED-2026-0456"
  }'
```

A resposta é `202 Accepted`, com uma entrega por destinatário e canal. A entrega segue em segundo plano: acompanha o estado (`QUEUED` → `SENT` → `DELIVERED`) em **Envios** na [consola](./admin-ui#envios) ou em `GET /api/v1/notifications/{id}`.

| Para… | Ver |
|---|---|
| Enviar a partir de uma app Next.js ou de um serviço Spring Boot | [Envio — SDKs e API](./integracao) |
| Os campos do pedido e os erros | [API REST › Notificações](./api#notificações) |
| Os SDKs | [TypeScript](./frontend-client#enviar-com-template) · [Java](./java-client#enviar-com-template) |

---

## Arquitetura [#arquitetura]

<NotificationArchitecture />

1. A aplicação chama `POST /api/v1/notifications` com um token M2M. O serviço valida o pedido (aplicação, canal ativo, template publicado), grava-o e responde `202 Accepted` com uma entrega por destinatário e canal.
2. O **Temporal** entrega em segundo plano, com filas por prioridade e retentativas. Se o Temporal não responder, a entrega é feita na hora, dentro do pedido.
3. O **provider** do canal envia (SMTP, FCM, …). Os recibos de entrega e leitura voltam por `/webhooks/{providerId}` e atualizam o estado (`SENT` → `DELIVERED` → `READ`).
4. IN_APP não sai do serviço: a mensagem fica na inbox do utilizador, lida com o token dele.

O código segue **hexagonal + DDD** em 13 módulos Maven:

| Módulo | Responsabilidade |
|---|---|
| `notification-domain` | Modelo de domínio (sem Spring) |
| `notification-application` | Casos de uso e portas |
| `notification-provider-spi` | Interface `ChannelProvider` |
| `provider-email-smtp` | EMAIL por SMTP |
| `provider-sms-aggregator` | SMS |
| `provider-whatsapp-meta` | WhatsApp |
| `provider-push-fcm` | PUSH |
| `provider-inapp`, `provider-offline` | IN_APP e OFFLINE |
| `provider-email-sendgrid` | Reservado — ainda sem implementação |
| `notification-app` | Executável Spring Boot 3.5 / Java 26 (API + workers) |
| `client` | SDK Java |
| `notification-bom` | Versões |

Um canal novo é um módulo `provider-*` novo, sem mexer no núcleo.

---

## Configurar os canais [#configurar-os-canais]

Um envio para um canal sem provider ativo é recusado logo na API com `422 CHANNEL_NOT_ENABLED`. O canal tem de estar ligado **no serviço** (provider) e **na aplicação** ([Definições › Canais permitidos](./admin-ui#definições)).

### EMAIL (SMTP) [#email-smtp]

Sem SMTP configurado, nenhum email sai. Liga o provider com o servidor de correio do ambiente:

| Propriedade | Variável de ambiente | Notas |
|---|---|---|
| `notification.providers.email-smtp.enabled` | `NOTIFICATION_PROVIDERS_EMAILSMTP_ENABLED` | `true` para ativar |
| `…email-smtp.host` | `NOTIFICATION_PROVIDERS_EMAILSMTP_HOST` | Servidor SMTP |
| `…email-smtp.port` | `NOTIFICATION_PROVIDERS_EMAILSMTP_PORT` | Por omissão `587` |
| `…email-smtp.username`, `…password` | `NOTIFICATION_PROVIDERS_EMAILSMTP_USERNAME`, `…_PASSWORD` | Se o servidor exigir autenticação |
| `…email-smtp.starttls` | `NOTIFICATION_PROVIDERS_EMAILSMTP_STARTTLS` | Por omissão `true` |
| `…email-smtp.from` | `NOTIFICATION_PROVIDERS_EMAILSMTP_FROM` | Remetente de **todos** os emails |

<Callout type="warning" title="O remetente vem do SMTP">
O `From` dos emails é sempre o `from` do provider SMTP. Os remetentes configurados nas Definições da aplicação ainda não são usados no envio.
</Callout>

Para testes sem enviar emails reais, aponta o SMTP para um servidor de captura (por exemplo Mailpit).

### Outros canais [#outros-canais]

| Canal | Ativar com | Precisa de |
|---|---|---|
| `IN_APP` | `INAPP_ENABLED=true` | — |
| `OFFLINE` | `OFFLINE_ENABLED=true` | — |
| `SMS` | `notification.providers.sms-aggregator.enabled=true` | `url`, `token`, `from` do agregador |
| `WHATSAPP` | `WHATSAPP_ENABLED=true` | `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET` |
| `PUSH` | `FCM_ENABLED=true` | `FCM_CREDENTIALS` (conta de serviço Firebase) |

---

## Observabilidade [#observabilidade]

- **OpenAPI** em `/v3/api-docs` · **Swagger UI** em `/swagger-ui.html`
- **Logs estruturados** (ECS JSON em produção — `LOG_STRUCTURED_FORMAT=ecs`)
- **OpenTelemetry**: tracing ligado por omissão, exportado para `OTEL_EXPORTER_OTLP_ENDPOINT` (desliga com `TRACING_ENABLED=false`)
- **GraphQL** em `/graphql` (desliga a introspection em produção com `GRAPHQL_INTROSPECTION=false`)

---

## Variáveis de ambiente [#variáveis-de-ambiente]

O serviço é distribuído como imagem e corre no cluster ou no Docker do workspace, ao lado dos outros serviços da plataforma. Configura-se por variáveis de ambiente.

### Autenticação e Access Management [#autenticação-e-access-management]

| Variável | Descrição |
|---|---|
| `AUTH_JWT_ISSUER` | **Obrigatória.** Issuer do Access Management; o serviço valida os tokens com ele |
| `NOTIFICATION_ACCESSMANAGEMENT_ENABLED` | `true` para verificar se o utilizador tem acesso à aplicação de cada pedido (necessário para a consola e para tokens de utilizador) |
| `NOTIFICATION_ACCESSMANAGEMENT_BASEURL` | Base do Access Management |
| `NOTIFICATION_ACCESSMANAGEMENT_CLIENTID`, `…_CLIENTSECRET` | Cliente M2M do serviço no Access Management |

No arranque o serviço regista no Access Management as suas permissões (`igrp.notification.*`).

### Base de dados, Temporal e anexos [#base-de-dados-temporal-e-anexos]

| Variável | Descrição |
|---|---|
| `SPRING_DATASOURCE_URL`, `…_USERNAME`, `…_PASSWORD` | PostgreSQL (o esquema é criado pelo Flyway) |
| `TEMPORAL_ENABLED` | `true` por omissão; `false` entrega de forma síncrona, sem Temporal |
| `TEMPORAL_TARGET`, `TEMPORAL_NAMESPACE` | Servidor Temporal (`host:7233`) e namespace |
| `STORAGE_ENDPOINT`, `STORAGE_BUCKET`, `STORAGE_REGION` | Armazenamento S3 dos anexos (MinIO: endpoint próprio) |
| `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY` | Credenciais S3 |
| `STORAGE_PATH_STYLE`, `STORAGE_SSE` | `true`/`false` para MinIO (path-style ligado, encriptação SSE desligada) |
| `STORAGE_ALLOWED_MIME_TYPES` | Tipos de anexo aceites |

### Canais e templates [#canais-e-templates]

| Variável | Descrição |
|---|---|
| `NOTIFICATION_PROVIDERS_EMAILSMTP_*`, `INAPP_ENABLED`, … | Providers — ver [Configurar os canais](#configurar-os-canais) |
| `NOTIFICATION_SEED_DEFAULT_TEMPLATES` | `false` para não publicar os templates de convite no arranque |

### Observabilidade [#observabilidade-1]

| Variável | Descrição |
|---|---|
| `TRACING_ENABLED`, `TRACING_SAMPLING_PROBABILITY`, `OTEL_EXPORTER_OTLP_ENDPOINT` | Tracing OpenTelemetry |
| `LOG_STRUCTURED_FORMAT` | Formato dos logs (`ecs` por omissão) |
| `GRAPHQL_INTROSPECTION` | `false` em produção |


# Envio — SDKs e API

URL: /pt/docs/developer/notification/integracao

Passo a passo para uma aplicação iGRP enviar notificações — template, envio a partir do backend Spring Boot ou do frontend Next.js, inbox e sino da header.

# Envio — SDKs e API [#envio--sdks-e-api]

Este guia liga uma aplicação iGRP ao Notification Service, do template ao sino da header. O exemplo envia por email a confirmação de um pedido; troca os nomes pelos da tua aplicação.

Os SDKs têm páginas de referência próprias: [SDK TypeScript](./frontend-client) e [SDK Java](./java-client).

| Quem envia | Quando usar |
|---|---|
| **Backend** (Spring Boot + SDK Java) | O envio é consequência de uma regra de negócio do serviço (convites, alterações de estado). |
| **Frontend** (Server Action + SDK TypeScript) | O envio parte de uma ação do utilizador na app (fechar um passo, emitir um recibo). |

Nos dois casos o envio é **M2M**: um cliente OAuth2 `client_credentials` do Access Management. A inbox, essa, lê-se com o token do **utilizador**.

<Callout type="info" title="Processos já notificam">
O runtime de processos avisa por IN_APP quem pode executar a tarefa seguinte, sem código na tua app — ver [Notificações de tarefas](/docs/developer/process/management#notificações-de-tarefas).
</Callout>

---

## 1. Pré-requisitos [#1-pré-requisitos]

- Um **cliente M2M** no Access Management (client ID + secret) cuja conta de serviço:
  - está **associada à aplicação**, para o token sair com o claim `application_code`;
  - tem a permissão **`igrp.notification.send`** (e `igrp.notification.read` para consultar envios; `igrp.notification.admin` se também listares templates).
- O **código da aplicação** (`IGRP_APP_CODE`).
- O canal que vais usar **ligado no serviço** (para EMAIL, o SMTP — ver [Configurar os canais](/docs/developer/notification#configurar-os-canais)) e **ativo** nas [definições da aplicação](./admin-ui#definições). Senão: `422 CHANNEL_NOT_ENABLED`.

<Callout type="warning" title="Conta de serviço sem aplicação">
Se o token M2M não trouxer `application_code`, o serviço tenta validar o acesso à aplicação como se o chamador fosse um utilizador, e uma conta de serviço falha: `422 ACCESS_MANAGER_UNAVAILABLE` (`User not found`). Sem permissões, a resposta é `403`. Confirma os dois no Access Management antes de depurar o código.
</Callout>

## 2. Criar e publicar o template [#2-criar-e-publicar-o-template]

Cria o template na [Notifications UI](./admin-ui) (editor visual) ou pela [API de templates](./api#templates) e **publica-o**: só templates publicados são usados no envio.

| Campo | Exemplo |
|---|---|
| Código | `pedido-confirmacao` |
| Canal | `EMAIL` |
| Locale | `pt-CV` |
| Assunto | `Pedido {{numeroPedido}} recebido` |
| Variáveis | `nome`, `numeroPedido` |

As variáveis usam a sintaxe Handlebars (`{{nome}}`). O envio procura o template com o **locale exato**: se só publicaste `pt-CV`, um envio com `pt` falha com `422 TEMPLATE_ERROR`. Detalhes em [Templates](./templates).

---

## Backend (Spring Boot) [#backend-spring-boot]

### 3. Dependência e cliente [#3-dependência-e-cliente]

Adiciona `cv.igrp.platform.notification:client` e cria os beans `ApiClient` e `NotificationsApi` como em [SDK Java › Configurar num serviço Spring Boot](./java-client#configurar-num-serviço-spring-boot).

### 4. Serviço de envio [#4-serviço-de-envio]

<Callout type="warning" title="EMAIL precisa de SMTP configurado">
Para enviar por **EMAIL**, o provider SMTP tem de estar configurado no Notification Service (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`: servidor, porta, credenciais e remetente). Sem ele, o envio é recusado com `422 CHANNEL_NOT_ENABLED` e nenhum email sai — ver [Configurar os canais › EMAIL (SMTP)](/docs/developer/notification#email-smtp). O canal também tem de estar ativo nas [Definições da aplicação](/docs/developer/notification/admin-ui#definições).
</Callout>

```java
@Service
public class ConfirmacaoNotifier {

    private static final Logger LOG = LoggerFactory.getLogger(ConfirmacaoNotifier.class);
    private final Optional<NotificationsApi> notificationsApi;

    public ConfirmacaoNotifier(Optional<NotificationsApi> notificationsApi) {
        this.notificationsApi = notificationsApi;
    }

    public void enviar(String email, String nome, String numeroPedido) {
        if (notificationsApi.isEmpty()) {
            LOG.info("Notificações desativadas, confirmação {} não enviada", numeroPedido);
            return;
        }
        TemplateRefDto template = new TemplateRefDto();
        template.setCode("pedido-confirmacao");
        template.setLocale("pt-CV");

        RecipientDto recipient = new RecipientDto();
        recipient.setEmail(email);

        SendNotificationRequest request = new SendNotificationRequest();
        request.setChannelStrategy(ChannelStrategy.SPECIFIC);
        request.setChannels(List.of(Channel.EMAIL));
        request.setRecipients(List.of(recipient));
        request.setTemplate(template);
        request.setVariables(Map.of("nome", nome, "numeroPedido", numeroPedido));
        request.setBusinessKey(numeroPedido);
        request.setIdempotencyKey("pedido-confirmacao:" + numeroPedido);

        try {
            notificationsApi.get().send(request);
        } catch (ApiException e) {
            // o envio falhou; decide se isto deve falhar o comando de negócio
            LOG.warn("Confirmação {} não enviada: {} {}", numeroPedido, e.getErrorCode(), e.getErrorMessage());
        }
    }
}
```

`idempotencyKey` evita emails duplicados se o comando for repetido.

---

## Frontend (Next.js) [#frontend-nextjs]

### 3. Instalar o SDK [#3-instalar-o-sdk]

```bash
pnpm add @igrp/platform-notification-client-ts
```

### 4. Factories do cliente [#4-factories-do-cliente]

Um cliente M2M (criado uma vez) para enviar, e um cliente por pedido com o token do utilizador para a inbox:

```ts
// src/lib/notification-client.ts
import 'server-only';
import { NotificationClient } from '@igrp/platform-notification-client-ts';
import { auth } from '@/lib/auth';

const baseUrl = process.env.NOTIFICATION_SERVICE_BASE_URL ?? '';

let m2mClient: NotificationClient | undefined;

/** Envio, anexos, templates — token client_credentials do Access Management. */
export function getNotificationM2MClient(): NotificationClient {
  m2mClient ??= NotificationClient.create(
    { baseUrl },
    {
      clientId: process.env.IGRP_M2M_CLIENT_ID!,
      clientSecret: process.env.IGRP_M2M_CLIENT_SECRET!,
      authBaseUrl: process.env.IGRP_ACCESS_MANAGEMENT_API || process.env.IGRP_AUTH_ISSUER!,
    },
  );
  return m2mClient;
}

/** Inbox do utilizador autenticado — o userId sai do token. */
export async function getNotificationUserClient(): Promise<NotificationClient> {
  const token = await auth.getAccessToken();
  if (!token?.accessToken) throw new Error('Sem sessão');
  return NotificationClient.create({
    baseUrl,
    timeout: 30000,
    headers: { Authorization: `Bearer ${token.accessToken}` },
  });
}
```

### 5. Server Action de envio [#5-server-action-de-envio]

<Callout type="warning" title="EMAIL precisa de SMTP configurado">
Para enviar por **EMAIL**, o provider SMTP tem de estar configurado no Notification Service (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`: servidor, porta, credenciais e remetente). Sem ele, o envio é recusado com `422 CHANNEL_NOT_ENABLED` e nenhum email sai — ver [Configurar os canais › EMAIL (SMTP)](/docs/developer/notification#email-smtp). O canal também tem de estar ativo nas [Definições da aplicação](/docs/developer/notification/admin-ui#definições).
</Callout>

```ts
// src/app/(minha-app)/client/notification.ts
'use server';

import {
  ApiClientError,
  Channel,
  ChannelStrategy,
  IgrpNotificationErrorCode,
  type SendNotificationRequest,
} from '@igrp/platform-notification-client-ts';
import { getNotificationM2MClient } from '@/lib/notification-client';

export async function enviarConfirmacao(input: { email: string; nome: string; numeroPedido: string }) {
  const client = getNotificationM2MClient();
  const base: SendNotificationRequest = {
    applicationCode: process.env.IGRP_APP_CODE!,
    channelStrategy: ChannelStrategy.SPECIFIC,
    channels: [Channel.EMAIL],
    recipients: [{ email: input.email }],
    category: 'pedido-confirmacao',
    businessKey: input.numeroPedido,
    idempotencyKey: `pedido-confirmacao-${input.numeroPedido}`,
  };

  try {
    const { data } = await client.notifications.send({
      ...base,
      template: { code: 'pedido-confirmacao', locale: 'pt-CV' },
      variables: { nome: input.nome, numeroPedido: input.numeroPedido },
    });
    return data.id;
  } catch (error) {
    if (!(error instanceof ApiClientError)) throw error;
    const semTemplate =
      error.is(IgrpNotificationErrorCode.TEMPLATE_NOT_FOUND) || error.is(IgrpNotificationErrorCode.TEMPLATE_ERROR);
    if (!semTemplate) {
      // Server Actions só passam `message` ao browser
      throw new Error(`${error.serverMessage ?? error.message} [${error.errorCode ?? error.status}]`);
    }
    // template ainda não publicado: envia o mesmo texto inline
    const { data } = await client.notifications.send({
      ...base,
      inline: {
        subject: `Pedido ${input.numeroPedido} recebido`,
        title: 'Pedido recebido',
        body: `Olá ${input.nome}, recebemos o pedido ${input.numeroPedido}.`,
      },
    });
    return data.id;
  }
}
```

Para enviar um PDF, faz primeiro `client.attachments.upload(blob, 'recibo.pdf', process.env.IGRP_APP_CODE)` e junta `attachments: [{ filename: 'recibo.pdf', contentRef }]` — ver [Anexos](./frontend-client#anexos).

### 6. Sino de notificações na header [#6-sino-de-notificações-na-header]

A header do framework mostra o array `notifications` devolvido por `getHeaderData()` em `src/igrp.template.config.ts`. Lê a inbox do utilizador e converte cada item com o `mapInboxItem` de [SDK TypeScript › Sino da header](./frontend-client#sino-da-header):

```ts
// src/lib/header-notifications.ts
import 'server-only';
import type { IGRPNotificationArgs } from '@igrp/framework-next-types';
import { getNotificationUserClient } from '@/lib/notification-client';
import { mapInboxItem } from '@/lib/map-inbox-notification';

export async function getHeaderNotifications(): Promise<IGRPNotificationArgs[]> {
  try {
    const client = await getNotificationUserClient();
    const { data } = await client.inbox.list({ unreadOnly: false }, { page: 0, size: 10 });
    return (data.content ?? []).map(mapInboxItem);
  } catch {
    return []; // o sino nunca deve partir a página
  }
}
```

```ts
// src/igrp.template.config.ts (excerto)
getHeaderData: async () => {
  const user = bypass ? getMockUser().mockUser : undefined;
  return {
    user,
    notifications: bypass ? [] : await getHeaderNotifications(),
    notificationsUrl: process.env.NEXT_PUBLIC_IGRP_NOTIFICATION_URL || '',
    showNotifications: true,
    // ... restantes flags
  };
},
```

Para marcar como lida, chama `inbox.markRead(deliveryId)` (o `id` do item mapeado) numa Server Action.

---

## Variáveis de ambiente [#variáveis-de-ambiente]

### Backend [#backend]

| Variável | Descrição |
|---|---|
| `NOTIFICATION_SERVICE_BASE_URL` | Base do Notification Service, sem `/api/v1` |
| `IGRP_ACCESS_MANAGEMENT_API` | Base do Access Management (`/oauth2/token`) |
| `IGRP_M2M_CLIENT_ID`, `IGRP_M2M_CLIENT_SECRET` | Cliente M2M |

### Frontend [#frontend]

| Variável | Descrição |
|---|---|
| `NOTIFICATION_SERVICE_BASE_URL` | Base do Notification Service, sem `/api/v1` |
| `IGRP_APP_CODE` | Código da aplicação (`applicationCode`) |
| `IGRP_M2M_CLIENT_ID`, `IGRP_M2M_CLIENT_SECRET` | Cliente M2M — só no servidor |
| `IGRP_ACCESS_MANAGEMENT_API` (ou `IGRP_AUTH_ISSUER`) | `authBaseUrl` do token M2M |
| `NEXT_PUBLIC_IGRP_NOTIFICATION_URL` | Página "ver todas" do sino |

No workspace do Studio o serviço fica em `http://<slug>:<porta>/gateway-api/igrp-notification-service`.


# SDK Java

URL: /pt/docs/developer/notification/java-client

Cliente Java do Notification Service — dependência Maven, configuração M2M num serviço Spring Boot, envio com template, erros.

# SDK Java [#sdk-java]

O módulo `client` é o cliente Java do Notification Service. Usa só `java.net.http.HttpClient` e Jackson: **não depende do Spring**. A configuração num serviço Spring Boot (propriedades, beans) é feita por ti — ver [Configurar num serviço Spring Boot](#configurar-num-serviço-spring-boot).

É o cliente que o Access Management usa para enviar os emails de convite.

---

## Dependência Maven [#dependência-maven]

```xml
<dependency>
  <groupId>cv.igrp.platform.notification</groupId>
  <artifactId>client</artifactId>
  <version>0.2.0-beta.5</version>
</dependency>
```

O artefacto está no repositório Maven do iGRP (não no registry npm):

```xml
<repository>
  <id>igrp-framework-releases</id>
  <url>https://sonatype.nosi.cv/repository/igrp-framework-releases/</url>
</repository>
```

---

## `ApiClient` e classes de API [#apiclient-e-classes-de-api]

`ApiClient` guarda a base URL e as credenciais. Cada domínio da API tem uma classe que recebe o `ApiClient` no construtor:

| Classe | Endpoint | Métodos principais |
|---|---|---|
| `NotificationsApi` | `/api/v1/notifications` | `send`, `list`, `get`, `resend` |
| `BulkApi` | `/api/v1/notifications/bulk` | `bulk(request)`, `bulkCsv(request, csv, filename)` |
| `AttachmentsApi` | `/api/v1/attachments` | `upload`, `download` |
| `TemplatesApi` | `/api/v1/templates` | `list`, `create`, `get`, `update`, `publish`, `archive`, `preview` |
| `InboxApi` | `/api/v1/inbox` | `list`, `unreadCount`, `markRead`, `archive` |
| `BatchesApi`, `RecipientsApi`, `GroupsApi`, `RecurrencesApi`, `DeliveriesApi`, `StatsApi`, `SettingsApi`, `AdminApi`, `WebhooksApi` | | Restantes recursos da [API REST](./api) |

### Autenticação M2M [#autenticação-m2m]

```java
ApiClient client = new ApiClient();
client.setBaseUrl("https://<gateway>/igrp-notification-service"); // sem /api/v1
client.setClientCredentials(
    "https://<gateway>/igrp-access-management", // authServerBaseUrl
    clientId,
    clientSecret,
    null);                                       // scope: só se o cliente OAuth2 o exigir
```

O cliente pede o token em `POST {authServerBaseUrl}/oauth2/token` (`client_credentials`), guarda-o em cache e renova-o antes de expirar.

| Sobrecarga | Token pedido a |
|---|---|
| `setClientCredentials(authServerBaseUrl, clientId, clientSecret, scope)` | `authServerBaseUrl` — **a recomendada** |
| `setClientCredentials(clientId, clientSecret[, scope])` | `baseUrl` do Notification Service — só funciona se ambos estiverem atrás do mesmo host |

<Callout type="warning" title="authServerBaseUrl é o Access Management">
O endpoint é `{authServerBaseUrl}/oauth2/token`: usa a base do Access Management, não o URL do realm do Keycloak.
</Callout>

`setAuthToken(token)` serve quando já tens um token (por exemplo, o do utilizador para ler a inbox).

### `applicationCode` [#applicationcode]

O serviço lê a aplicação do claim `application_code` do token; se o token não o tiver, usa `request.setApplicationCode(...)` (escritas) ou o argumento `applicationCode` das sobrecargas de leitura (`get(id, applicationCode)`, `list(..., applicationCode)`). Sem nenhum dos dois a resposta é `400 APPLICATION_CODE_REQUIRED`.

---

## Enviar com template [#enviar-com-template]

<Callout type="warning" title="EMAIL precisa de SMTP configurado">
Para enviar por **EMAIL**, o provider SMTP tem de estar configurado no Notification Service (`NOTIFICATION_PROVIDERS_EMAILSMTP_*`: servidor, porta, credenciais e remetente). Sem ele, o envio é recusado com `422 CHANNEL_NOT_ENABLED` e nenhum email sai — ver [Configurar os canais › EMAIL (SMTP)](/docs/developer/notification#email-smtp). O canal também tem de estar ativo nas [Definições da aplicação](/docs/developer/notification/admin-ui#definições).
</Callout>

Os modelos são JavaBeans (construtor vazio + setters):

```java
TemplateRefDto template = new TemplateRefDto();
template.setCode("pedido-confirmacao");
template.setLocale("pt-CV");

RecipientDto recipient = new RecipientDto();
recipient.setEmail(email);

SendNotificationRequest request = new SendNotificationRequest();
request.setChannelStrategy(ChannelStrategy.SPECIFIC);
request.setChannels(List.of(Channel.EMAIL));
request.setRecipients(List.of(recipient));
request.setTemplate(template);
request.setVariables(Map.of("nome", nome, "numeroPedido", referencia));
request.setBusinessKey(referencia);
request.setIdempotencyKey("pedido-confirmacao:" + referencia);

NotificationResponse response = new NotificationsApi(client).send(request);
UUID id = response.getId();   // o serviço responde 202: a entrega é assíncrona
```

| Modelo | Campos |
|---|---|
| `SendNotificationRequest` | `channelStrategy` (**obrigatório**), `channels`, `recipients`, `groups`, `template` **ou** `inline`, `variables`, `priority`, `locale`, `attachments`, `category`, `businessKey`, `correlationId`, `idempotencyKey`, `callbackUrl`, `scheduledAt`, `expiresAt`, `applicationCode` |
| `RecipientDto` | `userId`, `email`, `phone`, `pushToken` |
| `GroupRefDto` | um de `groupId`, `departmentCode`, `roleCode`, `permissionName` |
| `TemplateRefDto` | `code`, `locale` |
| `InlineContentDto` | `subject`, `title`, `body` |
| `AttachmentRefDto` | `filename`, `contentRef` |

Enumerados (`cv.igrp.platform.notification.client.constants`): `ChannelStrategy` = `SPECIFIC`, `ALL`, `PREFERRED`; `Channel` = `EMAIL`, `SMS`, `WHATSAPP`, `PUSH`, `IN_APP`, `OFFLINE`; `Priority` = `LOW`, `NORMAL`, `HIGH`, `URGENT`.

<Callout type="info" title="O locale do template tem de existir">
O serviço procura o template publicado com o locale exato e não faz fallback. Sem ele responde `422 TEMPLATE_ERROR` com a mensagem `No PUBLISHED template…`. O Access Management repete o envio uma vez com o locale por omissão do módulo antes de desistir.
</Callout>

### Envio em massa [#envio-em-massa]

`new BulkApi(client).bulk(request)` envia o mesmo pedido a muitos destinatários e devolve `BulkResponse` (`batchId`, `notificationId`, `totalDeliveries`, `deliveries`). `bulkCsv(request, csvBytes, "destinatarios.csv")` lê os destinatários de um CSV. O progresso consulta-se em `BatchesApi`.

### Anexos [#anexos]

<Callout type="warning" title="AttachmentsApi.upload não envia multipart">
`upload(byte[], filename)` manda os bytes como corpo com `Content-Type: multipart/form-data`, sem partes. O endpoint espera um multipart com a parte `file`. Faz o upload com um cliente HTTP multipart (por exemplo `RestClient` com `MultipartBodyBuilder`), com o mesmo token, e usa o `contentRef` devolvido em `AttachmentRefDto`.
</Callout>

---

## Erros [#erros]

As chamadas lançam `ApiException`:

| Método | Devolve |
|---|---|
| `getCode()` | Estado HTTP |
| `getErrorCode()` | Código do serviço (`String`), por exemplo `TEMPLATE_ERROR` |
| `getErrorCodeEnum()` | O mesmo como `IgrpErrorCode` (`null` se desconhecido) |
| `getErrorMessage()` | Mensagem do serviço |

Códigos mais comuns (`IgrpErrorCode`):

| Código | HTTP |
|---|---|
| `VALIDATION_ERROR`, `INVALID_REQUEST`, `APPLICATION_CODE_REQUIRED` | 400 |
| `APPLICATION_ACCESS_DENIED`, `ATTACHMENT_ACCESS_DENIED`, `MISSING_USER_ID` | 403 |
| `TEMPLATE_NOT_FOUND`, `NOTIFICATION_NOT_FOUND`, `ATTACHMENT_NOT_FOUND`, … | 404 |
| `ATTACHMENT_TOO_LARGE` | 413 |
| `TEMPLATE_ERROR`, `CHANNEL_NOT_ENABLED`, `CHANNEL_NOT_ALLOWED`, `CALLBACK_DOMAIN_NOT_ALLOWED`, `ACCESS_MANAGER_UNAVAILABLE` | 422 |
| `QUOTA_EXCEEDED` | 429 |

---

## Configurar num serviço Spring Boot [#configurar-num-serviço-spring-boot]

O cliente não traz propriedades nem auto-configuração. O padrão usado no Access Management:

```java
@ConfigurationProperties(prefix = "app.notification")
public record NotificationProperties(
        boolean enabled, String baseUrl, String authUrl,
        String clientId, String clientSecret, String scope) {}
```

```java
@Configuration
@EnableConfigurationProperties(NotificationProperties.class)
public class NotificationClientConfig {

    @Bean
    @ConditionalOnProperty(name = "app.notification.enabled", havingValue = "true")
    ApiClient notificationApiClient(NotificationProperties props) {
        ApiClient client = new ApiClient();
        client.setBaseUrl(props.baseUrl());
        client.setClientCredentials(props.authUrl(), props.clientId(), props.clientSecret(), props.scope());
        return client;
    }

    @Bean
    @ConditionalOnProperty(name = "app.notification.enabled", havingValue = "true")
    NotificationsApi notificationsApi(ApiClient notificationApiClient) {
        return new NotificationsApi(notificationApiClient);
    }
}
```

```properties
app.notification.enabled=${NOTIFICATION_ENABLED:false}
app.notification.base-url=${NOTIFICATION_SERVICE_BASE_URL:}
app.notification.auth-url=${IGRP_ACCESS_MANAGEMENT_API:}
app.notification.client-id=${IGRP_M2M_CLIENT_ID:}
app.notification.client-secret=${IGRP_M2M_CLIENT_SECRET:}
app.notification.scope=
```

Com `enabled=false` os beans não são criados: injeta `Optional<NotificationsApi>` no serviço que envia e trata a ausência (sem envio, ou um envio alternativo). O prefixo `app.notification` é um exemplo; usa o da tua aplicação.


# Anexos

URL: /pt/docs/developer/notification/referencia-api/anexos

Carregar e descarregar os anexos de um envio.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Carregar anexo

`POST /api/v1/attachments`

Permissão: `igrp.notification.send`. Multipart (parte `file`). O `contentRef` devolvido vai em `attachments` no envio.

**Corpo do pedido** (`multipart/form-data`, `object`)

- `file` (string (binary), obrigatório)

**Respostas**

- `200` OK — `AttachmentUploadResponse`

## Descarregar anexo

`GET /api/v1/attachments/{id}`

Permissão: `igrp.notification.read`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `string (binary)`

# Configuração e administração

URL: /pt/docs/developer/notification/referencia-api/configuracao-e-administracao

Definições da aplicação, RGPD e retenção.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Obter definições da aplicação

`GET /api/v1/settings`

Permissão: `igrp.notification.admin`. Canais, remetentes, quotas, domínios de callback e retenção.

**Parâmetros**

- `applicationCode` (query, string)

**Respostas**

- `200` OK — `ApplicationSettingsResponse`

## Atualizar definições da aplicação

`PUT /api/v1/settings`

Permissão: `igrp.notification.admin`.

**Corpo do pedido** (`application/json`, `ApplicationSettingsRequest`)

- `allowedChannels` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `senders` (SenderIdentityDto[])
- `defaultPreferredOrder` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `quotas` (QuotaDto[])
- `allowedCallbackDomains` (string[])
- `defaultCallbackUrl` (string)
- `retention` (RetentionPolicyDto)
- `applicationCode` (string)

**Respostas**

- `200` OK — `ApplicationSettingsResponse`

## Anonimizar utilizador (RGPD)

`POST /api/v1/admin/recipients/{userId}/forget`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `userId` (path, string, obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `object`

## Apagar dados fora da retenção

`POST /api/v1/admin/retention/purge`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `applicationCode` (query, string)

**Respostas**

- `200` OK — `object`

# Destinatários e grupos

URL: /pt/docs/developer/notification/referencia-api/destinatarios-e-grupos

Contactos, preferências, opt-outs e grupos de destinatários.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Obter destinatário

`GET /api/v1/recipients/{userId}`

Permissão: `igrp.notification.admin`. Contactos e preferências de um utilizador.

**Parâmetros**

- `userId` (path, string, obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecipientResponse`

## Remover destinatário

`DELETE /api/v1/recipients/{userId}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `userId` (path, string, obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK

## Criar ou atualizar destinatário

`PUT /api/v1/recipients/{userId}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `userId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `RecipientRequest`)

- `contacts` (ContactPointDto[])
- `pushTokens` (string[])
- `preferredChannels` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `optOuts` (OptOutDto[])
- `applicationCode` (string)

**Respostas**

- `200` OK — `RecipientResponse`

## Canais preferidos

`PUT /api/v1/recipients/{userId}/preferences`

Permissão: `igrp.notification.admin`. Usados pela estratégia `PREFERRED`.

**Parâmetros**

- `userId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `PreferredChannelsRequest`)

- `preferredChannels` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `applicationCode` (string)

**Respostas**

- `200` OK — `RecipientResponse`

## Opt-outs

`PUT /api/v1/recipients/{userId}/opt-outs`

Permissão: `igrp.notification.admin`. Por canal e categoria.

**Parâmetros**

- `userId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `OptOutsRequest`)

- `optOuts` (OptOutDto[])
- `applicationCode` (string)

**Respostas**

- `200` OK — `RecipientResponse`

## Listar grupos

`GET /api/v1/groups`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecipientGroupResponse[]`

## Criar grupo

`POST /api/v1/groups`

Permissão: `igrp.notification.admin`.

**Corpo do pedido** (`application/json`, `RecipientGroupRequest`)

- `name` (string, obrigatório)
- `type` ("INTERNAL" | "EXTERNAL_DEPARTMENT", obrigatório)
- `memberUserIds` (string[])
- `externalGroupRef` (string)
- `applicationCode` (string)

**Respostas**

- `200` OK — `RecipientGroupResponse`

## Obter grupo

`GET /api/v1/groups/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecipientGroupResponse`

## Remover grupo

`DELETE /api/v1/groups/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK

## Atualizar grupo

`PUT /api/v1/groups/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Corpo do pedido** (`application/json`, `RecipientGroupRequest`)

- `name` (string, obrigatório)
- `type` ("INTERNAL" | "EXTERNAL_DEPARTMENT", obrigatório)
- `memberUserIds` (string[])
- `externalGroupRef` (string)
- `applicationCode` (string)

**Respostas**

- `200` OK — `RecipientGroupResponse`

# Entregas e estatísticas

URL: /pt/docs/developer/notification/referencia-api/entregas-e-estatisticas

Estado manual das entregas OFFLINE e contagens agrupadas.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Mudar estado de uma entrega OFFLINE

`POST /api/v1/deliveries/{id}/status`

Permissão: `igrp.notification.manual_status`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Corpo do pedido** (`application/json`, `ManualStatusChangeRequest`)

- `status` ("PENDING" | "QUEUED" | "SENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED" | "CANCELLED" | "EXPIRED", obrigatório)
- `reason` (string)
- `applicationCode` (string)

**Respostas**

- `200` OK — `object`

## Estatísticas de entregas

`GET /api/v1/stats/deliveries`

Permissão: `igrp.notification.read`. `groupBy` é obrigatório: `CHANNEL`, `STATUS` ou `APPLICATION`.

**Parâmetros**

- `groupBy` (query, "CHANNEL" | "STATUS" | "APPLICATION", obrigatório)
- `from` (query, string (date-time))
- `to` (query, string (date-time))
- `allApplications` (query, boolean)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `DeliveryStatBucket[]`

# Inbox

URL: /pt/docs/developer/notification/referencia-api/inbox

Notificações IN_APP do utilizador autenticado.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Listar a inbox

`GET /api/v1/inbox`

Permissão: `igrp.notification.inbox`. Token do utilizador: o `userId` sai do token. Filtros `unreadOnly` e `includeArchived`; página de 20 por omissão.

**Parâmetros**

- `unreadOnly` (query, boolean)
- `includeArchived` (query, boolean)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageResponseInboxItemResponse`

## Contar não lidas

`GET /api/v1/inbox/unread-count`

Permissão: `igrp.notification.inbox`. Token do utilizador: o `userId` sai do token.

**Respostas**

- `200` OK — `object`

## Marcar como lida

`POST /api/v1/inbox/{deliveryId}/read`

Permissão: `igrp.notification.inbox`. Token do utilizador: o `userId` sai do token.

**Parâmetros**

- `deliveryId` (path, string (uuid), obrigatório)

**Respostas**

- `200` OK

## Marcar todas como lidas

`POST /api/v1/inbox/read`

Permissão: `igrp.notification.inbox`. Token do utilizador: o `userId` sai do token.

**Respostas**

- `200` OK

## Arquivar

`POST /api/v1/inbox/{deliveryId}/archive`

Permissão: `igrp.notification.inbox`. Token do utilizador: o `userId` sai do token.

**Parâmetros**

- `deliveryId` (path, string (uuid), obrigatório)

**Respostas**

- `200` OK

# Notificações e lotes

URL: /pt/docs/developer/notification/referencia-api/notificacoes-e-lotes

Enviar, listar, reenviar, envios em massa e lotes.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Listar notificações

`GET /api/v1/notifications`

Permissão: `igrp.notification.read`. Filtros por `businessKey`, `category`, `priority`, `channel`, `status`, `batchId`, `recurrenceId`, `recipient` e intervalo `createdFrom`/`createdTo` (ISO 8601).

**Parâmetros**

- `businessKey` (query, string)
- `category` (query, string)
- `priority` (query, "LOW" | "NORMAL" | "HIGH" | "URGENT")
- `channel` (query, "EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE")
- `status` (query, "PENDING" | "QUEUED" | "SENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED" | "CANCELLED" | "EXPIRED")
- `batchId` (query, string (uuid))
- `recurrenceId` (query, string (uuid))
- `recipient` (query, string)
- `createdFrom` (query, string (date-time))
- `createdTo` (query, string (date-time))
- `applicationCode` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageResponseNotificationSummaryResponse`

## Enviar notificação

`POST /api/v1/notifications`

Permissão: `igrp.notification.send`. Pedido M2M (token `client_credentials`). Responde 202 com o `id` e as entregas; a entrega é assíncrona. EMAIL exige o provider SMTP configurado (senão `422 CHANNEL_NOT_ENABLED`).

**Corpo do pedido** (`application/json`, `SendNotificationRequest`)

- `businessKey` (string)
- `category` (string)
- `priority` ("LOW" | "NORMAL" | "HIGH" | "URGENT")
- `channelStrategy` ("SPECIFIC" | "ALL" | "PREFERRED", obrigatório)
- `channels` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `recipients` (RecipientDto[])
- `groups` (GroupRefDto[])
- `template` (TemplateRefDto)
- `inline` (InlineContentDto)
- `variables` (object)
- `locale` (string)
- `attachments` (AttachmentRefDto[])
- `idempotencyKey` (string)
- `correlationId` (string)
- `callbackUrl` (string)
- `scheduledAt` (string (date-time))
- `expiresAt` (string (date-time))
- `applicationCode` (string)

**Respostas**

- `200` OK — `NotificationResponse`

## Obter notificação

`GET /api/v1/notifications/{id}`

Permissão: `igrp.notification.read`. Inclui as entregas e o conteúdo renderizado.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `NotificationDetailResponse`

## Reenviar notificação

`POST /api/v1/notifications/{id}/resend`

Permissão: `igrp.notification.send`. Canais e destinatários opcionais.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Corpo do pedido** (`application/json`, `ResendNotificationRequest`)

- `channels` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `recipients` (RecipientDto[])
- `correlationId` (string)
- `applicationCode` (string)

**Respostas**

- `200` OK — `NotificationResponse`

## Envio em massa

`POST /api/v1/notifications/bulk`

Permissão: `igrp.notification.send`. O mesmo pedido para muitos destinatários; devolve o `batchId`.

**Corpo do pedido** (`application/json`, `SendNotificationRequest`)

- `businessKey` (string)
- `category` (string)
- `priority` ("LOW" | "NORMAL" | "HIGH" | "URGENT")
- `channelStrategy` ("SPECIFIC" | "ALL" | "PREFERRED", obrigatório)
- `channels` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE"[])
- `recipients` (RecipientDto[])
- `groups` (GroupRefDto[])
- `template` (TemplateRefDto)
- `inline` (InlineContentDto)
- `variables` (object)
- `locale` (string)
- `attachments` (AttachmentRefDto[])
- `idempotencyKey` (string)
- `correlationId` (string)
- `callbackUrl` (string)
- `scheduledAt` (string (date-time))
- `expiresAt` (string (date-time))
- `applicationCode` (string)

**Respostas**

- `200` OK — `BulkResponse`

## Envio em massa por CSV

`POST /api/v1/notifications/bulk/csv`

Permissão: `igrp.notification.send`. Multipart: parte `request` (JSON) e parte `file` (CSV).

**Corpo do pedido** (`multipart/form-data`, `object`)

- `request` (SendNotificationRequest, obrigatório)
- `file` (string (binary), obrigatório)

**Respostas**

- `200` OK — `BulkResponse`

## Progresso do lote

`GET /api/v1/batches/{batchId}`

Permissão: `igrp.notification.read`.

**Parâmetros**

- `batchId` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `BatchProgressResponse`

## Cancelar lote

`POST /api/v1/batches/{batchId}/cancel`

Permissão: `igrp.notification.send`. Cancela as entregas ainda pendentes.

**Parâmetros**

- `batchId` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `BatchProgressResponse`

# Recorrências

URL: /pt/docs/developer/notification/referencia-api/recorrencias

Envios periódicos e as suas execuções.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Listar recorrências

`GET /api/v1/recurrences`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecurrenceResponse[]`

## Criar recorrência

`POST /api/v1/recurrences`

Permissão: `igrp.notification.admin`.

**Corpo do pedido** (`application/json`, `RecurrenceRequest`)

- `cron` (string)
- `intervalSeconds` (integer (int64))
- `startAt` (string (date-time))
- `endAt` (string (date-time))
- `notification` (SendNotificationRequest, obrigatório)
- `applicationCode` (string)

**Respostas**

- `200` OK — `RecurrenceResponse`

## Obter recorrência

`GET /api/v1/recurrences/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecurrenceResponse`

## Terminar recorrência

`DELETE /api/v1/recurrences/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecurrenceResponse`

## Pausar recorrência

`POST /api/v1/recurrences/{id}/pause`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecurrenceResponse`

## Retomar recorrência

`POST /api/v1/recurrences/{id}/resume`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `RecurrenceResponse`

## Execuções da recorrência

`GET /api/v1/recurrences/{id}/executions`

Permissão: `igrp.notification.read`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageResponseNotificationSummaryResponse`

# Templates

URL: /pt/docs/developer/notification/referencia-api/templates

Criar, publicar, arquivar e pré-visualizar templates.

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Listar templates

`GET /api/v1/templates`

Permissão: `igrp.notification.admin`. Filtros `code`, `channel`, `status` e `locale`. Para encontrar um template publicado por código, filtra com `code` e `status=PUBLISHED`.

**Parâmetros**

- `code` (query, string)
- `channel` (query, "EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE")
- `status` (query, "DRAFT" | "PUBLISHED" | "ARCHIVED")
- `locale` (query, string)
- `applicationCode` (query, string)
- `pageable` (query, Pageable, obrigatório)

**Respostas**

- `200` OK — `PageResponseTemplateResponse`

## Criar template (rascunho)

`POST /api/v1/templates`

Permissão: `igrp.notification.admin`.

**Corpo do pedido** (`application/json`, `CreateTemplateRequest`)

- `code` (string, obrigatório)
- `channel` ("EMAIL" | "SMS" | "WHATSAPP" | "PUSH" | "IN_APP" | "OFFLINE", obrigatório)
- `locale` (string)
- `subject` (string)
- `body` (string)
- `title` (string)
- `variables` (VariableDefDto[])
- `applicationCode` (string)

**Respostas**

- `200` OK — `TemplateResponse`

## Obter template

`GET /api/v1/templates/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `TemplateResponse`

## Atualizar template

`PUT /api/v1/templates/{id}`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)

**Corpo do pedido** (`application/json`, `UpdateTemplateRequest`)

- `subject` (string)
- `body` (string)
- `title` (string)
- `variables` (VariableDefDto[])
- `applicationCode` (string)

**Respostas**

- `200` OK — `TemplateResponse`

## Publicar template

`POST /api/v1/templates/{id}/publish`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `TemplateResponse`

## Arquivar template

`POST /api/v1/templates/{id}/archive`

Permissão: `igrp.notification.admin`.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Respostas**

- `200` OK — `TemplateResponse`

## Pré-visualizar template

`POST /api/v1/templates/{id}/preview`

Permissão: `igrp.notification.admin`. Renderiza com variáveis de teste.

**Parâmetros**

- `id` (path, string (uuid), obrigatório)
- `applicationCode` (query, string)

**Corpo do pedido** (`application/json`, `PreviewTemplateRequest`)

- `variables` (object)

**Respostas**

- `200` OK — `RenderedContentResponse`

# Webhooks dos providers

URL: /pt/docs/developer/notification/referencia-api/webhooks-dos-providers

Recibos enviados pelos providers (sem token).

Servidor: `http://{host}/gateway-api/igrp-notification-service`

## Verificar webhook

`GET /webhooks/{providerId}`

Sem token. Verificação do webhook (parâmetros `hub.*`).

Sem autenticação.

**Parâmetros**

- `providerId` (path, string, obrigatório)
- `hub.mode` (query, string)
- `hub.verify_token` (query, string)
- `hub.challenge` (query, string)

**Respostas**

- `200` OK — `string`

## Receber recibos do provider

`POST /webhooks/{providerId}`

Sem token: a autenticidade é validada por provider. Recibos de entrega e leitura.

Sem autenticação.

**Parâmetros**

- `providerId` (path, string, obrigatório)

**Corpo do pedido** (`*/*`, `string`)

**Respostas**

- `200` OK — `object`

# Templates — Notifications UI

URL: /pt/docs/developer/notification/templates

Templates de notificação — o que identifica um template, ciclo de vida, variáveis Handlebars, criação na consola e pela API, e como o envio escolhe a versão.

# Templates — Notifications UI [#templates--notifications-ui]

Um template guarda o texto de uma notificação fora do código da aplicação. A aplicação envia o **código** do template e os valores das variáveis; o serviço escolhe a versão publicada e renderiza o conteúdo.

---

## O que identifica um template [#o-que-identifica-um-template]

Um template é **um canal e um locale**. A combinação `aplicação + código + canal + locale` identifica-o:

| Campo | Exemplo | Notas |
|---|---|---|
| `applicationCode` | `REGISTOS` | Aplicação do Access Management dona do template |
| `code` | `pedido-confirmacao` | O que a aplicação envia em `template.code` |
| `channel` | `EMAIL` | Um template por canal: para IN_APP cria outro com o mesmo código |
| `locale` | `pt-CV` | Um template por locale |
| `subject` | `Pedido {{numeroPedido}} recebido` | EMAIL |
| `title` | `Pedido recebido` | PUSH e IN_APP |
| `body` | HTML (EMAIL, IN_APP) ou texto | Handlebars |
| `variables` | `[{ name, type, required, example }]` | Declaração opcional das variáveis |

<Callout type="warning" title="O locale tem de coincidir">
O envio procura o template publicado com o **locale exato** do pedido. Não há fallback de `pt-CV` para `pt` nem para um locale por omissão: sem correspondência, o envio falha com `422 TEMPLATE_ERROR` (`No PUBLISHED template…`). Publica o template em cada locale que usas, ou envia sempre o mesmo locale.
</Callout>

---

## Ciclo de vida [#ciclo-de-vida]

| Estado | Significado |
|---|---|
| `DRAFT` | Em edição. Não é usado nos envios. |
| `PUBLISHED` | Usado nos envios. |
| `ARCHIVED` | Retirado. Não é usado nos envios. |

Editar um template publicado cria uma **nova versão** em `DRAFT` (`v2`, `v3`, …). Ao publicá-la, a consola arquiva a versão anterior, para ficar só uma publicada por código, canal e locale.

---

## Variáveis [#variáveis]

As variáveis usam Handlebars: `{{nome}}`. Os valores vêm do campo `variables` do envio:

```json
{
  "template": { "code": "pedido-confirmacao", "locale": "pt-CV" },
  "variables": { "nome": "Maria Silva", "numeroPedido": "PED-2026-0456" }
}
```

- Uma variável sem valor fica **vazia** no texto. Declara-a com `required: true` para o envio falhar (`TEMPLATE_ERROR`) em vez de sair incompleto.
- Em EMAIL e IN_APP os valores são escapados e o HTML final é sanitizado (sem `<script>`, por exemplo). `subject` e `title` são texto simples.

---

## Criar na consola [#criar-na-consola]

A [Notifications UI](./admin-ui) cria templates com um assistente de dois passos e um editor visual.

**1. Metadados.** Aplicação, código, canal, idioma, assunto (EMAIL) e título (PUSH / IN_APP):

<StudioPreview imageName="ntTemplateNovo1" alt="Assistente Novo template com aplicação registos, código pedido-confirmacao, canal EMAIL, idioma Português (Cabo Verde), assunto Pedido {{numeroPedido}} recebido e título Pedido recebido" />

**2. Ponto de partida.** Canvas em branco ou um dos exemplos do editor:

<StudioPreview imageName="ntTemplateNovo2" alt="Passo Escolher ponto de partida com as opções Em branco, Sample visual template, Recibo de Compra E-commerce e Boletim Informativo Semanal" />

**3. Editor.** *Criar e abrir editor* grava o `DRAFT` e abre o editor visual. Adiciona linhas e blocos (texto, título, botão, imagem…) e escreve as variáveis no texto. *Guardar* grava a versão; *Publicar* torna-a ativa:

<StudioPreview imageName="ntTemplateEditor" alt="Editor visual do template pedido-confirmacao (EMAIL, DRAFT, v1, REGISTOS) com um bloco de texto que usa {{nome}} e {{numeroPedido}} e o painel de propriedades do bloco" />

Depois de publicado, o template aparece na lista com o estado **Publicado**:

<StudioPreview imageName="ntTemplatesLista" alt="Lista de templates da aplicação registos com pedido-confirmacao, idioma pt-CV, versão 1, estado Publicado" />

---

## Criar pela API [#criar-pela-api]

```http
POST /api/v1/templates
Authorization: Bearer <token com igrp.notification.admin>
Content-Type: application/json

{
  "applicationCode": "REGISTOS",
  "code": "pedido-confirmacao",
  "channel": "EMAIL",
  "locale": "pt-CV",
  "subject": "Pedido {{numeroPedido}} recebido",
  "body": "<p>Olá {{nome}},</p><p>Recebemos o seu pedido <b>{{numeroPedido}}</b>.</p>",
  "variables": [
    { "name": "nome", "type": "STRING", "required": true, "example": "Maria Silva" },
    { "name": "numeroPedido", "type": "STRING", "required": true, "example": "PED-2026-0456" }
  ]
}
```

A resposta traz o `id` do rascunho. Para o ativar:

```http
POST /api/v1/templates/{id}/publish
```

Para ver o resultado antes de publicar, `POST /api/v1/templates/{id}/preview` com `{ "variables": { … } }` devolve `subject`, `title` e `body` renderizados.

Todos os endpoints de templates exigem `igrp.notification.admin` — ver [API REST › Templates](./api#templates).

### Encontrar um template pelo código [#encontrar-um-template-pelo-código]

Não há endpoint "obter por código". Lista com filtros:

```ts
const { data } = await client.templates.list(
  { code: 'pedido-confirmacao', status: TemplateStatus.PUBLISHED, locale: 'pt-CV', channel: Channel.EMAIL },
  { page: 0, size: 5 },
  'REGISTOS',
);
```

```java
PageResponseTemplateResponse page = new TemplatesApi(client).list(
    "pedido-confirmacao", Channel.EMAIL, TemplateStatus.PUBLISHED, "pt-CV", new Pageable(0, 5));
```

---

## Templates da plataforma [#templates-da-plataforma]

O serviço publica no arranque os templates de convite usados pelo Access Management (aplicação `APP_IGRP_CENTER`), em `pt` e `en`: `user-invitation`, `user-invitation-cancelled`, `user-invitation-responded` e `user-invitation-otp`. Quando estes ficheiros mudam numa versão nova do serviço, é publicada uma versão nova do template, que passa a ser a usada; as versões editadas à mão continuam guardadas. Desliga com `notification.templates.seed-defaults=false`.


# Process Management · API REST

URL: /pt/docs/developer/process/api-management

API REST do Process Management — áreas, definições publicadas, instâncias e tarefas — com a base, a autenticação e exemplos reais.

# Process Management API [#process-management-api]

O `process-management-api` é o runtime dos processos: guarda as definições publicadas (motor Activiti), cria instâncias e gere tarefas. É o que o [Process Management](./management.mdx) e as apps de execução usam — normalmente através do [SDK TypeScript](./sdk-management.mdx).

| | |
|---|---|
| **Base (workspace)** | `http://<slug>:2575/gateway-api/<serviço>-process-management-api` |
| **Prefixo** | Nenhum — os recursos ficam na raiz (`/process-instances`, não `/api/v1/...`) |
| **Stack** | Spring Boot 4.1 · Java 26 · Activiti · PostgreSQL (Flyway) |
| **Referência** | [Gerada do OpenAPI do serviço](./referencia-process-management/areas.mdx) (springdoc em `/v3/api-docs`) |

O `<serviço>` é o nome com que o serviço está registado no Eureka; no gateway a rota é `/gateway-api/<nome-eureka>`.

---

## Autenticação [#autenticação]

Todos os pedidos levam um JWT do Access Management:

```http
Authorization: Bearer <token>
```

Nas apps é o token do utilizador (sessão NextAuth). Entre serviços usa um token `client_credentials` — ver [API M2M](../access-management/m2m-api.mdx).

---

## Referência [#referência]

Cada operação, com parâmetros, corpo do pedido, respostas e exemplos em várias linguagens, está na referência gerada a partir do OpenAPI do serviço:

<Cards>
  <Card title="Áreas" href="/docs/developer/process/referencia-process-management/areas">Áreas e os processos associados a cada uma.</Card>
  <Card title="Definições de processo" href="/docs/developer/process/referencia-process-management/definicoes-de-processo">Definições publicadas, artefactos das tarefas, numeração e prioridades.</Card>
  <Card title="Instâncias" href="/docs/developer/process/referencia-process-management/instancias">Criar, arrancar, pesquisar e cancelar instâncias.</Card>
  <Card title="Tarefas" href="/docs/developer/process/referencia-process-management/tarefas">Pesquisar, assumir, atribuir, guardar e concluir tarefas.</Card>
  <Card title="Atividades" href="/docs/developer/process/referencia-process-management/atividades">Eventos e progresso de uma instância.</Card>
</Cards>

<Callout type="info" title="Testar no playground">
O botão **Testar no playground** envia o pedido a partir do browser para o servidor indicado (por omissão, o gateway do workspace em `localhost:2575`). Preenche o `host` e o `servico` do teu ambiente e um token em **Autorização**. O gateway tem de aceitar pedidos (CORS) vindos da origem da documentação; se não aceitar, usa os exemplos `curl`.
</Callout>

---

## Exemplos [#exemplos]

### Iniciar uma instância [#iniciar-uma-instância]

```bash
curl -X POST "$PM/process-instances" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "processKey": "registo-entidade",
    "applicationBase": "registos",
    "priority": 1,
    "variables": [
      { "name": "nomeEntidade", "value": "Exemplo Serviços, Lda." }
    ]
  }'
```

`processKey` e `applicationBase` são obrigatórios; `businessKey`, `priority`, `variables` e `assignmentRules` são opcionais.

### Concluir uma tarefa [#concluir-uma-tarefa]

```bash
curl -X POST "$PM/tasks-instances/$TASK_ID/complete" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "variables": [
      { "name": "decision", "value": "CONFIRMAR" },
      { "name": "parecer", "value": "Documentação em ordem" }
    ],
    "forms": []
  }'
```

`variables` vão para o processo (e para as condições dos gateways); `forms` guarda os dados do formulário associados à tarefa. `save` aceita o mesmo corpo.

### Tarefas de uma instância [#tarefas-de-uma-instância]

```bash
curl -X POST "$PM/tasks-instances/search?processInstanceId=$INSTANCE_ID" \
  -H "Authorization: Bearer $TOKEN"
```


# Process Studio · API REST

URL: /pt/docs/developer/process/api-studio

API REST do Process Studio — projetos, definições de processo, diagrama e publicação — com a base, a autenticação e exemplos reais.

# Process Studio API [#process-studio-api]

O `process-studio-api` guarda os projetos e os rascunhos BPMN e publica-os no [Process Management](./api-management.mdx). É usado pelo [Process Studio](./studio.mdx) e pelo separador BPMN do Studio IDE — normalmente através do [SDK TypeScript](./sdk-studio.mdx).

| | |
|---|---|
| **Base (workspace)** | `http://<slug>:2575/gateway-api/<serviço>-process-studio-api` |
| **Prefixo** | `/api/v1` (está no caminho dos recursos) |
| **Stack** | Spring Boot 3.5 · Java 26 · PostgreSQL |
| **Referência** | [Gerada do OpenAPI do serviço](./referencia-process-studio/projetos.mdx) (springdoc em `/v3/api-docs`) |

O `<serviço>` é o nome com que o serviço está registado no Eureka.

---

## Autenticação [#autenticação]

```http
Authorization: Bearer <token>
```

O token é o do utilizador (Access Management). Ao publicar, o Process Studio reencaminha-o ao Process Management.

---

## Referência [#referência]

Cada operação, com parâmetros, corpo do pedido, respostas e exemplos em várias linguagens, está na referência gerada a partir do OpenAPI do serviço:

<Cards>
  <Card title="Projetos" href="/docs/developer/process/referencia-process-studio/projetos">Projetos de processos e os processos publicados de cada um.</Card>
  <Card title="Definições de processo" href="/docs/developer/process/referencia-process-studio/definicoes-de-processo">Criar processos, guardar o diagrama, declarar variáveis e publicar.</Card>
  <Card title="Parametrização" href="/docs/developer/process/referencia-process-studio/parametrizacao">Valores de referência, como os estados de uma definição.</Card>
</Cards>

<Callout type="info" title="Testar no playground">
O botão **Testar no playground** envia o pedido a partir do browser para o servidor indicado (por omissão, o gateway do workspace em `localhost:2575`). Preenche o `host` e o `servico` do teu ambiente e um token em **Autorização**. O gateway tem de aceitar pedidos (CORS) vindos da origem da documentação; se não aceitar, usa os exemplos `curl`.
</Callout>

---

## Exemplos [#exemplos]

### Criar um projeto e um processo [#criar-um-projeto-e-um-processo]

```bash
curl -X POST "$PS/api/v1/projects" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "code": "REGISTOS", "name": "Registos", "description": "Processos de registo de entidades" }'

curl -X POST "$PS/api/v1/projects/$PROJECT_ID/process-definitions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "title": "Registo de Entidade", "processKey": "registo-entidade" }'
```

### Guardar o diagrama e publicar [#guardar-o-diagrama-e-publicar]

```bash
curl -X PUT "$PS/api/v1/projects/process-definitions/registo-entidade/diagram" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"content\": $(jq -Rs . < registo-entidade.bpmn)}"

curl -X POST "$PS/api/v1/projects/process-definitions/registo-entidade/deploy" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"content\": $(jq -Rs . < registo-entidade.bpmn)}"
```

<Callout type="warning" title="O XML tem de começar por <?xml">
O diagrama é validado ao guardar; conteúdo que não seja XML bem formado devolve *500 — SAXException … Content is not allowed in prolog*.
</Callout>


# Formulários — Studio IDE

URL: /pt/docs/developer/process/forms

Ligar o Studio IDE ao Process Studio, gerar os passos das User Tasks, desenhar os formulários, enviar as variáveis ao processo e instalar o runtime de processos na app.

# Formulários BPMN — Studio IDE [#formulários-bpmn--studio-ide]

Depois de [publicado no Process Studio](./studio.mdx), cada User Task com Form Key `ui:form:*` passa a ser um **artefacto** do processo. No **Studio IDE** esses artefactos viram **passos** (steps): componentes React gerados no teu projeto Next.js, desenhados no Page Builder como qualquer página. Em runtime, o [Process Management](./management.mdx) abre o passo certo para cada tarefa.

O fluxo tem quatro partes:

1. Ligar o separador **BPMN** do projeto ao Process Studio.
2. Gerar um passo por artefacto e desenhar o formulário.
3. Escrever o `handleComplete`, que devolve as variáveis ao processo.
4. Instalar o bundle **Process Management UI**, que traz as páginas de runtime.

O exemplo continua o **Registo de Entidade**, num projeto Next.js chamado `registos`.

---

## Ligar o Studio IDE ao Process Studio [#ligar-o-studio-ide-ao-process-studio]

No projeto, abre o separador **BPMN** e clica em **API** → **Add**. A configuração aponta para o API do Process Studio, através do gateway do workspace.

<StudioPreview imageName="ideBpmnApi" alt="Diálogo BPMN API Configuration com nome Demo Processos (local), API URL http://localhost:2575 e Base Path do process-studio-api" />

| Campo | Exemplo | Notas |
|---|---|---|
| **Configuration Name** | `Demo Processos (local)` | Só para identificar a ligação |
| **API URL** | `http://localhost:2575` | Gateway do workspace (porta do nginx) |
| **Base Path** | `/gateway-api/<serviço>-process-studio-api/api/v1` | Rota do gateway para o `process-studio-api`; o nome do serviço é o registado no Eureka |
| **Access Token** | — | Opcional |
| **Active Configuration** | ligado | Só uma ligação fica ativa; é usada em todas as vistas BPMN do Studio |

Clica em **Test Connection** e depois em **Save**.

<Callout type="info" title="Usa um host que o teu sistema resolve">
O teste de ligação corre no processo principal do Studio (Node), não no browser. Se o workspace usa um host como `http://<slug>:2575`, ele tem de estar no `/etc/hosts`; caso contrário usa `http://localhost:2575`.
</Callout>

---

## Escolher o processo [#escolher-o-processo]

Em **Select Project** escolhe o projeto do Process Studio (`Registos · REGISTOS`). A lista mostra só os processos **publicados**, com a versão e o número de artefactos.

<StudioPreview imageName="ideBpmnProcessos" alt="Separador BPMN do projeto registos com o projeto Registos selecionado e o processo Registo de Entidade v1 com 2 artefactos" />

Clica no processo para ver os **Process Artifacts**: um por cada User Task com `ui:form:*`, com o ID da tarefa, a versão e a Form Key.

<StudioPreview imageName="ideBpmnArtefactos" alt="Process Artifacts do Registo de Entidade: Pedido (Task_pedido, ui:form:pedido@1) e Qualificação (Task_qualificacao, ui:form:qualificacao@1) com Open Editor" />

<Callout type="info" title="Só formulários ui:form aparecem">
Tarefas com `ui:shared:*` usam o formulário partilhado `process/shared/<página>.v<versão>.tsx` (o bundle instala o `default.v1.tsx`); não aparecem nesta lista.
</Callout>

---

## Gerar o passo [#gerar-o-passo]

Num artefacto novo o botão diz **Generate Step**. Ao clicar, confirma a descrição e o nome do componente:

<StudioPreview imageName="ideGenerateStep" alt="Diálogo Configure New Step com Descrição do Componente e Nome do Componente Pedido" />

O Studio cria dois ficheiros e o botão passa a **Open Editor**:

| Ficheiro | Conteúdo |
|---|---|
| `.igrpstudio/process/<processKey>/<tarefa>.v<versão>.json` | Definição do passo (editada pelo Studio) |
| `src/app/(igrp)/(generated)/process/[...process]/(<processKey>)/<tarefa>.v<versão>.tsx` | Componente gerado — não editar à mão |

No exemplo: `(registo-entidade)/pedido.v1.tsx` e `(registo-entidade)/qualificacao.v1.tsx`.

---

## Desenhar o formulário [#desenhar-o-formulário]

**Open Editor** abre o passo no Page Builder. Começa por arrastar um **Form** (em *Contentores*): traz um *Headline* e uma *Grid*. Depois arrasta os campos para a grid e configura cada um na roda dentada da barra do componente — o **nome** (primeiro campo do painel) é a chave do valor no formulário, e é esse nome que chega ao processo como variável.

<StudioPreview imageName="ideFormPedido" alt="Passo Pedido no Page Builder com o título Pedido de registo e os campos Nome da entidade, Morada, NIF e Email" />

| Passo | Campos (nome → rótulo) |
|---|---|
| Pedido | `nomeEntidade` → Nome da entidade · `morada` → Morada · `nif` → NIF · `email` → Email |
| Qualificação | `parecer` → Parecer · `decision` → Decisão (Select) |

<StudioPreview imageName="ideFormQualificacao" alt="Passo Qualificação com os campos Parecer e Decisão" />

### Associar o Form a um esquema (obrigatório) [#associar-o-form-a-um-esquema-obrigatório]

Seleciona o Form (pela margem) e abre **Binding Config** na barra do componente. Em **New**, com **Definição → Auto**, o Studio propõe um esquema com os campos que já estão no Form: dá-lhe um nome (`pedido`, `qualificacao`) e marca os obrigatórios. **Guardar alterações** gera o esquema zod e liga-o ao Form (`schema={pedido}`).

<StudioPreview imageName="ideBindingConfig" alt="Configuração de Associação do Form com o nome pedido e os campos nomeEntidade, morada, nif e email" />

<Callout type="warning" title="Sem esquema o passo não abre">
O `IGRPForm` valida com zod e exige `schema`. Um Form sem Binding Config é gerado sem `schema` e, em runtime, a tarefa mostra *An error occurred in this section* (`Cannot use 'in' operator to search for '_zod' in undefined`).
</Callout>

O Select **Decisão** tem as opções que o gateway espera: `RECTIFICAR`, `CONFIRMAR` e `REJEITAR`.

<StudioPreview imageName="ideSelectOpcoes" alt="Painel do Select decision com as opções RECTIFICAR/Retificar, CONFIRMAR/Confirmar e REJEITAR/Rejeitar" />

<Callout type="warning" title="Opções estáticas do Select">
Nesta versão, as opções escritas na tabela **Options** do Select não chegam ao código gerado: o estado `select<nome>Options` nasce vazio. Até à correção, preenche o estado com as opções (por exemplo no `defaultValue` do estado em `.igrpstudio/process/<processKey>/<tarefa>.v<versão>.json`) e confirma no separador **Code** que o `useState` tem a lista.
</Callout>

---

## Devolver as variáveis ao processo [#devolver-as-variáveis-ao-processo]

Cada passo gerado regista dois métodos no runtime: `saveStep` (guardar sem concluir) e `completeStep` (concluir a tarefa). Eles chamam as funções **handleSave** e **handleComplete**, que se editam em **Código Personalizado** → *Functions* (lápis).

<StudioPreview imageName="ideHandleComplete" alt="Editor da função handleComplete com o código que lê os valores do formulário e os devolve como variáveis" />

O `handleComplete` lê os valores do Form e devolve-os como variáveis:

```ts
const data = formform1Ref.current?.getValues();

const variables = data
  ? Object.entries(data).map(([key, value]) => ({
      name: key,
      value: value as string,
    }))
  : [];

return {
  success: true,
  variables,
  forms: undefined,
};
```

Ao concluir a Qualificação, a variável `decision` fica no processo e o gateway escolhe o caminho: `RECTIFICAR` volta ao Pedido, `CONFIRMAR` ou `REJEITAR` terminam.

As variáveis ficam disponíveis em todos os passos seguintes:

- **Gateways** — nas condições (`${decision == 'RECTIFICAR'}`).
- **Service Tasks** — como parâmetros dos delegates.
- **Outros passos** — em `config.variables`, para pré-preencher ou mostrar dados de passos anteriores.

Grava com **Salvar** para regenerar o `.tsx`.

---

## Instalar o runtime de processos [#instalar-o-runtime-de-processos]

Os passos são só componentes; quem os abre é a página `process/[...process]` da app. Essa página vem no bundle **Process Management UI**: no projeto, separador **Bundles** → **Discover** → **Process Management UI** → **Instalar**.

<StudioPreview imageName="ideBundles" alt="Separador Bundles do projeto registos com o bundle Process Management UI e o botão Instalar" />

O bundle adiciona os pacotes `@igrp/platform-process-management-client-ts`, `-client-ui` e `-types` e copia para `src/app/(igrp)/(generated)/process/`:

| Rota | Para quê |
|---|---|
| `[...process]/page.tsx` + `layout.tsx` | Executa a tarefa: `/process/<processKey>/<instância>/<tarefa>/<id>` — escolhe o passo pela Form Key |
| `view/[...process]/` | Consulta só de leitura de uma instância |
| `resumo/[processInstanceId]/` | Resumo depois de concluir |
| `shared/default.v1.tsx` | Formulário de `ui:shared:default@1` |

A app precisa de saber onde está o API do Process Management:

```bash
# Rota do gateway para o process-management-api (sem /api/v1: os recursos ficam na raiz)
PROCESS_MANAGEMENT_CLIENT_BASE_URL=http://<slug>:2575/gateway-api/<serviço>-process-management-api
# Para onde volta o botão "Voltar" depois de concluir (Minhas Tarefas do Process Management)
IGRP_APP_PAGE_TASK=http://<slug>:2575/apps/igrp-process-management
```

Para avisar quem trata a etapa seguinte (IN_APP), a app precisa também do acesso ao Notification Service — ver [Notificações de tarefas](./management.mdx#notificações-de-tarefas).

<Callout type="warning" title="pnpm 11 e scripts de build ignorados">
Com pnpm 11, `pnpm add` termina com erro (`ERR_PNPM_IGNORED_BUILDS`) quando há dependências com scripts de build por aprovar, e o Studio dá a instalação do bundle como falhada. Declara-as no `pnpm-workspace.yaml` do projeto antes de instalar:

```yaml
allowBuilds:
  '@parcel/watcher': false
  '@swc/core': false
```
</Callout>

<Callout type="warning" title="Uma só versão do Design System">
O `@igrp/platform-process-management-client-ui` 0.1.0-beta.63 depende do Design System 0.2.0-beta.3, e o template usa a 0.2.0-beta.8. Com as duas versões instaladas a página da tarefa falha com *`Tooltip` must be used within `TooltipProvider`*. Força uma só versão no `pnpm-workspace.yaml` e reinstala do zero (apaga `node_modules` e `pnpm-lock.yaml`):

```yaml
overrides:
  '@igrp/igrp-framework-react-design-system': 0.2.0-beta.8
```
</Callout>

---

## Executar a tarefa [#executar-a-tarefa]

Quando o Process Management abre uma tarefa, a app mostra o cabeçalho do processo (número da instância, **Guardar** e **Concluir Tarefa**), a barra de passos e o formulário do passo atual:

<StudioPreview imageName="rtPedido" alt="Tarefa Pedido aberta na app registos, com o número REG-2026-22, Guardar, Concluir Tarefa, a barra Pedido/Qualificação e o formulário preenchido" />

**Guardar** chama `saveStep`; **Concluir Tarefa** chama `completeStep` — o teu `handleComplete` — e envia as variáveis. Na Qualificação, a decisão escolhida segue o caminho do gateway:

<StudioPreview imageName="rtQualificacao" alt="Tarefa Qualificação com o Pedido concluído na barra de passos, o parecer preenchido e a decisão Retificar" />

Com **Retificar**, o Pedido volta a ficar ativo com os valores anteriores; com **Confirmar** ou **Rejeitar**, a instância termina. As variáveis ficam visíveis no [detalhe da instância](./management.mdx#detalhe-da-instância).

---

## Próximo passo [#próximo-passo]

Com os passos gerados e o runtime instalado, associa o processo a uma área cuja **Aplicação** seja esta app e inicia instâncias no [Process Management](./management.mdx).


# Visão geral

URL: /pt/docs/developer/process

Como modelar, publicar e executar processos de negócio com BPMN no iGRP — do Process Studio ao runtime.

# Processos BPMN no iGRP [#processos-bpmn-no-igrp]

O iGRP executa processos de negócio descritos em **BPMN 2.0**. O ciclo passa por três ferramentas e termina numa app Next.js tua, onde vivem os formulários das tarefas.

<BpmnToolchain />

| Fase | Ferramenta | O que fazes |
| --- | --- | --- |
| ① Modelar | [Process Studio](./studio.mdx) | Criar o projeto e o processo, desenhar ou importar o BPMN, definir as Form Keys, publicar |
| ② Formulários | [Studio IDE — separador BPMN](./forms.mdx) | Ligar ao Process Studio, gerar um passo por Form Key, desenhar o formulário, instalar o runtime na app |
| ③ Executar | [Process Management](./management.mdx) | Configurar o processo, associá-lo a uma área, iniciar instâncias, operar tarefas e consultar o histórico |

O Process Studio e o Process Management fazem parte da [stack de processos do workspace](../workspace/stack-process.mdx). O motor guarda o BPMN com o namespace `activiti`; o editor trata da conversão.

---

## Conceitos-chave [#conceitos-chave]

**Processo** — definição BPMN: eventos, tarefas, gateways e as ligações entre eles. Identifica-se pela **chave** (`registo-entidade`).

**Versão** — cada publicação cria uma versão imutável (`v1`, `v2`…). As instâncias já iniciadas continuam na versão com que arrancaram.

**Instância** — uma execução concreta do processo, com número de negócio próprio (por exemplo `REG-2026-001`).

**User Task** — passo que espera por uma pessoa. A **Form Key** (`ui:form:pedido@1`) diz que formulário a executa.

**Passo (step)** — o componente React gerado pelo Studio IDE para uma Form Key `ui:form:*`. Ao concluir, devolve **variáveis** ao processo.

**Gateway** — ponto de decisão ou paralelismo (exclusivo, paralelo, inclusivo). As condições usam as variáveis do processo (`${decision == 'RECTIFICAR'}`).

**Área** — agrupamento do Mapa de Processos. A **Aplicação** da área indica em que app se abrem as tarefas.

---

## O exemplo destas páginas [#o-exemplo-destas-páginas]

As três páginas seguem o mesmo processo, **Registo de Entidade**:

1. **Pedido** — alguém preenche os dados da entidade (`ui:form:pedido@1`).
2. **Qualificação** — um técnico dá parecer e decide (`ui:form:qualificacao@1`).
3. **decisão** — `RECTIFICAR` devolve ao Pedido; `CONFIRMAR` ou `REJEITAR` terminam o processo.

---

## Por onde começar [#por-onde-começar]

- Queres **desenhar ou editar** um processo? → [Process Studio](./studio.mdx)
- Queres **desenvolver os formulários** das User Tasks? → [Formulários BPMN](./forms.mdx)
- Queres **iniciar, acompanhar ou gerir** instâncias? → [Process Management](./management.mdx)

## Referência [#referência]

| Serviço | API REST | SDK TypeScript |
|---|---|---|
| Process Studio | [API](./api-studio.mdx) — projetos, rascunhos, publicação | [`@igrp/framework-process-studio-*`](./sdk-studio.mdx) — cliente, tipos, editor BPMN |
| Process Management | [API](./api-management.mdx) — instâncias, tarefas, áreas | [`@igrp/platform-process-management-*`](./sdk-management.mdx) — cliente, tipos, runtime das tarefas |


# Executar — Process Management

URL: /pt/docs/developer/process/management

Como monitorizar e operar processos BPMN em runtime — configuração, mapa de processos, tarefas, histórico de instâncias e variáveis de ambiente.

# Process Management — Gestão de Instâncias [#process-management--gestão-de-instâncias]

O **iGRP Process Management** é a aplicação de operações BPM. Depois de um processo ser modelado no [Process Studio](./studio.mdx) e os formulários desenvolvidos no [Studio IDE](./forms.mdx), é aqui que as instâncias são iniciadas, as tarefas geridas e o histórico consultado.

---

## Configuração de Processos [#configuração-de-processos]

`/config` — ecrã de dois painéis: à esquerda a lista de processos, à direita as tabs de configuração do processo selecionado.

### Painel de processos [#painel-de-processos]

- Pesquisa em tempo real por nome de processo.
- Clica num processo para carregar as suas configurações nas tabs à direita.
- O cabeçalho mostra o nome, a chave (`processKey`) e a versão do processo selecionado.

**Menu de ações** (botão `⋯` no canto superior direito do painel direito):

| Ação | O que faz |
|---|---|
| **Export** | Faz download da definição do processo como ficheiro JSON (`{processDefinitionId}.json`) — inclui configuração de numeração, grupos e tarefas |
| **Import new** | Abre o seletor de ficheiro; carrega um JSON exportado previamente para criar ou atualizar uma definição de processo |
| **Archive** | Arquiva a definição de processo (ação destrutiva); pede confirmação antes de executar. Após arquivar, o processo é removido da lista |

{/* Print: /img/process/config-actions-menu.png */}

---

### Tab 1 — Definições Gerais [#tab-1--definições-gerais]

#### Grupos Candidatos ao Nível do Processo [#grupos-candidatos-ao-nível-do-processo]

Define quais os grupos com acesso a **este processo**. Aplicam-se a todas as User Tasks, exceto quando substituídos ao nível da tarefa (ver Tab 3).

| Operação | Como funciona |
|---|---|
| Adicionar grupo existente | Seleciona da lista de grupos disponíveis (vem do Access Management) |
| Adicionar grupo personalizado | Escreve o nome do grupo no campo de texto e confirma |
| Remover grupo | Clica no `×` junto ao nome do grupo na lista de selecionados |

<Callout type="info" title="Herança de grupos">
Os grupos definidos aqui são o **fallback** de todas as tarefas do processo. Uma tarefa com grupos próprios configurados ignora esta lista.
</Callout>

#### Opções de Prioridade [#opções-de-prioridade]

Define os níveis de prioridade disponíveis para as tarefas **deste processo**. Cada opção tem:

| Campo | Descrição |
|---|---|
| **Rótulo** | Nome exibido ao utilizador (ex: `Normal`, `Urgente`) |
| **Valor numérico** | Valor interno enviado à API (ex: `1`, `2`) |
| **Cor** | Cor de destaque associada ao nível (hex, ex: `#B91C1C` para vermelho) |

Pode adicionar, editar e remover opções de prioridade. As opções definem o dropdown disponível no editor de tarefa (Tab 3) e na lista de Minhas Tarefas.

<StudioPreview imageName="pmConfigGerais" alt="Configuração de Processo do Registo de Entidade, separador Definições Gerais, com o grupo * e a prioridade Normal" />

---

### Tab 2 — Numeração do Processo [#tab-2--numeração-do-processo]

Configura o formato do número gerado para cada instância deste processo (o **Business Key**). O número é composto por até quatro partes:

```
<Prefixo><Separador><Data><Separador><Sequência>
Exemplo: LOAN-2026-001
```

| Campo | Opções / Formato | Descrição |
|---|---|---|
| **Prefixo** | Texto livre (ex: `LOAN`, `LIC`, `REQ`) | Parte inicial fixa do número |
| **Formato da Data** | `YYYY` · `YYYYMM` · `YYYYMMDD` · `DDMMYYYY` · `MMDDYYYY` | Componente de data; usa a data de criação da instância. Omitir = sem data no número |
| **Separador** | Texto livre (default: `-`) | Carácter entre cada parte |
| **Comprimento da Sequência** | Inteiro de 1 a 10 (default: `3`) | Número de dígitos do contador sequencial com zeros à esquerda (ex: `3` → `001`, `002`, ...) |

O ecrã mostra um **preview em tempo real** do número resultante com os valores atuais — por exemplo `LOAN-2026-001`.

<StudioPreview imageName="pmNumeracao" alt="Separador Numeração do Processo com prefixo REG, formato YYYY e exemplo REG-2026-001" />

---

### Tab 3 — Configuração de Tarefas [#tab-3--configuração-de-tarefas]

Tabela com todas as User Tasks do processo selecionado.

**Colunas:**

| Coluna | Descrição |
|---|---|
| Nome da Tarefa | Nome legível da User Task definido no BPMN |
| Chave da Tarefa | ID técnico da tarefa no diagrama BPMN |
| Grupos Candidatos | Grupos atualmente configurados para esta tarefa |
| Prioridade | Prioridade por defeito configurada |
| Data Limite | Prazo por defeito configurado |

**Editar uma tarefa** — clica no ícone de lápis (`✏`) para abrir o modal de edição com os seguintes campos:

| Campo | Formato / Opções | Descrição |
|---|---|---|
| **Prioridade Predefinida** | Dropdown com as opções definidas na Tab 1 | Prioridade atribuída automaticamente ao criar a tarefa |
| **Prazo Predefinido** | Duração ISO 8601 (ex: `P3D` = 3 dias, `P1W` = 1 semana, `PT8H` = 8 horas) | Prazo calculado a partir da data de criação da tarefa |
| **Chave do Formulário** | Texto (ex: `ui:form:pedido@1`) | Sobrescreve o Form Key definido no BPMN para esta tarefa |
| **Grupos Candidatos** | Seleção de grupos do AM + grupos personalizados | Substitui os grupos do processo para esta tarefa específica |

{/* Print: /img/process/config-task-editor.png */}

<Callout type="tip" title="Formato ISO 8601 para prazos">
Usa a notação `P[n]D` para dias, `P[n]W` para semanas, `PT[n]H` para horas. Exemplos: `P3D` (3 dias), `P1W` (1 semana), `PT4H` (4 horas). O prazo é calculado a partir do momento em que a tarefa fica ativa.
</Callout>

---

## Mapa de Processos [#mapa-de-processos]

`/process-map` — catálogo de processos organizado em hierarquia de áreas de negócio.

<Callout type="info" title="Para que serve">
O Mapa de Processos organiza o catálogo por áreas de negócio e é daqui que se **iniciam instâncias** (botão ▶ de cada processo). Um processo publicado só aparece no mapa depois de associado a uma área em **Gerir Área**.
</Callout>

### Ver Mapa [#ver-mapa]

Tab **Ver Mapa** (ícone: Eye) — navegação pelo catálogo de processos acessíveis ao utilizador autenticado.

- Campo de pesquisa filtra a árvore por nome de processo em tempo real.
- Árvore hierárquica: áreas → subáreas → processos (carrega subáreas sob pedido, não tudo de uma vez).
- Por cada processo aparece o botão **Iniciar Processo**.

#### Fluxo de Iniciar Processo [#fluxo-de-iniciar-processo]

<StartProcessSequence />

<Callout type="info" title="Resolução da app de execução">
O `applicationBase` é o código da app associada ao processo (o campo **Aplicação** da área em **Gerir Área**). O AM guarda o `slug` ou `url` dessa app — é por aí que o Process Management sabe para onde redirecionar (`<app>/process/<processKey>/<instância>/<tarefa>/<id>`). O parâmetro `returnUrl` garante que o botão **Voltar** na app de execução aterra de volta no Mapa de Processos.
</Callout>

<Callout type="warning" title="A área tem de apontar para a app dos formulários">
Escolhe em **Aplicação** a app Next.js onde geraste os passos no [Studio IDE](./forms.mdx) e instalaste o bundle **Process Management UI**. Se escolheres outra app (por exemplo o próprio Process Management), o redirecionamento cai numa rota que não existe e dá **404**. Em alternativa, `IGRP_PROCESS_EXECUTION_URL` força uma app de execução única para todos os processos.
</Callout>

<StudioPreview imageName="pmMapa" alt="Mapa de Processos com a área Registos e o processo Registo de Entidade com o botão de iniciar" />

Ao clicar em ▶ escolhe-se a prioridade da nova instância:

<StudioPreview imageName="pmIniciar" alt="Diálogo Definir Prioridade para o processo Registo de Entidade, com Normal selecionado" />

---

### Gerir Área [#gerir-área]

Tab **Gerir Área** (ícone: Settings) — ecrã de dois painéis para criar e organizar a hierarquia de áreas.

**Painel esquerdo — Estrutura do Mapa de Processos:**

- Botão **Adicionar Área** no topo para criar uma nova área raiz.
- Árvore com todas as áreas e subáreas.
- Cada nó tem três ações inline:

| Ação | Ícone | O que faz |
|---|---|---|
| **Editar** | Lápis | Abre o modal de edição da área |
| **Eliminar** | Caixote | Abre diálogo de confirmação; se tiver subáreas, **todas são removidas em cascata** |
| **Adicionar Subárea** | Plus | Abre o modal de criação com a área pai pré-selecionada |

**Painel direito — Detalhes da área:**

Ao selecionar uma área na árvore, o painel mostra:
- Bloco de cor + nome + descrição da área.
- Lista **Processos Atribuídos** — cada processo com o seu `processKey` em badge monospace.
- Botões **Edit** e **Delete** que acionam o mesmo modal e diálogo de confirmação.
- Estado vazio: "Selecione uma área para ver detalhes".

<StudioPreview imageName="pmGerirArea" alt="Gerir Área com a área Registos selecionada e o processo registo-entidade atribuído" />

#### Modal Adicionar / Editar Área [#modal-adicionar--editar-área]

Partilhado entre criação e edição — o título muda para **Adicionar Nova Área** ou **Editar Área**.

| Campo | Obrigatório | Descrição |
|---|---|---|
| **Aplicação** | ✓ | Combobox (se houver aplicações registadas) ou campo de texto livre (ex: `registos`) — identifica a aplicação a que a área pertence |
| **Nome** | ✓ | Nome legível da área (ex: `Finanças e Contabilidade`) |
| **Código** | ✓ | Código curto da área (ex: `FIN`) |
| **Cor** | — | Color picker em hex; default `#3b82f6` — cor de destaque na árvore |
| **Descrição** | — | Texto livre de apoio |
| **Área Pai** | — | Select a partir das áreas existentes; deixar vazio = área raiz |
| **Associar Processos** | — | Checklist de todos os processos disponíveis — seleciona os que ficam agrupados nesta área |

Clicar **Gravar** cria ou atualiza a área e fecha o modal automaticamente.

<Callout type="warning" title="Eliminar área com subáreas">
O diálogo de confirmação avisa: "Todas as subáreas também serão removidas." A eliminação é irreversível — reorganiza primeiro os processos para outra área antes de apagar.
</Callout>

<StudioPreview imageName="pmNovaArea" alt="Diálogo Editar Área com a aplicação registos, nome Registos, código REGISTOS e o processo Registo de Entidade associado" />

---

## Minhas Tarefas [#minhas-tarefas]

`/my-tasks` — tarefas atribuídas ao utilizador autenticado.

**Contadores de estado:**

| Contador | O que representa |
|---|---|
| Total | Todas as tarefas do utilizador |
| Disponíveis | Prontas para executar |
| Suspensas | Paradas por suspensão da instância |
| Canceladas | Tarefas cuja instância foi terminada |
| Concluídas | Já submetidas |

**Operações disponíveis por tarefa:**

| Operação | O que faz |
|---|---|
| **Executar Tarefa** | Redireciona para o formulário da User Task na aplicação de execução (`IGRP_PROCESS_EXECUTION_URL`). Após submissão do formulário, o processo avança automaticamente. |
| **Libertar Tarefa** | Devolve a tarefa ao pool de tarefas disponíveis. Abre um modal com campo de nota opcional antes de confirmar. |

<StudioPreview imageName="pmMinhasTarefas" alt="Minhas Tarefas com a tarefa Qualificação atribuída, pronta a executar" />

<Callout type="info" title="URL de execução">
O botão **Executar Tarefa** constrói o URL a partir de `IGRP_PROCESS_EXECUTION_URL`. Se estiver vazio, o URL é resolvido dinamicamente via Access Management com base no processo — cada processo pode ter a sua própria app de execução.
</Callout>

---

## Tarefas Disponíveis [#tarefas-disponíveis]

`/available-tasks` — pool partilhado de tarefas sem utilizador atribuído.

**Contadores de estado:** total de tarefas disponíveis no pool.

**Operações disponíveis por tarefa:**

| Operação | O que faz |
|---|---|
| **Assumir Tarefa** | Atribui a tarefa ao utilizador autenticado. A tarefa sai do pool e aparece em **Minhas Tarefas**. |

**Filtros e pesquisa:** pesquisa por nome de processo ou tarefa; filtro por estado.

<StudioPreview imageName="pmDisponiveis" alt="Tarefas Disponíveis com a Qualificação da instância REG-2026-22 por assumir" />

<Callout type="tip" title="Quem vê o quê">
Só aparecem no pool as tarefas cujos **grupos candidatos** incluem um grupo ou papel que o utilizador possui. A configuração de grupos candidatos é feita em **Configuração de Processos** (`/config`).
</Callout>

---

## Gestão de Tarefas [#gestão-de-tarefas]

`/task-management` — vista administrativa com **todas as tarefas de todos os utilizadores**.

**Contadores de estado:** total, disponíveis, suspensas, canceladas e concluídas (âmbito global).

**Colunas da tabela:**

| Coluna | Descrição |
|---|---|
| Processo | Nome do processo ao qual a tarefa pertence |
| Tarefa | Nome da User Task |
| Utilizador | Utilizador a quem a tarefa está atribuída (vazio = pool) |
| Data de Início | Quando a tarefa ficou ativa |
| Data de Fim | Quando foi concluída/cancelada (se aplicável) |
| Estado | Ativo, suspenso, cancelado, concluído |

**Operações:** clica numa linha para abrir o detalhe da tarefa — ficha completa, ações administrativas e linha do tempo de eventos.

**Filtros e pesquisa:** pesquisa global; filtro por estado, processo e utilizador.

<StudioPreview imageName="pmGestaoTarefas" alt="Gestão de Tarefas com a tarefa Pedido atribuída ao iGRP Super Admin" />

<Callout type="warning" title="Permissão necessária">
A Gestão de Tarefas requer permissão de administrador de processos. Utilizadores sem essa permissão não veem o item no menu.
</Callout>

### Detalhe da Tarefa [#detalhe-da-tarefa]

`/task-management/[taskId]` — página de detalhe de uma tarefa específica.

**Cabeçalho:** nome do processo (`processName`), nome e chave da tarefa (`name — taskKey`), badge de estado.

O ecrã divide-se em dois painéis laterais e dois cards inferiores:

#### Painel esquerdo — Informação da Tarefa [#painel-esquerdo--informação-da-tarefa]

| Campo | Descrição |
|---|---|
| **Responsável** | Utilizador a quem a tarefa está atribuída; `Não atribuído` se ainda estiver no pool |
| **Prioridade** | Badge colorido com a etiqueta de prioridade (ex: Normal, Urgente) |
| **Grupos Candidatos** | Badges dos grupos com acesso a esta tarefa |
| **Formulário** | Chave do formulário associado (`formKey`), em fonte monospace |
| **Criado** | Data/hora em que a tarefa ficou ativa (`startedAt`) |
| **Assumida** | Data/hora em que foi atribuída a um utilizador (`assignedAt`) |
| **Prazo** | Data-limite configurada (`dueDate`); mostra ícone de aviso ⚠ se já ultrapassado |
| **Acompanhamento** | Data de follow-up (`followUpDate`), se definida |
| **Instância do Processo** | Nome e número do processo (copiável); botão **Ver Processo** que navega para `/process-instances/[processInstanceId]` |


#### Painel direito — Ações da Tarefa [#painel-direito--ações-da-tarefa]

Visível apenas quando o estado da tarefa **não é** `COMPLETED`. Se a tarefa já estiver concluída, o painel mostra o estado vazio "Nenhuma ação disponível".

| Ação | Estilo | O que faz |
|---|---|---|
| **Concluir Tarefa** | Botão primário | Marca a tarefa como concluída — faz avançar o processo BPMN para o próximo passo |
| **Delegar** | Contorno | Abre o modal **Atribuir Tarefa** para selecionar um utilizador (campo obrigatório); reatribui a tarefa sem a executar |
| **Alterar Prioridade** | Contorno | Abre modal para escolher um novo nível de prioridade (a partir das opções configuradas em `/config`) |
| **Libertar** | Contorno | Abre modal **Libertar Tarefa** com campo de nota opcional; devolve a tarefa ao pool de disponíveis |
| **Cancelar Tarefa** | Destrutivo (vermelho) | Cancela a tarefa e interrompe a instância; pede confirmação antes de executar |

<StudioPreview imageName="pmDetalheTarefa" alt="Detalhe da tarefa Pedido com Informação da Tarefa, Ações da Tarefa e Variáveis" />

<Callout type="warning" title="Concluir vs Cancelar">
**Concluir Tarefa** avança o fluxo BPMN para o gateway ou tarefa seguinte. **Cancelar Tarefa** termina a instância do processo de forma definitiva — não pode ser revertido.
</Callout>

#### Cards inferiores [#cards-inferiores]

| Card | Descrição |
|---|---|
| **Variáveis** | Tabela chave-valor com todas as variáveis do processo no momento atual — útil para depuração |
| **Eventos** | Linha do tempo dos eventos da tarefa: criação, atribuição, libertação, prioridade alterada, conclusão |


---

## Processos (Instâncias) [#processos-instâncias]

### Lista de Instâncias [#lista-de-instâncias]

`/process-instances` — histórico completo de todas as instâncias (ativas e terminadas).

**Contadores de estado:** total, a correr, canceladas e concluídas.

**Colunas da tabela:**

| Coluna | Descrição |
|---|---|
| Processo | Nome e chave do processo |
| Business Key | Identificador de negócio da instância (ex: número do pedido) |
| Iniciado Por | Utilizador ou serviço que iniciou a instância |
| Versão | Versão do processo em execução |
| Iniciado Em | Data/hora de início |
| Duração | Tempo decorrido desde o início (instâncias ativas) ou total (concluídas) |
| Estado | A correr, suspenso, cancelado, concluído |

**Filtros:** estado, processo, versão, intervalo de datas de início.

<StudioPreview imageName="pmProcessos" alt="Histórico de Processo com instâncias do Registo de Entidade, uma delas concluída" />

### Detalhe da Instância [#detalhe-da-instância]

`/process-instances/[id]` — página completa de inspeção de uma instância específica.

**Cabeçalho:** nome do processo (título), número de negócio da instância (`process.number`, subtítulo), badge de estado.

#### Cards de resumo [#cards-de-resumo]

Quatro métricas no topo da página:

| Card | Ícone | O que mostra |
|---|---|---|
| **Business Key** | GitBranch | Identificador único da instância (número do processo) |
| **Iniciado Por** | User | Utilizador ou serviço que iniciou a instância |
| **Iniciado em** | Calendar | Data/hora de início + duração relativa (ex: "há 3 dias") |
| **Duração** | Clock | Tempo total desde o início; se concluída, mostra também a data de fim (`endedAt`) |


#### Tabs de detalhe [#tabs-de-detalhe]

<Tabs>
<TabItem value="timeline" label="Linha do Tempo de Eventos">

Mostra a sequência cronológica de **todos os eventos** da instância: início do processo, entradas e saídas de tarefas, passagens por gateways, erros e fim.

- N eventos registados (contador no cabeçalho da tab)
- Cada linha: tipo de evento, nome do elemento BPMN, data/hora
- Útil para perceber o caminho percorrido no diagrama e o tempo gasto em cada atividade

<StudioPreview imageName="pmInstancia" alt="Detalhe da instância REG-2026-22 concluída, com a linha do tempo dos eventos" />

</TabItem>
<TabItem value="tasks" label="Histórico de Tarefas">

Lista de todas as **User Tasks** da instância, ativas ou concluídas.

| Coluna | Descrição |
|---|---|
| Tarefa | Nome da User Task |
| Utilizador | Quem executou ou tem atribuída a tarefa |
| Início | Quando a tarefa ficou ativa |
| Conclusão | Quando foi submetida (vazio se ainda ativa) |
| Estado | Ativo, concluído, cancelado |

- N tarefas concluídas (contador no cabeçalho da tab)
- Clica numa tarefa para navegar para o seu detalhe em `/task-management/[taskId]`

<StudioPreview imageName="pmHistoricoTarefas" alt="Histórico de Tarefas da instância concluída: Pedido, Pedido (após Retificar), Qualificação e Qualificação" />

</TabItem>
<TabItem value="variables" label="Variáveis">

Tabela chave-valor de todas as **variáveis do processo** no estado atual.

- N variáveis encontradas (contador no cabeçalho da tab)
- Inclui variáveis de processo e variáveis locais de tarefas
- Útil para depuração: perceber porque é que um gateway escolheu um caminho ou verificar dados recebidos de um delegate

<StudioPreview imageName="pmVariaveis" alt="Variáveis da instância: decision CONFIRMAR, parecer, nif, email, nomeEntidade, morada e startedBy" />

</TabItem>
</Tabs>

<Callout type="info" title="Depuração com variáveis">
O separador **Variáveis** mostra o estado atual em tempo real. Usa-o para perceber porque é que um gateway escolheu um caminho específico ou para verificar dados recebidos de um delegate.
</Callout>

---

## Notificações de tarefas [#notificações-de-tarefas]

Quando alguém conclui uma tarefa na app de execução (**Concluir Tarefa**), o runtime de processos (`@igrp/platform-process-management-client-ui`, incluído no bundle **Process Management UI**) avisa quem tem de tratar a etapa seguinte. Não é preciso escrever código: basta configurar o acesso ao [Notification Service](/docs/developer/notification).

| | |
|---|---|
| **Quando** | Logo a seguir a concluir uma tarefa, para cada User Task que fica ativa a seguir |
| **A quem** | Ao responsável da tarefa, se já estiver atribuída; senão, aos **grupos candidatos** da tarefa (cada grupo enviado como `roleCode`) |
| **Canal** | `IN_APP` — aparece no sino de notificações das apps iGRP |
| **Conteúdo** | Título = nome da tarefa; assunto `<tarefa> · <número>`; texto `Tem uma nova tarefa no processo <número> (<processo>): <tarefa>.` |
| **Identidade** | Enviado em nome da aplicação **Process Management** (`IGRP_PROCESS_MANAGEMENT`), categoria `igrp-task-next`, com `businessKey` = número do processo |
| **Duplicados** | `idempotencyKey` por instância de tarefa — repetir o envio não duplica a notificação |

O envio é *best-effort*: se o Notification Service estiver em baixo ou mal configurado, a tarefa é concluída na mesma e o motivo fica no log do servidor da app (`[igrp-task-next] …`).

### Configuração na app de execução [#configuração-na-app-de-execução]

```bash
# Notification Service (rota do gateway)
NOTIFICATION_SERVICE_BASE_URL=http://<slug>:2575/gateway-api/igrp-notification-service
# Cliente M2M do Process Management no Access Management (não o da tua app)
IGRP_M2M_PROCESS_CLIENT_ID=igrp-process-management-api
IGRP_M2M_PROCESS_CLIENT_SECRET=<segredo>
# Emissor dos tokens (um dos dois)
IGRP_ACCESS_MANAGEMENT_API=http://<slug>:2575/gateway-api/<slug>-access-management
# Opcionais
IGRP_PROCESS_APP_CODE=IGRP_PROCESS_MANAGEMENT   # default
IGRP_NOTIFY_NEXT_TASK=false                     # desliga as notificações de tarefa
```

Sem `NOTIFICATION_SERVICE_BASE_URL` ou sem o par `IGRP_M2M_PROCESS_CLIENT_*`, o envio é ignorado (`skip: missing Notification Service access env`).

<Callout type="info" title="Os grupos candidatos decidem quem é avisado">
Uma tarefa sem responsável e sem grupos candidatos não gera notificação. Configura os grupos em [Configuração de Processos](#configuração-de-processos) (ao nível do processo ou da tarefa) com os códigos de papel do Access Management.
</Callout>

Para outras notificações do teu processo (por exemplo, um email ao requerente quando o pedido é aceite), chama o Notification Service a partir do código do passo ou de um serviço — ver [Notificações › Envio — SDKs e API](/docs/developer/notification/integracao).

---

## Iniciar uma instância via API [#iniciar-uma-instância-via-api]

Além da interface, qualquer serviço pode iniciar (criar e arrancar) uma instância com um token M2M:

```http
POST /process-instances
Authorization: Bearer <m2m-token>
Content-Type: application/json

{
  "processKey": "registo-entidade",
  "applicationBase": "registos",
  "businessKey": "REG-EXT-0001",
  "priority": 1,
  "variables": [
    { "name": "nomeEntidade", "value": "Exemplo, Lda." },
    { "name": "nif", "value": "123456789" }
  ]
}
```

Os caminhos são relativos à rota do gateway do `process-management-api` (`http://<slug>:2575/gateway-api/<serviço>-process-management-api`, sem `/api/v1`). `processKey` e `applicationBase` são obrigatórios; as variáveis vão como lista `name`/`value`. Para criar a instância sem a arrancar usa `POST /process-instances/create` e depois `POST /process-instances/{id}/start`.

O `m2m-token` é um JWT `client_credentials` obtido do Access Management. Ver [API M2M](../access-management/m2m-api.mdx) para o fluxo completo.

---

## Variáveis de ambiente [#variáveis-de-ambiente]

Variáveis de ambiente do `igrp-process-management-ui` (Next.js).

### Autenticação [#autenticação]

```bash
AUTH_PROVIDER=igrp-auth
IGRP_AUTH_CLIENT_ID=process-management
IGRP_AUTH_CLIENT_SECRET=<secret>
IGRP_AUTH_ISSUER=https://api.example.com/igrp-access-management
IGRP_AUTH_SCOPES=openid email
NEXTAUTH_URL=https://bpm.example.com/api/auth
NEXTAUTH_URL_INTERNAL=http://process-management:3000/api/auth
NEXTAUTH_SECRET=<openssl rand -base64 32>
```

| Variável | Obrigatória | Notas |
|---|---|---|
| `AUTH_PROVIDER` | ✓ | `igrp-auth` em produção; `none` desativa auth (dev) |
| `IGRP_AUTH_CLIENT_ID` | ✓ | Cliente OAuth2 `authorization_code` registado no AM |
| `IGRP_AUTH_CLIENT_SECRET` | ✓ | **Secret** |
| `IGRP_AUTH_ISSUER` | ✓ | URL base do AM (sem trailing slash) |
| `IGRP_AUTH_SCOPES` | — | Default `openid email` |
| `NEXTAUTH_URL` | ✓ | URL público da app + `/api/auth` |
| `NEXTAUTH_URL_INTERNAL` | — | URL interno (Docker/K8s) + `/api/auth` |
| `NEXTAUTH_SECRET` | ✓ | Gera com `openssl rand -base64 32` |

### Framework iGRP [#framework-igrp]

```bash
IGRP_APP_CODE=PROCESS_MANAGEMENT
NEXT_PUBLIC_IGRP_APP_CODE=PROCESS_MANAGEMENT
IGRP_ACCESS_MANAGEMENT_API=https://api.example.com/igrp-access-management
IGRP_PREVIEW_MODE=false
IGRP_SESSION_MAX_AGE=86400
```

| Variável | Obrigatória | Notas |
|---|---|---|
| `IGRP_APP_CODE` | ✓ | Código da app no AM — deve ser único no ecossistema |
| `NEXT_PUBLIC_IGRP_APP_CODE` | ✓ | Cópia client-side de `IGRP_APP_CODE` |
| `IGRP_ACCESS_MANAGEMENT_API` | ✓ | URL base da API do AM |
| `IGRP_PREVIEW_MODE` | — | `true` desativa auth e usa dados mock — **nunca em produção** |
| `IGRP_SESSION_MAX_AGE` | — | Duração da sessão em segundos (default ~30 dias) |

### Sincronização com Access Management [#sincronização-com-access-management]

```bash
IGRP_SYNC_ACCESS=true
IGRP_SYNC_ON_CODE_MENUS=false
IGRP_SYNC_ON_CODE_MENU_ROLES=true
IGRP_SERVICE_ID=process-management
IGRP_M2M_CLIENT_ID=process-management-m2m
IGRP_M2M_CLIENT_SECRET=<secret>
```

| Variável | Default | Notas |
|---|---|---|
| `IGRP_SYNC_ACCESS` | `true` | Sincroniza recursos, permissões e menus no arranque |
| `IGRP_SYNC_ON_CODE_MENUS` | `false` | `true` envia `menus.ts` ao AM no arranque |
| `IGRP_SYNC_ON_CODE_MENU_ROLES` | `true` | Cria roles referenciados nos menus se não existirem |
| `IGRP_SERVICE_ID` | — | Identidade da máquina — enviado no header `X-Machine-Service-ID` |
| `IGRP_M2M_CLIENT_ID` | — | Cliente OAuth2 `client_credentials` para chamadas M2M |
| `IGRP_M2M_CLIENT_SECRET` | — | **Secret** |

### Execução de tarefas [#execução-de-tarefas]

```bash
# URL da app que aloja os formulários das User Tasks.
# Deixa vazio para resolução dinâmica por processo via Access Management.
IGRP_PROCESS_EXECUTION_URL=https://bpm.example.com
```

| Variável | Default | Notas |
|---|---|---|
| `IGRP_PROCESS_EXECUTION_URL` | — | Vazio = cada processo resolve o URL via AM (permite apps de execução diferentes por processo) |

### Navegação e URLs externos [#navegação-e-urls-externos]

```bash
NEXT_PUBLIC_IGRP_APP_HOME_SLUG=/dashboard
NEXT_IGRP_APP_CENTER_URL=https://apps.example.com/app-center
NEXT_PUBLIC_BASE_PATH=
NEXT_PUBLIC_ALLOWED_DOMAINS=cdn.example.com
NEXT_PUBLIC_IGRP_PROFILE_URL=
NEXT_PUBLIC_IGRP_NOTIFICATION_URL=
NEXT_PUBLIC_IGRP_SETTINGS_URL=
```

| Variável | Notas |
|---|---|
| `NEXT_PUBLIC_IGRP_APP_HOME_SLUG` | Rota para onde redirecionar após login (default `/dashboard`) |
| `NEXT_IGRP_APP_CENTER_URL` | URL do App Center para troca de aplicação |
| `NEXT_PUBLIC_BASE_PATH` | Subdiretório se a app não estiver na raiz (ex: `/bpm`) |
| `NEXT_PUBLIC_ALLOWED_DOMAINS` | Domínios de imagens remotas permitidos (vírgula-separados) |
| `NEXT_PUBLIC_IGRP_PROFILE_URL` | Override do URL de perfil de utilizador |
| `NEXT_PUBLIC_IGRP_NOTIFICATION_URL` | Override do URL de notificações |
| `NEXT_PUBLIC_IGRP_SETTINGS_URL` | Override do URL de definições |

---

## Menu da aplicação [#menu-da-aplicação]

| Posição | Ecrã | Rota |
|---|---|---|
| 1 | Dashboard | `/` |
| 2 | Configuração de Processos | `/config` |
| 3 | Mapa de Processos | `/process-map` |
| 4 | Minhas Tarefas | `/my-tasks` |
| 5 | Tarefas Disponíveis | `/available-tasks` |
| 6 | Gestão de Tarefas | `/task-management` |
| 7 | Processos | `/process-instances` |

Em produção, o menu é servido pelo Access Management (sincronizado com `IGRP_SYNC_ON_CODE_MENUS=true`). A definição de código está em `src/temp/menus/menus.ts`.

---

## Integração com o resto da plataforma [#integração-com-o-resto-da-plataforma]

| Componente | Como se integra |
|---|---|
| **Process Studio** | Publica versões consumidas pelo runtime do Process Management. Versões imutáveis — instâncias em curso mantêm a versão original. |
| **Access Management** | Controla quem acede a cada ecrã via permissões. Grupos candidatos das tarefas são roles do AM. Menu servido pelo AM em produção. |
| **Notification Service** | O runtime pode enviar notificação ao utilizador ao criar uma User Task — via [Notification Service](../access-management/notification-service.mdx). |
| **Formulários iGRP** | A execução de tarefas abre a app de formulários (Studio IDE). URL configurado via `IGRP_PROCESS_EXECUTION_URL`. |

---

## Próximo passo [#próximo-passo]

- Volta ao [Process Studio](./studio.mdx) para criar ou editar um processo.
- Consulta os [Formulários BPMN](./forms.mdx) para desenvolver os formulários das User Tasks.


# Áreas

URL: /pt/docs/developer/process/referencia-process-management/areas

Áreas e os processos associados a cada uma.

Servidor: `http://{host}/gateway-api/{servico}-process-management-api`

## Listar áreas

`GET /areas`

**Parâmetros**

- `code` (query, string)
- `name` (query, string)
- `applicationBase` (query, string)
- `status` (query, string)
- `parentId` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))

**Respostas**

- `200` List of areas — `AreaListPageDTO`

## Criar área

`POST /areas`

**Corpo do pedido** (`application/json`, `AreaRequestDTO`)

- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `applicationBase` (string, obrigatório)
- `parentId` (string (uuid))
- `color` (string)

**Respostas**

- `201` Persisted Area — `AreaDTO`

## Obter área

`GET /areas/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` Area Info — `AreaDTO`

## Eliminar área (e subáreas)

`DELETE /areas/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Atualizar área

`PUT /areas/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `AreaRequestDTO`)

- `code` (string, obrigatório)
- `name` (string, obrigatório)
- `description` (string)
- `applicationBase` (string, obrigatório)
- `parentId` (string (uuid))
- `color` (string)

**Respostas**

- `200` Updated area — `AreaDTO`

## Estados possíveis de uma área

`GET /areas/status`

**Respostas**

- `200` List of area status — `ConfigParameterDTO`

## Processos da área

`GET /areas/{areaId}/process-definitions`

**Parâmetros**

- `processKey` (query, string)
- `status` (query, string)
- `releaseId` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))
- `areaId` (path, string, obrigatório)

**Respostas**

- `200` List of process definition of an area — `ProcessDefinitionListPageDTO`

## Associar processo à área

`POST /areas/{areaId}/process-definitions`

**Parâmetros**

- `areaId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProcessDefinitionRequestDTO`)

- `processKey` (string, obrigatório)
- `releaseId` (string, obrigatório)
- `version` (string)
- `name` (string, obrigatório)

**Respostas**

- `201` Persisted process definition — `ProcessDefinitionDTO`

## Desassociar processo da área

`DELETE /areas/{areaId}/process-definitions/{processDefinitionId}`

**Parâmetros**

- `areaId` (path, string, obrigatório)
- `processDefinitionId` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

# Atividades

URL: /pt/docs/developer/process/referencia-process-management/atividades

Eventos e progresso de uma instância.

Servidor: `http://{host}/gateway-api/{servico}-process-management-api`

## Atividades de uma instância

`GET /activities/instances`

**Parâmetros**

- `processIdentifier` (query, string, obrigatório)
- `type` (query, string)

**Respostas**

- `200` OK — `ActivityDTO`

## Progresso de uma instância

`GET /activities/progress`

**Parâmetros**

- `processIdentifier` (query, string, obrigatório)
- `type` (query, string)

**Respostas**

- `200` OK — `ActivityProgressDTO`

## Obter atividade

`GET /activities/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `ActivityDTO`

# Definições de processo

URL: /pt/docs/developer/process/referencia-process-management/definicoes-de-processo

Definições publicadas, artefactos das tarefas, numeração e prioridades.

Servidor: `http://{host}/gateway-api/{servico}-process-management-api`

## Listar definições publicadas

`GET /process-definitions`

**Parâmetros**

- `applicationBase` (query, string)
- `processName` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))
- `filterByCurrentUser` (query, boolean)
- `candidateGroups` (query, string)

**Respostas**

- `200` List of deployments — `ProcessDeploymentListPageDTO`

## Publicar definição (chamado pelo Process Studio)

`POST /process-definitions/deploy`

**Corpo do pedido** (`application/json`, `ProcessDeploymentRequestDTO`)

- `name` (string)
- `description` (string)
- `key` (string, obrigatório)
- `resourceName` (string, obrigatório)
- `bpmnXml` (string, obrigatório)
- `applicationBase` (string)

**Respostas**

- `201` Process deploymeny — `ProcessDeploymentDTO`

## Importar definição exportada

`POST /process-definitions/import`

**Corpo do pedido** (`application/json`, `ProcessPackageDTO`)

- `processKey` (string, obrigatório)
- `processName` (string, obrigatório)
- `processVersion` (string)
- `processDescription` (string)
- `bpmnXml` (string, obrigatório)
- `applicationBase` (string, obrigatório)
- `artifacts` (ProcessArtifactDTO[])
- `sequence` (ProcessSequenceDTO)
- `candidateGroups` (string)

**Respostas**

- `204` No Content — `string`

## Exportar definição

`GET /process-definitions/{id}/export`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProcessPackageDTO`

## Arquivar definição

`DELETE /process-definitions/{id}/archive`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Desarquivar definição

`POST /process-definitions/{id}/unarchive`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Atribuir grupos candidatos ao processo

`POST /process-definitions/{id}/assign`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `AssignProcessDTO`)

- `candidateGroups` (string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Retirar grupos candidatos do processo

`POST /process-definitions/{id}/unassign`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `AssignProcessDTO`)

- `candidateGroups` (string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Artefactos da definição (User Tasks e Form Keys)

`GET /process-definitions/{id}/artifacts`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` List of Process Artifacts — `ProcessArtifactDTO`

## Artefactos da versão publicada

`GET /process-definitions/{id}/deployed-artifacts`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` List of Deployed Process Artifact — `ProcessArtifactDTO`

## Configurar uma tarefa

`PUT /process-definitions/{id}/artifacts/{taskKey}`

**Parâmetros**

- `id` (path, string, obrigatório)
- `taskKey` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProcessArtifactRequestDTO`)

- `name` (string, obrigatório)
- `formKey` (string, obrigatório)
- `candidateGroups` (string)
- `dueDate` (string)
- `priority` (integer (int32))

**Respostas**

- `200` Persisted Project Artifact — `ProcessArtifactDTO`

## Remover configuração de tarefa

`DELETE /process-definitions/artifacts/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Obter numeração das instâncias

`GET /process-definitions/{processDefinitionKey}/sequence`

**Parâmetros**

- `processDefinitionKey` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProcessSequenceDTO`

## Definir numeração das instâncias

`POST /process-definitions/{processDefinitionKey}/sequence`

**Parâmetros**

- `processDefinitionKey` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `SequenceRequestDTO`)

- `name` (string, obrigatório)
- `prefix` (string, obrigatório)
- `dateFormat` (string, obrigatório)
- `checkDigitSize` (integer (int32), obrigatório)
- `padding` (integer (int32), obrigatório)
- `numberIncrement` (integer (int32))
- `separator` (string)

**Respostas**

- `200` OK — `ProcessSequenceDTO`

## Opções de prioridade

`GET /process-definitions/{processKey}/priorities`

**Parâmetros**

- `processKey` (path, string, obrigatório)

**Respostas**

- `200` OK — `TaskPriorityDTO`

## Configurar prioridades

`PUT /process-definitions/{processKey}/priorities`

**Parâmetros**

- `processKey` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `TaskPriorityRequestDTO[]`)

**Respostas**

- `200` OK — `TaskPriorityDTO`

## Remover prioridade

`DELETE /process-definitions/priorities/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

# Instâncias

URL: /pt/docs/developer/process/referencia-process-management/instancias

Criar, arrancar, pesquisar e cancelar instâncias de processo.

Servidor: `http://{host}/gateway-api/{servico}-process-management-api`

## Criar e arrancar instância

`POST /process-instances`

**Corpo do pedido** (`application/json`, `StartProcessRequestDTO`)

- `processDefinitionId` (string)
- `processKey` (string, obrigatório)
- `businessKey` (string)
- `applicationBase` (string, obrigatório)
- `priority` (integer (int32))
- `variables` (ProcessVariableDTO[])
- `assignmentRules` (ProcessTaskAssignmentRuleDTO[])

**Respostas**

- `200` OK — `ProcessInstanceDTO`

## Criar instância (sem arrancar)

`POST /process-instances/create`

**Corpo do pedido** (`application/json`, `CreateProcessRequestDTO`)

- `processDefinitionId` (string)
- `processKey` (string, obrigatório)
- `applicationBase` (string, obrigatório)
- `businessKey` (string)
- `priority` (integer (int32))

**Respostas**

- `201` Created — `ProcessInstanceDTO`

## Arrancar instância criada

`POST /process-instances/{id}/start`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProcessVariablesRequestDTO`)

- `variables` (ProcessVariableDTO[])
- `assignmentRules` (ProcessTaskAssignmentRuleDTO[])

**Respostas**

- `200` OK — `ProcessInstanceDTO`

## Pesquisar instâncias

`POST /process-instances/search`

**Parâmetros**

- `number` (query, string)
- `name` (query, string)
- `procReleaseKey` (query, string)
- `procReleaseId` (query, string)
- `status` (query, string)
- `applicationBase` (query, string)
- `dateFrom` (query, string)
- `dateTo` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))

**Corpo do pedido** (`application/json`, `VariablesFilterDTO`)

- `variables` (VariablesExpressionDTO[])

**Respostas**

- `200` List of process instances — `ProcessInstanceListPageDTO`

## Obter instância

`GET /process-instances/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` Process Instance Info — `ProcessInstanceDTO`

## Estado das tarefas da instância

`GET /process-instances/{id}/task-status`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` List of task status of the process instance — `ProcessInstanceTaskStatusDTO`

## Cancelar instância

`POST /process-instances/{id}/cancel`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `CancelProcessInstanceDTO`)

- `obsCancel` (string)

**Respostas**

- `200` OK — `ProcessInstanceDTO`

## Enviar evento (mensagem ou sinal)

`POST /process-instances/event`

**Corpo do pedido** (`application/json`, `ProcessEventDTO`)

- `messageName` (string)
- `taskId` (string)
- `businessKey` (string)
- `variables` (ProcessVariableDTO[])

**Respostas**

- `200` OK — `string`

## Reagendar timer

`POST /process-instances/{id}/timer/reschedule`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `TimerRescheduleDTO`)

- `elementId` (string)
- `seconds` (integer (int64), obrigatório)

**Respostas**

- `204` No Content — `string`

## Contadores de instâncias

`GET /process-instances/stats`

**Respostas**

- `200` OK — `ProcessInstanceStatsDTO`

## Estados possíveis de uma instância

`GET /process-instances/status`

**Respostas**

- `200` List of process instance status — `ConfigParameterDTO`

# Tarefas

URL: /pt/docs/developer/process/referencia-process-management/tarefas

Pesquisar, assumir, atribuir, guardar e concluir tarefas, e regras de atribuição.

Servidor: `http://{host}/gateway-api/{servico}-process-management-api`

## Pesquisar tarefas

`POST /tasks-instances/search`

**Parâmetros**

- `processInstanceId` (query, string)
- `processNumber` (query, string)
- `processReleaseKey` (query, string)
- `applicationBase` (query, string)
- `candidateGroups` (query, string)
- `candidateUsers` (query, string)
- `user` (query, string)
- `status` (query, string)
- `dateFrom` (query, string)
- `dateTo` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))
- `name` (query, string)
- `processName` (query, string)
- `filterByCurrentUser` (query, boolean)
- `priority` (query, integer (int32))

**Corpo do pedido** (`application/json`, `VariablesFilterDTO`)

- `variables` (VariablesExpressionDTO[])

**Respostas**

- `200` List of task instances — `TaskInstanceListPageDTO`

## Tarefas do utilizador autenticado

`POST /tasks-instances/me`

**Parâmetros**

- `processInstanceId` (query, string)
- `processNumber` (query, string)
- `applicationBase` (query, string)
- `processName` (query, string)
- `status` (query, string)
- `dateFrom` (query, string)
- `dateTo` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))
- `processReleaseKey` (query, string)
- `name` (query, string)
- `priority` (query, integer (int32))

**Corpo do pedido** (`application/json`, `VariablesFilterDTO`)

- `variables` (VariablesExpressionDTO[])

**Respostas**

- `200` List All My Tasks — `TaskInstanceListPageDTO`

## Obter tarefa

`GET /tasks-instances/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` Task Instance Info — `TaskInstanceDTO`

## Variáveis e formulários da tarefa

`GET /tasks-instances/{id}/variables`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `200` OK — `TaskVariablesFormsDTO`

## Assumir tarefa

`POST /tasks-instances/{id}/claim`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No Content — `string`

## Libertar tarefa

`POST /tasks-instances/{id}/unclaim`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `UnclaimTaskDTO`)

- `note` (string)

**Respostas**

- `204` No content — `string`

## Atribuir tarefa a outro utilizador

`POST /tasks-instances/{id}/assign`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `AssignTaskDTO`)

- `user` (string)
- `priority` (integer (int32))
- `note` (string)
- `candidateGroups` (string)
- `candidateUsers` (string)

**Respostas**

- `204` No content — `string`

## Guardar dados sem concluir

`POST /tasks-instances/{id}/save`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `TaskDataDTO`)

- `variables` (ProcessVariableDTO[])
- `forms` (TaskVariableDTO[])

**Respostas**

- `200` OK — `TaskInstanceDTO`

## Concluir tarefa

`POST /tasks-instances/{id}/complete`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `TaskDataDTO`)

- `variables` (ProcessVariableDTO[])
- `forms` (TaskVariableDTO[])

**Respostas**

- `200` OK — `TaskInstanceDTO`

## Contadores de tarefas

`GET /tasks-instances/stats`

**Respostas**

- `200` OK — `TaskInstanceStatsDTO`

## Contadores das minhas tarefas

`GET /tasks-instances/stats/me`

**Respostas**

- `200` OK — `TaskInstanceStatsDTO`

## Estados possíveis de uma tarefa

`GET /tasks-instances/status`

**Respostas**

- `200` OK — `ConfigParameterDTO`

## Tipos de evento de tarefa

`GET /tasks-instances/event_type`

**Respostas**

- `200` OK — `ConfigParameterDTO`

## Listar regras de atribuição

`GET /tasks-instances/assignment-rules`

**Parâmetros**

- `processInstanceId` (query, string)
- `processDefinitionKey` (query, string)
- `taskDefinitionKey` (query, string)
- `assignee` (query, string)
- `candidateUsers` (query, string)
- `candidateGroups` (query, string)
- `assignmentMode` (query, "ALWAYS" | "ONE_TIME")
- `consumed` (query, boolean)
- `active` (query, boolean)
- `createdByTask` (query, string)
- `page` (query, integer (int32))
- `size` (query, integer (int32))

**Respostas**

- `200` List task assignment rules — `TaskAssignmentRuleListPageDTO`

## Eliminar regra de atribuição

`DELETE /tasks-instances/assignment-rules/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Respostas**

- `204` No content — `string`

## Atualizar regra de atribuição

`PUT /tasks-instances/assignment-rules/{id}`

**Parâmetros**

- `id` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `TaskAssignmentRuleUpdateDTO`)

- `assignee` (string)
- `candidateUsers` (string)
- `candidateGroups` (string)

**Respostas**

- `200` Task assignment rule updated — `TaskAssignmentRuleListDTO`

# Definições de processo

URL: /pt/docs/developer/process/referencia-process-studio/definicoes-de-processo

Criar processos, guardar o diagrama, declarar variáveis e publicar no Process Management.

Servidor: `http://{host}/gateway-api/{servico}-process-studio-api`

## Criar processo no projeto

`POST /api/v1/projects/{projectId}/process-definitions`

**Parâmetros**

- `projectId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProcessDefinitionRequestDTO`)

- `title` (string)
- `processKey` (string)
- `description` (string)
- `projectId` (string)
- `status` ("DRAFT" | "VALIDATED" | "PUBLISHED" | "DEPRECATED" | "ARCHIVED" | "DISABLED" | "ERROR" | "DELETED")

**Respostas**

- `201` Created — `ProcessDefinitionResponseDTO`

## Listar processos

`GET /api/v1/projects/process-definitions`

**Parâmetros**

- `appCode` (query, string)
- `processKey` (query, string)
- `processName` (query, string)
- `projectCode` (query, string)
- `projectName` (query, string)
- `state` (query, string)
- `pageNumber` (query, string)
- `pageSize` (query, string)

**Respostas**

- `200` OK — `WrapperListaProcessDefinitionDTO`

## Obter processo (inclui o XML)

`GET /api/v1/projects/process-definitions/{processId}`

**Parâmetros**

- `processId` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProcessDefinitionResponseDTO`

## Atualizar dados do processo

`PUT /api/v1/projects/process-definitions/{processId}`

**Parâmetros**

- `processId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProcessDefinitionRequestDTO`)

- `title` (string)
- `processKey` (string)
- `description` (string)
- `projectId` (string)
- `status` ("DRAFT" | "VALIDATED" | "PUBLISHED" | "DEPRECATED" | "ARCHIVED" | "DISABLED" | "ERROR" | "DELETED")

**Respostas**

- `200` OK — `ProcessDefinitionResponseDTO`

## Guardar rascunho do diagrama

`PUT /api/v1/projects/process-definitions/{processKey}/diagram`

**Parâmetros**

- `processKey` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `BpmDiagramDTO`)

- `content` (string, obrigatório)

**Respostas**

- `200` OK — `ProcessDefinitionResponseDTO`

## Publicar no Process Management

`POST /api/v1/projects/process-definitions/{processKey}/deploy`

**Parâmetros**

- `processKey` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `BpmDiagramDTO`)

- `content` (string, obrigatório)

**Respostas**

- `200` OK — `ProcessDefinitionResponseDTO`

## Variáveis declaradas do processo

`GET /api/v1/projects/process-definitions/{processId}/variables`

**Parâmetros**

- `processId` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProcessVariableResponseDTO`

## Declarar variáveis do processo

`POST /api/v1/projects/process-definitions/{processId}/variables`

**Parâmetros**

- `processId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProcessVariableRequestDTO[]`)

**Respostas**

- `200` OK — `ProcessVariableResponseDTO`

## Apagar processo

`PATCH /api/v1/projects/process-definitions/{processId}/delete`

**Parâmetros**

- `processId` (path, string, obrigatório)

**Respostas**

- `200` OK — `string`

## Restaurar processo

`PATCH /api/v1/projects/process-definitions/{processId}/restore`

**Parâmetros**

- `processId` (path, string, obrigatório)

**Respostas**

- `200` OK — `string`

# Parametrização

URL: /pt/docs/developer/process/referencia-process-studio/parametrizacao

Valores de referência, como os estados de uma definição.

Servidor: `http://{host}/gateway-api/{servico}-process-studio-api`

## Estados possíveis de uma definição

`GET /parameterization/process-definition-state`

**Respostas**

- `200` OK — `EnumItemString[]`

# Projetos

URL: /pt/docs/developer/process/referencia-process-studio/projetos

Projetos de processos e os processos publicados de cada um.

Servidor: `http://{host}/gateway-api/{servico}-process-studio-api`

## Listar projetos

`GET /api/v1/projects`

**Parâmetros**

- `appCode` (query, string)
- `pageNumber` (query, string)
- `pageSize` (query, string)

**Respostas**

- `200` OK — `WrapperListaProjectDTO`

## Criar projeto

`POST /api/v1/projects`

**Corpo do pedido** (`application/json`, `ProjectRequestDTO`)

- `code` (string)
- `name` (string)
- `description` (string)
- `appCode` (string)

**Respostas**

- `201` Created — `ProjectResponseDTO`

## Obter projeto

`GET /api/v1/projects/{projectId}`

**Parâmetros**

- `projectId` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProjectResponseDTO`

## Atualizar projeto

`PUT /api/v1/projects/{projectId}`

**Parâmetros**

- `projectId` (path, string, obrigatório)

**Corpo do pedido** (`application/json`, `ProjectRequestDTO`)

- `code` (string)
- `name` (string)
- `description` (string)
- `appCode` (string)

**Respostas**

- `200` OK — `ProjectResponseDTO`

## Ativar projeto

`PATCH /api/v1/projects/{projectId}/enable`

**Parâmetros**

- `projectId` (path, string, obrigatório)

**Respostas**

- `200` OK — `string`

## Desativar projeto

`PATCH /api/v1/projects/{projectId}/disable`

**Parâmetros**

- `projectId` (path, string, obrigatório)

**Respostas**

- `200` OK — `string`

## Processos publicados do projeto

`GET /api/v1/projects/{projectId}/deployed-process`

**Parâmetros**

- `processName` (query, string)
- `processKey` (query, string)
- `pageSize` (query, string)
- `pageNumber` (query, string)
- `projectId` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProjectResponseDTO`

## Histórico de versões do projeto

`GET /api/v1/projects/{projectId}/history-process`

**Parâmetros**

- `processName` (query, string)
- `processKey` (query, string)
- `pageSize` (query, string)
- `pageNumber` (query, string)
- `projectId` (path, string, obrigatório)

**Respostas**

- `200` OK — `ProjectResponseDTO`

# Process Management · SDK TypeScript

URL: /pt/docs/developer/process/sdk-management

Os pacotes @igrp/platform-process-management-client-ts, -types e -client-ui — cliente HTTP por recurso, runtime de tarefas em React e o contrato dos passos.

# SDK TypeScript — Process Management [#sdk-typescript--process-management]

O Process Management UI e as apps que executam tarefas falam com o [Process Management API](./api-management.mdx) através de três pacotes:

| Pacote | Versão | Para quê |
|---|---|---|
| `@igrp/platform-process-management-client-ts` | 0.1.0-beta.21 | Cliente HTTP: processos, instâncias, tarefas, áreas, atividades |
| `@igrp/platform-process-management-types` | 0.1.0-beta.21 | Tipos (`Task`, `ProcessInstance`, `TaskData`, filtros…) |
| `@igrp/platform-process-management-client-ui` | 0.1.0-beta.63 | Runtime das tarefas em React: página, provider, contrato dos passos |

Numa app gerada pelo Studio, o bundle **Process Management UI** instala os três e as páginas de runtime — ver [Formulários BPMN](./forms.mdx#instalar-o-runtime-de-processos).

---

## Cliente HTTP [#cliente-http]

```ts
import { ProcessManagementClient } from "@igrp/platform-process-management-client-ts";

const pm = ProcessManagementClient.create({
  baseUrl: process.env.API_GATEWAY!, // rota do gateway, SEM /api/v1
  timeout: 30000,
  headers: { Authorization: `Bearer ${accessToken}` },
});
```

| Opção | Notas |
|---|---|
| `baseUrl` | `http://<slug>:2575/gateway-api/<serviço>-process-management-api`. Os caminhos começam na raiz (`/process-instances`). |
| `timeout` | ms (default 30000), com AbortController |
| `headers` | Junta-se a `Content-Type`/`Accept: application/json`. A autenticação vai aqui. |

O Process Management UI cria **um cliente novo por pedido**, no servidor, com o token da sessão (`auth.getAccessToken()`): guardar o cliente num módulo partilharia o token entre utilizadores.

Cada método devolve `Promise<ApiResponse<T>>` = `{ data, status, statusText }`. As listas vêm em `PaginatedResponse<T>` (`content`, `pageNumber`, `pageSize`, `totalElements`, `totalPages`, `first`, `last`).

### `pm.processes` [#pmprocesses]

| Método | Pedido |
|---|---|
| `createAndStartProcess(body)` | `POST /process-instances` — `{ processKey, applicationBase, priority?, businessKey?, variables?: {name,value}[], assignmentRules? }` |
| `createProcessInstance(body)` · `startProcessInstance(id, body)` | `POST /process-instances/create` · `/{id}/start` |
| `getProcessInstances(params?, body?)` | `POST /process-instances/search` (filtros na query) |
| `getProcessInstanceById(id)` · `getProcessInstanceTaskStatus(id)` | `GET /process-instances/{id}` · `/{id}/task-status` |
| `cancelProcessInstance(id, { obsCancel? })` | `POST /process-instances/{id}/cancel` |
| `getProcesses(filter?)` · `getProcessById(id)` | `GET /process-definitions[/{id}]` |
| `getProcessArtifacts(id)` · `getProcessDeployedArtifacts(id)` | `GET /process-definitions/{id}/artifacts` · `/deployed-artifacts` |
| `updateProcessArtifact(id, artifact)` | `PUT /process-definitions/{id}/artifacts/{taskKey}` |
| `assignGroupsToProcessDefinition` · `unassign…` | `POST /process-definitions/{id}/assign` · `/unassign` |
| `get…/createProcessSequence(key)` | `GET`/`POST /process-definitions/{key}/sequence` |
| priorities, `export…`, `import…`, `archive…`, `unarchive…`, `deployProcess`, `triggerProcessEvent`, `rescheduleTimer` | ver [API](./api-management.mdx) |

### `pm.tasks` [#pmtasks]

| Método | Pedido |
|---|---|
| `getMyTasks(params?, body?)` | `POST /tasks-instances/me` (força `status=ASSIGNED`) |
| `getAvailableTasks(params?)` | `POST /tasks-instances/search` com `status=CREATED` |
| `getTasks(params?, body?)` | `POST /tasks-instances/search` — filtros na query (`processInstanceId`, `processNumber`, `name`, `status`, `applicationBase`, `page`, `size`…); filtros por variável no corpo (`{ variables: [{ name, operator, value }] }`) |
| `getTasksByProcessInstance(id, paging?)` | atalho de `getTasks` |
| `getTaskById(id)` · `getTaskVariablesById(id)` | `GET /tasks-instances/{id}` · `/{id}/variables` |
| `claimTask(id, { user?, note? })` | `POST /tasks-instances/{id}/claim` (parâmetros na query) |
| `unclaimTask(id, { note? })` | `POST /tasks-instances/{id}/unclaim` |
| `assignTask(id, { user?, note?, priority?, candidateGroups?, candidateUsers? })` | `POST /tasks-instances/{id}/assign` |
| `saveTask(id, data)` · `completeTask(id, data)` | `POST /tasks-instances/{id}/save` · `/complete` — `TaskData = { variables?, forms? }` |
| `getTaskStats()` · `getMyTaskStats()` | `GET /tasks-instances/stats` · `/stats/me` |

### `pm.areas` e `pm.activities` [#pmareas-e-pmactivities]

`getAreas`, `getAreaById`, `createArea`, `updateArea`, `deleteArea`, `getAreaProcesses`, `associateProcessToArea`, `removeProcessFromArea` (`/areas…`); `getActivityInstances(instanceId)`, `getActivityProgress(instanceId)`, `getActivityById(id)` (`/activities…`).

### Exemplo — iniciar, assumir e concluir [#exemplo--iniciar-assumir-e-concluir]

```ts
import { ProcessManagementClient, ApiClientError } from "@igrp/platform-process-management-client-ts";

const pm = ProcessManagementClient.create({ baseUrl, headers: { Authorization: `Bearer ${token}` } });

const { data: instance } = await pm.processes.createAndStartProcess({
  processKey: "registo-entidade",
  applicationBase: "registos",
  variables: [{ name: "nomeEntidade", value: "Exemplo Serviços, Lda." }],
});

const { data: tasks } = await pm.tasks.getTasksByProcessInstance(instance.id);
const pedido = tasks.content[0];

try {
  await pm.tasks.claimTask(pedido.id);
  await pm.tasks.completeTask(pedido.id, {
    variables: [{ name: "nif", value: "123456789" }],
  });
} catch (e) {
  if (e instanceof ApiClientError) console.error(e.status, e.details);
}
```

### Erros [#erros]

`ApiClientError` (exportado) tem `status` (HTTP; `0` em falha de rede ou timeout), `message` (`HTTP Error: <status> <statusText>`) e `details` (corpo da resposta).

---

## Runtime de tarefas (`client-ui`) [#runtime-de-tarefas-client-ui]

O `client-ui` desenha a página de uma tarefa: cabeçalho com o número da instância, **Guardar** e **Concluir Tarefa**, a barra de passos e o passo atual. As apps usam-no através das páginas do bundle; o essencial:

```tsx
// process/[...process]/layout.tsx (servidor) — /process/<processKey>/<instância>/<tarefa>/<id>
const config: IGRPProcessClientConfig = {
  baseUrl: process.env.PROCESS_MANAGEMENT_CLIENT_BASE_URL ?? "",
  accessToken: token?.accessToken ?? null,
  taskReturnUrl: processManagementAppHref("my-tasks", process.env.IGRP_APP_PAGE_TASK),
  summaryPage: "/process/resumo",
};

// process/[...process]/page.tsx (cliente)
<IGRPProcessProvider params={params} config={config} resolveStepComponent={resolve}>
  <IGRPProcessPage />
</IGRPProcessProvider>
```

| `IGRPProcessClientConfig` | Para quê |
|---|---|
| `baseUrl` | API do Process Management (sem `/api/v1`) |
| `accessToken` | Token do utilizador → `Authorization: Bearer` |
| `taskReturnUrl` | Para onde volta "Voltar" depois de concluir, quando o URL não traz `?returnUrl=` |
| `summaryPage` | Se definido, concluir com sucesso navega para `<summaryPage>/<instância>` em vez de mostrar o diálogo |

`IGRPProcessProvider` recebe `params` (`processKey`, `processInstanceId` — UUID ou número —, `userTaskKey`, `userTaskInstanceId`), `config`, `resolveStepComponent` e `mode` (`execution` | `consultation`; sem `userTaskInstanceId` assume consulta só de leitura). Dentro dele, `useIGRPProcessContext()` dá acesso a `stepConfig`, `completeTask`, `saveTask`, `getFormDataForTask`, `getVariableForTask`, `readOnly`…

### Que passo abre — Form Key [#que-passo-abre--form-key]

`getFormDiscovery(form)` traduz a Form Key da tarefa:

| Form Key | Resultado | Ficheiro (bundle) |
|---|---|---|
| `ui:form:<nome>@<v>` | `{ kind: "form", page: "<nome>.v<v>" }` | `process/[...process]/(<processKey>)/<nome>.v<v>.tsx` |
| `ui:shared:<nome>@<v>` ou outra | `{ kind: "shared", page }` | `process/shared/<nome>.v<v>.tsx` |
| sem Form Key | `default.v1` | `process/shared/default.v1.tsx` |

### Contrato de um passo [#contrato-de-um-passo]

O passo recebe `{ config: IGRPStepComponentConfig }` — `processKey`, `processInstanceId`, `userTaskInstanceId`, `processName`, `processNumber`, `variables`, `readOnly`, `onRegisterMethods` — e regista dois métodos:

```ts
config.onRegisterMethods?.({
  saveStep: async () => ({ success: true, variables, forms }),
  completeStep: async () => ({ success: true, variables, forms }),
});
```

A página usa o resultado assim: com `success: true`, envia `variables` (para o processo) e `forms` (dados do formulário, `{ name, value }[]`) para `saveTask`/`completeTask`; com `success: false`, mostra `error`. Os passos gerados pelo Studio já trazem este esqueleto; tu escreves o corpo em `handleSave`/`handleComplete`.

<Callout type="info" title="O tipo exportado não bate com o runtime">
`IGRPStepMethods` declara `{ success, data?, error? }`, mas a página lê `{ success, variables, forms, error }`. Segue o segundo formato (é o que os passos gerados devolvem).
</Callout>

### Dependências [#dependências]

- **Peers:** `next` 15.5, `next-auth` 4.24, `react`/`react-dom` 19.2.
- Depende diretamente do Design System **0.2.0-beta.3** — numa app com outra versão, força uma só com `overrides` (ver [Formulários BPMN](./forms.mdx#instalar-o-runtime-de-processos)).
- Estilos próprios em `@igrp/platform-process-management-client-ui/styles.css` (as páginas do bundle não o importam; usam os estilos do Design System da app).

<Callout type="warning" title="README desatualizado">
O README do pacote ainda fala de `@igrp/process-ui`, `ProcessPage` e `createProcessActions`, e de Next 14/React 18. Os nomes atuais são os desta página (`IGRPProcessPage`, `IGRPProcessProvider`, `IGRPProcessPageRenderer`).
</Callout>


# Process Studio · SDK TypeScript

URL: /pt/docs/developer/process/sdk-studio

Os pacotes @igrp/framework-process-studio-client, -types e -bpmn-editor — criar o cliente, métodos por recurso, erros e o editor BPMN em React.

# SDK TypeScript — Process Studio [#sdk-typescript--process-studio]

O frontend do Process Studio fala com o [Process Studio API](./api-studio.mdx) através de três pacotes, que também podes usar numa app tua (por exemplo, para listar ou publicar processos a partir de outro sistema):

| Pacote | Versão | Para quê |
|---|---|---|
| `@igrp/framework-process-studio-client` | 0.0.1-beta.16 | Cliente HTTP: projetos, definições, diagrama, publicação |
| `@igrp/framework-process-studio-types` | 0.0.1-beta.16 | Tipos partilhados (`Project`, `ProcessDefinition`, filtros, paginação) |
| `@igrp/framework-process-studio-bpmn-editor` | 0.1.0-beta.13 | Editor BPMN (bpmn-js + painel de propriedades) como componente React |

Os pacotes estão no registry `@igrp` (Sonatype) e são **ESM**, para Node ≥ 20.

```bash
pnpm add @igrp/framework-process-studio-client @igrp/framework-process-studio-types
```

---

## Criar o cliente [#criar-o-cliente]

```ts
import { createProcessStudioClient } from "@igrp/framework-process-studio-client";

const client = createProcessStudioClient({
  baseUrl: process.env.API_GATEWAY ?? "", // rota do gateway, SEM /api/v1
  timeout: 30000,                          // ms (default 30000)
  headers: { Authorization: `Bearer ${accessToken}` },
});
```

| Opção | Notas |
|---|---|
| `baseUrl` | Rota do gateway do `process-studio-api` (`http://<slug>:2575/gateway-api/<serviço>-process-studio-api`). O `/api/v1` já faz parte dos caminhos. Vazio → usa `NEXT_PUBLIC_API_GATEWAY`. |
| `timeout` | Cancela o pedido (AbortController). |
| `headers` | Junta-se a `Content-Type`/`Accept: application/json`. A autenticação vai aqui — não há opção `token`. |

No frontend do Process Studio o cliente é criado **no servidor**, por pedido, com o token da sessão NextAuth, e as server actions são consumidas no browser via TanStack Query:

```ts
// lib/server-client.ts
import { createProcessStudioClient } from "@igrp/framework-process-studio-client";
import { serverSession } from "@/lib/auth";

export async function createServerClient() {
  const session = await serverSession();
  if (!session?.accessToken) throw new Error("Authentication required.");
  return createProcessStudioClient({
    baseUrl: process.env.API_GATEWAY ?? "",
    headers: { Authorization: `Bearer ${session.accessToken}` },
  });
}
```

<Callout type="warning" title="O cliente regista cada pedido na consola">
Nesta versão, cada pedido faz `console.log` do URL e das opções — incluindo o cabeçalho `Authorization`. Usa-o no servidor e não em produção com logs partilhados até isto ser removido.
</Callout>

---

## Métodos [#métodos]

### `client.projects` [#clientprojects]

| Método | Pedido | Devolve |
|---|---|---|
| `getAll(filter?)` | `GET /api/v1/projects?appCode&pageNumber&pageSize` | `PaginatedResponse<Project>` |
| `getById(id)` | `GET /api/v1/projects/{id}` | `Project` |
| `create(project)` | `POST /api/v1/projects` | `Project` |
| `update(project)` | `PUT /api/v1/projects/{projectId}` | `Project` |
| `createOrUpdate(project)` | `PUT` se tiver `projectId`, senão `POST` | `Project` |
| `enable(id)` · `disable(id)` | `PATCH …/{id}/enable` · `/disable` | — |
| `delete(id)` | igual a `disable` (apagamento lógico) | — |
| `getDeployedProcesses(id, filter?)` | `GET /api/v1/projects/{id}/deployed-process` | `Project` com os processos publicados |
| `getProcessHistory(id, filter?)` | `GET /api/v1/projects/{id}/history-process` | `Project` |

O corpo de `create`/`update` é `{ code, name, description, appCode }`.

### `client.processDefinitions` [#clientprocessdefinitions]

| Método | Pedido | Devolve |
|---|---|---|
| `list(filter?)` | `GET /api/v1/projects/process-definitions` | `PaginatedResponse<ProcessDefinition>` |
| `getAll()` | lista os 100 primeiros projetos e junta os processos | `ProcessDefinition[]` |
| `getById(id)` | `GET …/process-definitions/{id}` | `ProcessDefinition` (com `bpmFileContent`) |
| `create(projectId, pd)` | `POST /api/v1/projects/{projectId}/process-definitions` | `ProcessDefinition` |
| `update(processId, pd)` | `PUT …/process-definitions/{processId}` | `ProcessDefinition` |
| `createOrUpdate(pd)` | conforme `processDefinitionId` | `ProcessDefinition` |
| `saveDiagram(processKey, { content })` | `PUT …/process-definitions/{processKey}/diagram` | `ProcessDefinition` |
| `deploy(processKey, { content })` | `POST …/process-definitions/{processKey}/deploy` | `ProcessDefinition` |
| `getVariables(id)` · `createOrUpdateVariable(id, vars)` | `GET` · `POST …/process-definitions/{id}/variables` | `VariableDefinition[]` |
| `delete(id)` · `restore(id)` | `PATCH …/{id}/delete` · `/restore` | — |
| `getStates()` | `GET /parameterization/process-definition-state` | `EnumItem[]` |

O corpo de `create`/`update` é `{ title, processKey, description, projectId, status }`. Os artefactos (User Tasks e Form Keys) chegam só em leitura, em `ProcessDefinition.processArtifacts`.

<Callout type="info" title="Namespace do XML">
O editor trabalha em `camunda`; o motor guarda em `activiti`. O frontend do Process Studio converte o conteúdo (`convertCamundaToActiviti`) antes de `saveDiagram` e `deploy`. Se chamares o cliente diretamente, envia o XML já com `activiti:formKey`.
</Callout>

### Exemplo — publicar um processo [#exemplo--publicar-um-processo]

```ts
"use server";
import { createServerClient } from "@/lib/server-client";

export async function publish(processKey: string, xml: string) {
  const client = await createServerClient();
  try {
    await client.processDefinitions.saveDiagram(processKey, { content: xml });
    return await client.processDefinitions.deploy(processKey, { content: xml });
  } catch (e) {
    const err = e as { status?: number; details?: { message?: string } };
    throw new Error(err.details?.message ?? `Falha ao publicar (HTTP ${err.status})`);
  }
}
```

---

## Erros [#erros]

Qualquer falha lança um `ApiClientError` com:

| Campo | Conteúdo |
|---|---|
| `message` | `HTTP Error: <status> <statusText>` |
| `status` | Código HTTP; `0` em falha de rede ou timeout |
| `details` | Corpo da resposta (JSON ou texto) |

A classe não é exportada: lê `status` e `details` do erro, como no exemplo acima.

---

## Ajudantes [#ajudantes]

| Função | O que faz |
|---|---|
| `formatDeploymentDate(valor)` | Data de publicação (ISO, array `LocalDateTime` do Java ou texto) → `dd/MM/yyyy HH:mm` |
| `formatAuthorSuffix(user)` | `" por <nome>"` ou vazio |
| `decorateProcessDefinitionRow(pd)` | Linha pronta para tabela (versão `N/D`, datas e autores formatados) |
| `filterProcessDefinitionsBySubstring(rows, filtros)` | Filtro local por `processKey`, `processName` (título) e `projectName` |
| `convertToMapOptions(items, nome?, valor?)` | Lista → `{ label, value }[]` para selects |

---

## Tipos [#tipos]

`@igrp/framework-process-studio-types` exporta, entre outros:

| Tipo | Campos principais |
|---|---|
| `Project` | `projectId`, `code`, `name`, `description`, `appCode`, `active`, `processDefinitions[]` |
| `ProcessDefinition` | `processDefinitionId`, `processKey`, `title`, `status`, `statusDesc`, `version`, `deploymentDate`, `bpmFileContent`, `processArtifacts[]` |
| `ProcessArtifact` | `taskKey`, `name`, `formKey`, `artifactVariables[]` |
| `ProcessDefinitionState` | `DRAFT` · `VALIDATED` · `PUBLISHED` · `DEPRECATED` · `ARCHIVED` · `DISABLED` · `ERROR` · `DELETED` |
| `ProcessDefinitionFilter` | `appCode`, `processKey`, `processName`, `projectCode`, `projectName`, `state`, `pageNumber`, `pageSize` |
| `PaginatedResponse<T>` | `content`, `pageNumber`, `pageSize`, `totalElements`, `totalPages`, `first`, `last` |

---

## Editor BPMN [#editor-bpmn]

```bash
pnpm add @igrp/framework-process-studio-bpmn-editor
```

Exporta `IGRPBpmnModeler`, `IGRPBpmnControls` e `IGRPZoomControls`. O modeler já inclui a barra de controlos e o zoom.

```tsx
"use client";
import { IGRPBpmnModeler } from "@igrp/framework-process-studio-bpmn-editor";

export function Editor({ pd, onSave }: { pd: ProcessDefinition; onSave: (xml: string) => void }) {
  return (
    <IGRPBpmnModeler
      processKey={pd.processKey}
      processName={pd.title}
      xml={pd.bpmFileContent ?? ""}
      onChange={onSave} // chamado a cada alteração no canvas
    />
  );
}
```

| Prop | Notas |
|---|---|
| `processKey`, `processName` | Obrigatórios; com `xml` vazio, criam o diagrama inicial |
| `xml` | Diagrama a carregar; o editor ignora o eco do seu próprio `onChange` |
| `onChange(xml)` | A cada alteração (`commandStack.changed`) e ao criar o diagrama inicial |
| `onLoad(modeler)` | Acesso à instância bpmn-js |
| `className` | Altura por defeito `calc(100dvh - 13rem)`, mínimo 480 px |

Requisitos:

- **Peers:** `next` 15, `next-auth` 4, `react`/`react-dom` 19; usa botões e toasts do Design System (precisa do provider de toasts na app).
- **Estilos:** o CSS do bpmn-js vem com o componente; as classes Tailwind não. Acrescenta ao CSS global da app:

  ```css
  @source "../../node_modules/@igrp/framework-process-studio-bpmn-editor/dist/**/*.{js,mjs}";
  ```

- **Uma instância por página** — o painel de propriedades usa um id fixo (`#js-properties-panel`).
- Segue o tema escuro pela classe `.dark` no `<html>`.

<Callout type="warning" title="Comportamentos desta versão">
- Carregar um ficheiro pelo botão de upload não chama `onChange`: grava o diagrama depois de uma alteração no canvas.
- O diagrama inicial (`xml` vazio) começa por `?xml` em vez de `<?xml`; o servidor rejeita-o ao guardar. Passa sempre um XML válido ou desenha antes de guardar.
</Callout>


# Modelar — Process Studio

URL: /pt/docs/developer/process/studio

Criar um projeto e um processo, desenhar ou importar o BPMN, definir as Form Keys e publicar no Process Management.

# Process Studio — Editor BPMN [#process-studio--editor-bpmn]

O **Process Studio** é a aplicação web onde se modelam os processos em BPMN 2.0. Faz parte da [stack de processos do workspace](../workspace/stack-process.mdx) e abre em `http://<slug>:2575/apps/igrp-process-studio`. Quando um processo é **publicado**, a definição segue para o [Process Management](./management.mdx), que passa a poder iniciar instâncias.

Este guia usa um exemplo simples, **Registo de Entidade**: um pedido, uma qualificação e uma decisão que pode devolver o pedido para retificação.

---

## Dashboard [#dashboard]

A página inicial lista os processos com projeto, chave, versão, estado e data de publicação. Os botões **Novo Projeto** e **Novo Processo** ficam no canto superior direito; em cada linha, os ícones abrem o editor, editam os dados ou apagam o processo.

<StudioPreview imageName="psDashboard" alt="Dashboard de processos com o processo Registo de Entidade do projeto Registos, versão 1, publicado" />

---

## Criar um projeto [#criar-um-projeto]

Um processo pertence sempre a um projeto. Em **Novo Projeto** define o **Código**, o **Nome** e, opcionalmente, a **Descrição**.

<StudioPreview imageName="psNovoProjeto" alt="Diálogo Criar um Novo Projeto com código REGISTOS, nome Registos e descrição" />

<Callout type="info" title="Código do projeto">
É o código (`REGISTOS`) que o Studio IDE mostra quando se liga ao Process Studio para escolher o projeto. Usa um código curto e estável.
</Callout>

---

## Criar um processo [#criar-um-processo]

Em **Novo Processo** define o **Título**, a **Chave do processo** (o identificador no runtime, por exemplo `registo-entidade`), uma descrição e o **Projeto**.

<StudioPreview imageName="psNovoProcesso" alt="Diálogo de criação de processo com título Registo de Entidade, chave registo-entidade e projeto Registos" />

O processo fica em **Rascunho** até ser publicado.

---

## Editor de Diagrama [#editor-de-diagrama]

O editor abre com três separadores: **Editor de Diagrama**, **XML** e **Delegates e Variáveis**. No canvas, a paleta de elementos BPMN fica à esquerda e o painel de propriedades à direita. A barra vertical junto ao painel tem, por esta ordem: mostrar/esconder o painel, descarregar o XML, descarregar SVG, descarregar PNG e **carregar um diagrama** (`.bpmn`). Por baixo ficam o zoom e **Ajustar à Tela**.

<StudioPreview imageName="psEditor" alt="Editor de Processo com o diagrama Início, Pedido, Qualificação, gateway decisão com o fluxo Retificar de volta ao Pedido, e Fim" />

O exemplo tem:

| Elemento | ID | Configuração |
|---|---|---|
| Início | `StartEvent_1` | — |
| User Task **Pedido** | `Task_pedido` | Form Key `ui:form:pedido@1` |
| User Task **Qualificação** | `Task_qualificacao` | Form Key `ui:form:qualificacao@1` |
| Gateway exclusivo **decisão** | `Gateway_0mcbth4` | — |
| Fluxo **Retificar** (volta ao Pedido) | `Flow_0vupp2u` | `${decision == 'RECTIFICAR'}` |
| Fluxo **confirmar ou rejeitar** (para o Fim) | `Flow_06lggtn` | `${decision == 'CONFIRMAR' \|\| decision == 'REJEITAR'}` |

As condições usam a variável `decision`, que vem do formulário da Qualificação (ver [Formulários BPMN](./forms.mdx)).

<Callout type="warning" title="Depois de carregar um .bpmn, mexe no diagrama antes de guardar">
Nesta versão, carregar um ficheiro com **Upload Diagram** mostra o diagrama mas não o marca como alterado: **Guardar** e **Publicar** enviariam o diagrama anterior e mudar de separador repõe-no. Depois do upload, desloca um elemento no canvas — isso dispara a gravação automática (2 s) com o diagrama novo.
</Callout>

---

## Formulários em User Tasks [#formulários-em-user-tasks]

Cada User Task indica o formulário que a executa. Seleciona a tarefa, abre a secção **Forms** no painel de propriedades, escolhe o tipo **Embedded or External Task Forms** e preenche o **Form key**:

<StudioPreview imageName="psFormKey" alt="Painel de propriedades da User Task Pedido, secção Forms, com Form key ui:form:pedido@1" />

| Formato | Exemplo | Onde vive o formulário |
|---|---|---|
| `ui:form:<nome>@<versão>` | `ui:form:pedido@1` | Passo próprio do processo, gerado no Studio IDE em `(igrp)/(generated)/process/[...process]/(<processKey>)/<nome>.v<versão>.tsx` |
| `ui:shared:<página>@<versão>` | `ui:shared:default@1` | Formulário partilhado entre processos, em `(igrp)/(generated)/process/shared/<página>.v<versão>.tsx` |

Se o Form Key estiver vazio, o runtime usa `ui:shared:default@1`.

---

## Guardar e publicar [#guardar-e-publicar]

- **Guardar** grava o rascunho. O editor também grava sozinho 2 segundos depois de cada alteração no canvas.
- **Publicar** envia o BPMN para o Process Management, que o valida, cria uma **versão** nova (`v1`, `v2`…) e passa a poder iniciar instâncias dessa versão. O processo passa a **Publicado** no dashboard.

<Callout type="warning" title="Versões são imutáveis">
Uma versão publicada não muda. Para corrigir o processo, edita e publica de novo: as instâncias já iniciadas continuam na versão com que arrancaram.
</Callout>

<Callout type="info" title="Publicar e o applicationBase">
O Process Management identifica a aplicação dona de cada processo pelo `applicationBase`. As imagens `0.2.0-beta` do Process Management exigem esse campo no deploy, e o Process Studio dessa versão não o envia; nesse caso **Publicar** devolve *Internal Server Error*. A correção está no Process Management (`applicationBase` opcional no deploy); usa uma imagem que a inclua.
</Callout>

---

## XML BPMN [#xml-bpmn]

O separador **XML** mostra o BPMN 2.0 gravado (só de leitura) e tem o botão **Copiar XML**. O Process Studio grava com o namespace `activiti` (o motor do Process Management); o editor converte de e para `camunda` ao abrir e guardar.

<StudioPreview imageName="psXml" alt="Separador XML com o BPMN do processo registo-entidade e as form keys activiti:formKey" />

## Delegates e Variáveis [#delegates-e-variáveis]

O separador **Delegates e Variáveis** documenta os delegates disponíveis para Service Tasks.

<StudioPreview imageName="psDelegates" alt="Separador Delegates e Variáveis com o igrpSendEmailDelegate, a sintaxe e os parâmetros" />

Os delegates configuram-se numa **Service Task**, com **Implementation Type** `Delegate Expression` e a sintaxe `${<nome_do_delegate>}`.

Os parâmetros de cada delegate são injectados como **Field Injections** na própria tarefa ou definidos como variáveis globais do processo (as globais sobrepõem-se às da tarefa). O resultado de cada Service Task fica numa variável `<task-id>Data`, acedida com dot-notation (`testeData.nome`) ou índice de array (`testeData[0].email`).

### Delegates disponíveis [#delegates-disponíveis]

| Delegate | Expressão | Descrição |
|---|---|---|
| Webhook | `${igrpWebhookDelegate}` | Chamada HTTP a um endpoint externo |
| JSON Parser | `${igrpJsonParseDelegate}` | Parsing de JSON (raw ou Base64) |
| Email | `${igrpSendEmailDelegate}` | Envio de email |
| Message Broker | `${igrpMessageBrokerSenderDelegate}` | Publica dados do processo num broker (Kafka/RabbitMQ) |
| Process Webhook | `${igrpProcessWebhookDelegate}` | Alternativa ao broker — envia dados via HTTP POST |
| User Assignment | `${igrpExternalUserAssignmentDelegate}` | Atribui dinamicamente uma User Task com base numa API externa |

### igrpWebhookDelegate [#igrpwebhookdelegate]

Executa um pedido HTTP a um endpoint externo.

| Parâmetro | Obrigatório | Descrição |
|---|---|---|
| `webhookUrl` | Sim | URL base do endpoint |
| `webhookMethod` | Sim | Método HTTP (`GET`, `POST`, `PUT`, `DELETE`) |
| `webhookUrlPath` | Não | Path adicional a acrescentar ao URL |
| `webhookQueryParams` | Não | Query parameters |
| `webhookPayload` | Não | Corpo do pedido |
| `webhookPayloadHeader` | Não | Headers HTTP (JSON) |

<Callout type="info" title="Autenticação automática">
Se não forneceres headers, o delegate injeta automaticamente um token machine-to-machine para chamadas internas ao iGRP.
</Callout>

### igrpJsonParseDelegate [#igrpjsonparsedelegate]

Faz parsing de um JSON (raw ou Base64) e guarda o resultado em `<task-id>Data`.

| Parâmetro | Obrigatório | Descrição |
|---|---|---|
| `json` | Sim | String JSON ou Base64 |
| `isBase64Encoded` | Não | `true` se o JSON estiver codificado em Base64 |

### igrpSendEmailDelegate [#igrpsendemaildelegate]

| Parâmetro | Obrigatório | Descrição |
|---|---|---|
| `emailTo` | Sim | Endereço de destino |
| `emailSubject` | Sim | Assunto |
| `emailBody` | Sim | Corpo do email |
| `emailFrom` | Não | Remetente |

### igrpMessageBrokerSenderDelegate [#igrpmessagebrokersenderdelegate]

Publica os dados do processo num broker (Kafka/RabbitMQ).

| Parâmetro | Obrigatório | Descrição |
|---|---|---|
| `topic` | Sim | Tópico do broker |

Se não tiveres broker, usa `${igrpProcessWebhookDelegate}` com os parâmetros `url` (obrigatório), `path` e `headers`.

### igrpExternalUserAssignmentDelegate [#igrpexternaluserassignmentdelegate]

Chama uma API externa, extrai um utilizador da resposta via JSONPath e cria uma regra de atribuição para a User Task alvo.

| Parâmetro | Obrigatório | Descrição |
|---|---|---|
| `apiUrl` | Sim | URL da API. Suporta `${variavel}` e `$[ENV_VAR]` |
| `apiMethod` | Não | Método HTTP (default: `GET`) |
| `apiPayload` | Não | Corpo (POST/PUT) |
| `jsonPathExpression` | Sim | JSONPath para extrair o identificador do utilizador. Ex: `$.data.email` |
| `priorityJsonPathExpression` | Não | JSONPath para extrair a prioridade da tarefa |
| `targetTaskKey` | Sim | ID da User Task a atribuir (deve existir no BPMN) |
| `assignmentMode` | Não | `ONE_TIME` (default) ou `ALWAYS` |
| `outputVariable` | Não | Variável onde guardar o identificador extraído |

Usa `$[NOME_VAR]` nos parâmetros para referenciar variáveis de ambiente (ex: `$[BACKEND_URL]/api/v1/users`).

---

---

## Próximo passo [#próximo-passo]

Com o processo publicado, desenha os formulários das User Tasks no [Studio IDE](./forms.mdx) e depois opera as instâncias no [Process Management](./management.mdx).


# APIs da plataforma

URL: /pt/docs/developer/reference/api-reference

Índice das APIs REST do ecossistema iGRP — Access Management, Process Studio, Process Management e Notificações — e as convenções comuns de rota, autenticação, paginação e erros.

# APIs da plataforma [#apis-da-plataforma]

Cada serviço da plataforma expõe a sua própria API REST e está documentado **na secção do seu domínio**, com a referência de endpoints gerada do OpenAPI do serviço. Para a usar a partir de um assistente de IA, ver [Documentação com IA](./documentacao-ia.mdx). Num workspace, todos são alcançados através do gateway, em `http://<slug>:2575/gateway-api/<nome-eureka>`.

| API | Secção | Rota no gateway + prefixo | Stack | SDK | Endpoints |
|---|---|---|---|---|---|
| [Access Management](/docs/developer/access-management/api) | Access Management | `/gateway-api/<slug>-access-management` + `/api/...` | Spring Boot 3.5 · Java 26 | [M2M Java](/docs/developer/access-management/m2m-api) | [Referência](/docs/developer/access-management/referencia-api/utilizadores) |
| [Process Studio](/docs/developer/process/api-studio) | Processos | `/gateway-api/<serviço>-process-studio-api` + `/api/v1/...` | Spring Boot 3.5 · Java 26 | [TypeScript](/docs/developer/process/sdk-studio) | [Referência](/docs/developer/process/referencia-process-studio/projetos) |
| [Process Management](/docs/developer/process/api-management) | Processos | `/gateway-api/<serviço>-process-management-api` (sem prefixo) | Spring Boot 4.1 · Java 26 · Activiti | [TypeScript](/docs/developer/process/sdk-management) | [Referência](/docs/developer/process/referencia-process-management/areas) |
| [Notificações](/docs/developer/notification/api) | Notificações | `/gateway-api/igrp-notification-service` + `/api/v1/...` | Spring Boot 3.5 · Java 26 | [TypeScript](/docs/developer/notification/frontend-client) · [Java](/docs/developer/notification/java-client) | [Referência](/docs/developer/notification/referencia-api/notificacoes-e-lotes) |

O `<nome-eureka>` é o nome com que o serviço se regista no Eureka (em produção, o ingress do Kubernetes faz o mesmo papel — ver [Cloud & Deploy](/docs/developer/cloud)).

## Autenticação [#autenticação]

Os tokens são JWT emitidos pelo **Access Management**, que federa o fornecedor de identidade real (Autentika, Keycloak ou WSO2). Cada pedido leva:

```http
Authorization: Bearer <token>
```

- **Utilizadores** — o token da sessão da app (NextAuth com o fornecedor `igrp-auth`).
- **Serviços** — token `client_credentials` do Access Management; ver [API M2M e SDK Java](/docs/developer/access-management/m2m-api).

## Convenções [#convenções]

- Corpo e respostas em `application/json`.
- Listas paginadas com `page`/`size` (ou `pageNumber`/`pageSize` no Process Studio) e resposta com `content`, `totalElements`, `totalPages`.
- Identificadores de recursos são UUID (instâncias, tarefas, projetos, processos).
- Erros de validação seguem *problem details*: `status`, `title`, `errors` (campo → mensagem) e `instance`. Outros erros usam o corpo por defeito do Spring: `timestamp`, `status`, `error`, `path`.
- Cada serviço publica a sua especificação OpenAPI (springdoc), que é a referência exata de cada versão.


# Código de conduta

URL: /pt/docs/developer/reference/code-of-conduct

Normas de comportamento e participação na comunidade IGRP.

# Código de conduta [#código-de-conduta]

## O nosso compromisso [#o-nosso-compromisso]

No interesse de promover um ambiente aberto e acolhedor, nós, como contribuidores e mantenedores, comprometemo-nos a tornar a participação neste projeto e na nossa comunidade uma experiência livre de assédio para todos, independentemente da idade, tamanho corporal, deficiência, etnia, identidade e expressão de género, nível de experiência, nacionalidade, aparência pessoal, raça, religião ou identidade e orientação sexual.

## Os nossos padrões [#os-nossos-padrões]

Exemplos de comportamentos que contribuem para criar um ambiente positivo:

- Usar linguagem acolhedora e inclusiva
- Respeitar pontos de vista e experiências diferentes
- Aceitar graciosamente críticas construtivas
- Focar no que é melhor para a comunidade
- Mostrar empatia para com os outros membros da comunidade

Exemplos de comportamentos inaceitáveis:

- Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais indesejados
- Trolling, comentários insultuosos/depreciativos e ataques pessoais ou políticos
- Assédio público ou privado
- Publicar informações privadas de outros, como endereço físico ou eletrónico, sem permissão explícita
- Outras condutas que possam ser consideradas inapropriadas num ambiente profissional

## As nossas responsabilidades [#as-nossas-responsabilidades]

Os mantenedores do projeto são responsáveis por esclarecer os padrões de comportamento aceitável e espera-se que tomem medidas corretivas adequadas e justas em resposta a quaisquer instâncias de comportamento inaceitável.

Os mantenedores do projeto têm o direito e a responsabilidade de remover, editar ou rejeitar comentários, commits, código, edições de wiki, issues e outras contribuições que não estejam alinhadas com este Código de Conduta, ou banir temporária ou permanentemente qualquer contribuidor por outros comportamentos que considerem inapropriados, ameaçadores, ofensivos ou prejudiciais.

## Âmbito [#âmbito]

Este Código de Conduta aplica-se tanto nos espaços do projeto como nos espaços públicos quando um indivíduo representa o projeto ou a sua comunidade. Exemplos de representação de um projeto ou comunidade incluem o uso de um endereço de e-mail oficial do projeto, publicação através de uma conta oficial nas redes sociais, ou atuação como representante designado num evento online ou offline.

## Execução [#execução]

Instâncias de comportamento abusivo, de assédio ou de outra forma inaceitável podem ser reportadas contactando a equipa do projeto em **igrp@nosi.cv**. Todas as queixas serão analisadas e investigadas, resultando numa resposta considerada necessária e adequada às circunstâncias.

Os mantenedores do projeto que não seguirem ou aplicarem o Código de Conduta de boa-fé podem enfrentar repercussões temporárias ou permanentes, conforme determinado por outros membros da liderança do projeto.


# Código-fonte

URL: /pt/docs/developer/reference/codigo-fonte

Os repositórios públicos do iGRP 3.0 no GitHub (NOSiCode-CV), por domínio — onde ver o código, abrir issues e propor alterações.

# Código-fonte [#código-fonte]

O código do iGRP 3.0 está publicado no GitHub, na organização **[NOSiCode-CV](https://github.com/NOSiCode-CV)**: um repositório por módulo. É aí que podes ler o código, abrir issues e propor alterações — ver o [Guia de contribuição](./contributing.mdx).

O branch publicado de cada repositório é o branch por omissão (`github-release`).

## Framework [#framework]

| Repositório | Conteúdo |
|---|---|
| [igrp-framework-core-java](https://github.com/NOSiCode-CV/igrp-framework-core-java) | Núcleo Java (`cv.igrp.framework:core`): CQRS, base de todos os backends |
| [igrp-framework-stereotype-java](https://github.com/NOSiCode-CV/igrp-framework-stereotype-java) | Estereótipos Java usados pelo código gerado |
| [igrp-framework-auth-backend-monorepo](https://github.com/NOSiCode-CV/igrp-framework-auth-backend-monorepo) | Autenticação e autorização no backend (`@IgrpPermission`, sincronização com o Access Management) |
| [igrp-framework-nextjs](https://github.com/NOSiCode-CV/igrp-framework-nextjs) | Framework Next.js, Design System, template das apps e `template-migrator` |
| [igrp-framework-auth-frontend-monorepo](https://github.com/NOSiCode-CV/igrp-framework-auth-frontend-monorepo) | Autenticação no frontend |

## Access Management [#access-management]

| Repositório | Conteúdo |
|---|---|
| [igrp-platform-access-management](https://github.com/NOSiCode-CV/igrp-platform-access-management) | API do Access Management (servidor OAuth2, administração, M2M) |
| [igrp-application-center](https://github.com/NOSiCode-CV/igrp-application-center) | Application Center (consola de administração) |

## Processos (BPMN) [#processos-bpmn]

| Repositório | Conteúdo |
|---|---|
| [igrp-process-studio-backend-api](https://github.com/NOSiCode-CV/igrp-process-studio-backend-api) | API do Process Studio |
| [igrp-process-studio-backend-monorepo](https://github.com/NOSiCode-CV/igrp-process-studio-backend-monorepo) | Módulos Java do Process Studio (SDK e integração) |
| [igrp-process-studio-frontend](https://github.com/NOSiCode-CV/igrp-process-studio-frontend) | UI do Process Studio |
| [igrp-process-studio-frontend-monorepo](https://github.com/NOSiCode-CV/igrp-process-studio-frontend-monorepo) | Pacotes TypeScript do Process Studio (cliente, tipos, editor BPMN) |
| [igrp-process-management-backend-api](https://github.com/NOSiCode-CV/igrp-process-management-backend-api) | API do Process Management (motor BPMN) |
| [igrp-process-management-backend-monorepo](https://github.com/NOSiCode-CV/igrp-process-management-backend-monorepo) | Módulos Java do Process Management |
| [igrp-process-management-frontend](https://github.com/NOSiCode-CV/igrp-process-management-frontend) | UI do Process Management |
| [igrp-process-management-frontend-monorepo](https://github.com/NOSiCode-CV/igrp-process-management-frontend-monorepo) | Pacotes TypeScript do Process Management (cliente, tipos, `client-ui`) |

## Cloud [#cloud]

| Repositório | Conteúdo |
|---|---|
| [igrp-cloud-gateway](https://github.com/NOSiCode-CV/igrp-cloud-gateway) | API Gateway |
| [igrp-cloud-eureka](https://github.com/NOSiCode-CV/igrp-cloud-eureka) | Service discovery (Eureka) |

## Studio [#studio]

| Repositório | Conteúdo |
|---|---|
| [igrp-studio-ide](https://github.com/NOSiCode-CV/igrp-studio-ide) | Studio IDE (Electron) e as releases para instalar |
| [igrp-studio-nextjs-engine](https://github.com/NOSiCode-CV/igrp-studio-nextjs-engine) | Gerador de código Next.js |
| [igrp-studio-springboot-engine](https://github.com/NOSiCode-CV/igrp-studio-springboot-engine) | Gerador de código Spring Boot |

## Exemplos [#exemplos]

| Repositório | Conteúdo |
|---|---|
| [demo-training-backend](https://github.com/NOSiCode-CV/demo-training-backend) | Projeto de demonstração — backend |
| [demo-training-frontend](https://github.com/NOSiCode-CV/demo-training-frontend) | Projeto de demonstração — frontend |
| [igrp-springboot-skills](https://github.com/NOSiCode-CV/igrp-springboot-skills) | Skills de agentes de IA para projetos Spring Boot iGRP |

<Callout type="info" title="Ainda não publicados">
O Notification Service (serviço, consola e SDKs), a CLI `@igrp/cli` e as bibliotecas de ficheiros e email ainda não têm repositório público. Para estes, reporta pelo [e-mail ou Discord](./support.mdx).
</Callout>

Os repositórios marcados como v1/v2 na organização (por exemplo `IGRP-Framework`) são das versões anteriores do iGRP.


# Componentes da documentação

URL: /pt/docs/developer/reference/componentes-da-documentacao

Os componentes MDX para escrever páginas desta documentação, disponíveis sem import — não confundir com o Design System do iGRP usado pelas aplicações.

## Badge [#badge]

<Badge>neutral</Badge> <Badge color="blue" icon="Info">blue</Badge> <Badge color="green" icon="Check">green</Badge> <Badge color="teal">teal</Badge> <Badge color="purple">purple</Badge> <Badge color="orange">orange</Badge> <Badge color="amber">amber</Badge> <Badge color="red" icon="AlertTriangle">red</Badge> <Badge color="cyan">cyan</Badge> <Badge color="primary">primary</Badge> <Badge color="blue" outline>outline</Badge>

```mdx
<Badge color="green" icon="Check">Disponível</Badge>
```

## Grid + Feature [#grid--feature]

<Grid cols={3}>
  <Feature icon="Rocket" color="blue" title="Rápido de começar">
    Workspace criado e a correr em minutos.
  </Feature>
  <Feature icon="ShieldCheck" color="green" title="Seguro por defeito" eyebrow="Access Manager">
    Permissões e menus sincronizados com o IAM.
  </Feature>
  <Feature icon="Blocks" color="purple" title="Low code" href="/docs/developer/introducao">
    Clica para navegar — este card é um link.
  </Feature>
</Grid>

```mdx
<Grid cols={3}>
  <Feature icon="Rocket" color="blue" title="Título">Descrição.</Feature>
</Grid>
```

## Checklist [#checklist]

<Checklist cols={2}>
  <Item title="IGRP Studio instalado e funcional" />
  <Item title="Um workspace criado e a correr">Com todos os serviços verdes.</Item>
  <Item status="warn" title="Docker Desktop a correr">Necessário para os serviços locais.</Item>
  <Item status="no" title="Sem permissão de administrador" />
  <Item status="info" title="Opcional: Git configurado" />
</Checklist>

## Cards (Fumadocs) [#cards-fumadocs]

<Cards>
  <Card title="Introdução" href="/docs/developer/introducao" description="Do zero ao primeiro projeto." />
  <Card title="CLI" href="/docs/developer/studio/cli" description="Comandos do @igrp/cli." />
</Cards>

## Steps (Fumadocs) [#steps-fumadocs]

<Steps>
  <Step>
    ### Instalar o Studio [#instalar-o-studio]
    Faz download e instala a versão para o teu sistema.
  </Step>
  <Step>
    ### Instalar o CLI [#instalar-o-cli]
    No primeiro arranque, clica em **Install CLI**.
  </Step>
  <Step>
    ### Executar [#executar]
    No separador **Serviços**, ▶ na IGRP Stack levanta a plataforma.
  </Step>
</Steps>

## Callout [#callout]

<Callout type="info" title="Info">Nota que complementa o texto.</Callout>
<Callout type="tip" title="Dica">Atalho útil.</Callout>
<Callout type="objective" title="Objetivo">O que vais aprender nesta página.</Callout>
<Callout type="warning" title="Atenção">Pré-condição ou armadilha.</Callout>
<Callout type="error" title="Erro">Comportamento que quebra se ignorado.</Callout>

## Tabs [#tabs]

<Tabs>
  <TabItem value="npm" label="npm">`npm install @igrp/cli`</TabItem>
  <TabItem value="pnpm" label="pnpm">`pnpm add @igrp/cli`</TabItem>
</Tabs>

## Accordions (Fumadocs) [#accordions-fumadocs]

<Accordions>
  <Accordion title="Primeiro acesso">Recebes um convite por email com o link de acesso.</Accordion>
  <Accordion title="Acessos seguintes">Entra com utilizador e palavra-passe.</Accordion>
</Accordions>

## Files (Fumadocs) [#files-fumadocs]

<Files>
  <Folder name="workspace" defaultOpen>
    <File name="igrp-compose.yaml" />
    <Folder name="projects">
      <File name="my-first-project" />
    </Folder>
  </Folder>
</Files>

## NextStep [#nextstep]

<NextStep href="/docs/developer/introducao#como-começar" title="Getting Started">
  Instala o Studio e cria o teu primeiro workspace.
</NextStep>


# Guia de contribuição

URL: /pt/docs/developer/reference/contributing

Como reportar issues, propor funcionalidades e submeter Pull Requests para o projeto IGRP.

# Guia de contribuição [#guia-de-contribuição]

Obrigado por pensares em contribuir para o IGRP! Este guia explica como participar de forma eficaz.

---

## Código de conduta [#código-de-conduta]

Ao participar neste projeto, concordas em cumprir o nosso [Código de conduta](./code-of-conduct.mdx). Por favor lê-o antes de contribuires.

---

## Colocar dúvidas [#colocar-dúvidas]

Antes de abrir uma issue, verifica se a tua dúvida já foi respondida:

- Consulta a [documentação](/docs/developer/introducao#como-começar)
- Pesquisa nas issues do repositório no [GitHub](./codigo-fonte.mdx)
- Contacta a equipa em **igrp@nosi.cv** ou no [Discord](https://discord.gg/dywFBFaCQr)

---

## Reportar um bug [#reportar-um-bug]

Ao reportar um bug, inclui sempre:

- Versão do IGRP Studio
- Sistema operativo e versão
- Passos para reproduzir o problema
- Comportamento esperado vs. comportamento observado
- Logs relevantes ou screenshots

---

## Propor uma funcionalidade [#propor-uma-funcionalidade]

Para propor uma nova funcionalidade:

- Verifica se já existe uma issue semelhante
- Descreve o problema que resolve
- Explica a solução proposta
- Considera alternativas que tenhas ponderado

---

## Onde reportar [#onde-reportar]

| Canal | Para quê |
|---|---|
| **Issues no GitHub** | Bugs e pedidos de funcionalidade, no repositório do módulo — ver [Código-fonte](./codigo-fonte.mdx) |
| **E-mail** — igrp@nosi.cv | Bugs, pedidos e módulos que ainda não têm repositório público |
| **Discord** — [discord.gg/dywFBFaCQr](https://discord.gg/dywFBFaCQr) | Bugs e dúvidas rápidas, em conversa com a equipa |

Vulnerabilidades de segurança nunca em issues públicas nem no Discord — ver [Política de segurança](./security.mdx).

---

## Submeter pull requests [#submeter-pull-requests]

O código está no GitHub, na organização [NOSiCode-CV](https://github.com/NOSiCode-CV), um repositório por módulo (lista em [Código-fonte](./codigo-fonte.mdx)).

1. Faz fork do repositório do módulo
2. Cria o teu branch a partir do branch por omissão (`github-release`): `feature/<nome>`, `fix/<nome>` ou `refactor/<nome>`
3. Faz as alterações com testes
4. Corre os testes do módulo: `mvn test` (backend) ou o script de testes do `package.json` (frontend)
5. Abre o pull request contra o branch por omissão, a descrever o problema e a solução

A equipa revê o pull request e integra a alteração nas versões seguintes.

### Convenções de commit [#convenções-de-commit]

Seguimos o [Conventional Commits](https://www.conventionalcommits.org/):

| Prefixo | Uso |
|---|---|
| `feat:` | Nova funcionalidade |
| `fix:` | Correção de bug |
| `docs:` | Alterações na documentação |
| `chore:` | Tarefas de manutenção (build, deps) |
| `refactor:` | Refatoração sem mudança de comportamento |
| `test:` | Adição ou correção de testes |

Exemplos:

```
feat(access-management): add M2M client credential flow
fix(nextjs-engine): emit static select options
docs(process): document the TypeScript SDKs
```

## Escrever documentação [#escrever-documentação]

As páginas desta documentação são MDX e usam apenas Markdown e os [componentes da documentação](/docs/developer/reference/componentes-da-documentacao) — sem HTML/CSS próprio. Os diagramas de arquitetura usam o kit de diagramas (`src/components/diagrams`), nunca imagens com cores fixas. Consulta essa página para ver cada componente com exemplo de uso.


# Documentação com IA

URL: /pt/docs/developer/reference/documentacao-ia

Usar esta documentação a partir de assistentes de IA — servidor MCP, ficheiros para LLMs e as ações de cada página.

# Documentação com IA [#documentação-com-ia]

A documentação pode ser lida por assistentes de IA (Claude, ChatGPT, Cursor, Copilot…) de três formas: pelo servidor MCP, pelos ficheiros para LLMs ou pelas ações de cada página.

## Servidor MCP [#servidor-mcp]

| | |
|---|---|
| **URL** | `https://docs3.igrp.cv/api/mcp` |
| **Transporte** | Streamable HTTP, sem autenticação |
| **Ferramentas** | `search` (pesquisa), `list_pages` (índice das páginas), `get_page` (Markdown de uma página pelo URL) |

Com o servidor ligado, o assistente pesquisa e lê a documentação quando precisa — incluindo a referência das APIs (cada operação com a permissão, os parâmetros, o corpo e as respostas).

<Tabs>
<TabItem value="claude-code" label="Claude Code">

```bash
claude mcp add --transport http igrp-docs https://docs3.igrp.cv/api/mcp
```

</TabItem>
<TabItem value="cursor" label="Cursor">

Em `.cursor/mcp.json` (no projeto) ou `~/.cursor/mcp.json` (global):

```json
{
  "mcpServers": {
    "igrp-docs": { "url": "https://docs3.igrp.cv/api/mcp" }
  }
}
```

</TabItem>
<TabItem value="vscode" label="VS Code">

Em `.vscode/mcp.json`:

```json
{
  "servers": {
    "igrp-docs": { "type": "http", "url": "https://docs3.igrp.cv/api/mcp" }
  }
}
```

</TabItem>
</Tabs>

## Ficheiros para LLMs [#ficheiros-para-llms]

| Endereço | Conteúdo |
|---|---|
| [`/llms.txt`](/llms.txt) | Índice de todas as páginas, com o título e a descrição |
| [`/llms-full.txt`](/llms-full.txt) | Toda a documentação num só ficheiro Markdown |
| `/llms.mdx/<caminho>` | Markdown de uma página (por exemplo `/llms.mdx/developer/introducao`) |

## Ações da página [#ações-da-página]

No topo de cada página:

- **Copiar página** — copia o Markdown da página para colar num assistente.
- **Mais opções** — **Ver como Markdown** e **Abrir no ChatGPT**, **Claude**, **Cursor** ou **Scira AI**, com a página já indicada no pedido.


# Histórico

URL: /pt/docs/developer/reference/history

Evolução do IGRP e principais marcos da plataforma desde a sua criação.

# Histórico do iGRP [#histórico-do-igrp]

## iGRP 3.0 — Horizon [#igrp-30--horizon]

A versão **3.0 Horizon** representa uma reescrita completa da plataforma, com foco em:

- Arquitetura de microserviços independentes
- Developer Experience com iGRP Studio (IDE visual)
- Geração de código para Next.js, Spring Boot e .NET
- Motor de processos BPMN integrado (Activiti), modelado no Process Studio
- Sistema de permissões granular com RBAC

### Princípios arquiteturais [#princípios-arquiteturais]

- **Separação de concerns** — cada serviço tem uma única responsabilidade
- **API-first** — todas as integrações via REST com JWT
- **BFF Pattern** — Backend for Frontend para otimizar o contrato com o cliente
- **Code Generation** — Studio gera código production-ready sem boilerplate

---

## Conceitos fundamentais [#conceitos-fundamentais]

O IGRP 3.0 assenta em três pilares:

1. **iGRP Studio** — IDE visual para criar projetos, páginas e processos sem escrever boilerplate
2. **Framework** — as bibliotecas partilhadas (`@igrp/framework-next`, Design System, `cv.igrp.framework:core`) que o código gerado usa em runtime; os *engines* do Studio são os geradores desse código
3. **Serviços de plataforma** — Access Management, Process Studio, Process Management e Notificações como infraestrutura transversal


# Visão geral

URL: /pt/docs/developer/reference

Documentos transversais ao ecossistema iGRP — como contribuir e escrever documentação, índice das APIs, código de conduta, segurança, suporte e histórico.

# Referência [#referência]

Documentos transversais ao ecossistema iGRP.

| Documento | Descrição |
|---|---|
| [Código-fonte](./codigo-fonte.mdx) | Os repositórios públicos do iGRP 3.0 no GitHub, por domínio |
| [Guia de contribuição](./contributing.mdx) | Como reportar issues, propor funcionalidades, submeter pull requests e escrever documentação |
| [APIs da plataforma](./api-reference.mdx) | Índice das APIs REST e SDKs, com as convenções comuns |
| [Código de conduta](./code-of-conduct.mdx) | Normas de comportamento e participação na comunidade |
| [Política de segurança](./security.mdx) | Como reportar vulnerabilidades e medidas de segurança adotadas |
| [Suporte](./support.mdx) | Canais disponíveis para obter ajuda |
| [Histórico](./history.mdx) | Evolução do iGRP e principais marcos da plataforma |


# Política de segurança

URL: /pt/docs/developer/reference/security

Como reportar vulnerabilidades e as medidas de segurança adotadas no ecossistema IGRP.

# Política de segurança [#política-de-segurança]

## Reportar uma vulnerabilidade [#reportar-uma-vulnerabilidade]

Se descobrires uma vulnerabilidade de segurança no IGRP, **não abras uma issue pública**. Em vez disso, envia um e-mail para **igrp@nosi.cv** com:

- Descrição da vulnerabilidade
- Passos para reproduzir
- Impacto potencial
- Sugestão de mitigação (se tiveres)

Trabalharemos contigo para resolver o problema antes de qualquer divulgação pública.

---

## Medidas de segurança [#medidas-de-segurança]

### Autenticação [#autenticação]

- Autenticação via **JWT Bearer Token** em todas as APIs
- Tokens com expiração configurável
- Suporte a refresh tokens
- Integração com Keycloak para SSO

### Dados [#dados]

- Comunicação exclusivamente via **HTTPS/TLS**
- Variáveis de ambiente para segredos (nunca hardcoded)
- Validação de input em todos os endpoints

### Autorização [#autorização]

- Controlo de acesso baseado em funções (RBAC)
- Verificação de permissões por recurso e ação
- Auditoria de acessos e alterações

### Infraestrutura [#infraestrutura]

- Containers Docker com imagens mínimas
- Segredos geridos via variáveis de ambiente ou cofres
- Actualizações de dependências monitorizadas

---

## Versões suportadas [#versões-suportadas]

| Versão | Suporte de segurança |
|---|---|
| 3.0.x (Horizon) | ✅ Ativo |


# Suporte

URL: /pt/docs/developer/reference/support

Canais disponíveis para obter ajuda com o IGRP.

# Suporte [#suporte]

## Como obter ajuda [#como-obter-ajuda]

Antes de pedir ajuda, verifica se o problema já tem resposta:

1. Lê a [documentação](/docs/developer/introducao#como-começar)
2. Pesquisa nas issues do repositório do módulo no GitHub — ver [Código-fonte](./codigo-fonte.mdx)
3. Verifica o [Guia de contribuição](./contributing.mdx) para saber como reportar bugs

---

## Canais de suporte [#canais-de-suporte]

| Canal | Uso | Contacto |
|---|---|---|
| **E-mail** | Questões gerais, suporte técnico, bugs e pedidos | igrp@nosi.cv |
| **Discord** | Dúvidas rápidas, bugs e conversa com a equipa e a comunidade | [discord.gg/dywFBFaCQr](https://discord.gg/dywFBFaCQr) |
| **GitHub Issues** | Bugs e pedidos de funcionalidade, no repositório do módulo | [github.com/NOSiCode-CV](https://github.com/NOSiCode-CV) — ver [Código-fonte](./codigo-fonte.mdx) |
| **Segurança** | Vulnerabilidades (nunca em issues públicas nem no Discord) | igrp@nosi.cv |

---

## O que não fazer [#o-que-não-fazer]

- Não partilhes credenciais, tokens ou segredos em issues públicas nem no Discord
- Não uses issues para questões de suporte geral — usa o Discord ou o e-mail
- Não abras issues duplicadas — pesquisa antes de criar


# Diagrama ER

URL: /pt/docs/developer/studio/backend/diagrama-er

Relações entre esquemas (entidades JPA) no designer de APIs do iGRP Studio — tipos de relação, configuração, código gerado e o Diagrama ERD.

As relações entre entidades definem-se no **editor do esquema**, num campo do tipo **Relação**. O Studio gera as anotações JPA (`@OneToOne`, `@OneToMany`, `@ManyToOne`, `@ManyToMany`) e mostra o resultado no **Diagrama ERD**.

<StudioPreview imageName="diagramaER" alt="Diagrama ERD com as entidades Pedido e TipoPedido ligadas por uma relação N:1" />

<Callout type="info" title="O diagrama é só de leitura">
  Abre-o pelo **+** do nó **Esquemas** na árvore → **Diagrama ERD**. Serve para ver as entidades e as relações; para criar, mudar ou remover uma relação usa o editor do esquema.
</Callout>

## Criar uma relação [#criar-uma-relação]

O exemplo liga cada `Pedido` a um `TipoPedido` (muitos pedidos para um tipo).

<Steps>
  <Step>Cria primeiro a entidade do outro lado (`TipoPedido`), para a poderes referenciar.</Step>
  <Step>No esquema `Pedido`, junta um campo (`tipo`) e, no botão do tipo, escolhe **Relação**.</Step>
  <Step>Clica em **Definir Relação** e escolhe o tipo de relação nos quatro ícones — o nome aparece ao passar o rato: **OneToOne**, **OneToMany**, **ManyToOne**, **ManyToMany**.</Step>
  <Step>Escolhe a **Entidade** (`TipoPedido`) e a **Coluna de Referência** (`id`).</Step>
  <Step>Para uma relação **bidirecional**, indica o **Nome do Campo** no outro lado (`pedidos`) — o Studio gera também esse lado.</Step>
  <Step>**Aplicar** e grava o esquema. O campo passa a mostrar `ManyToOne com TipoPedido.id`.</Step>
</Steps>

<StudioPreview imageName="relacaoConfig" alt="Configurações de Relação do campo tipo, com ManyToOne selecionado, entidade TipoPedido, coluna id e relação bidirecional" />

No separador **Outros** do mesmo painel ficam o **Tipo de Busca** (`lazy` por defeito), a **Remoção de Órfãos** e o **Tipo de Cascata**.

<Callout type="warning" title="A relação define-se no lado proprietário">
  Cria a relação no campo da entidade que guarda a chave estrangeira (o lado proprietário — `Pedido.tipo` no exemplo). O lado inverso é gerado a partir de **Nome do Campo** quando a relação é bidirecional.
</Callout>

## Tipos de relação [#tipos-de-relação]

| Tipo | Exemplo | Lado proprietário |
|---|---|---|
| **OneToOne** (1:1) | `País ↔ Presidente` — um país tem um presidente | Quem tem a chave (`Presidente.pais`) |
| **OneToMany** (1:N) | `Estado → Cidades` — um estado tem várias cidades | Do lado N (`Cidade.estado`); o lado 1 usa `mappedBy` |
| **ManyToOne** (N:1) | `Pedidos → TipoPedido` — vários pedidos do mesmo tipo | O lado N (`Pedido.tipo`) |
| **ManyToMany** (N:M) | `Alunos ↔ Cursos` — um aluno em vários cursos e vice-versa | Quem define a tabela de junção (`Aluno.cursos`) |

<Accordions>
  <Accordion title="Código gerado — OneToOne">
    ```java
    // Presidente.java (proprietário)
    @OneToOne
    @JoinColumn(name = "pais_id")
    private Pais pais;

    // Pais.java (inverso)
    @OneToOne(mappedBy = "pais")
    private Presidente presidente;
    ```
  </Accordion>
  <Accordion title="Código gerado — OneToMany / ManyToOne">
    ```java
    // Cidade.java (lado N, proprietário)
    @ManyToOne
    @JoinColumn(name = "estado_id")
    private Estado estado;

    // Estado.java (lado 1, inverso)
    @OneToMany(mappedBy = "estado", cascade = CascadeType.ALL)
    private List<Cidade> cidades = new ArrayList<>();
    ```
  </Accordion>
  <Accordion title="Código gerado — ManyToMany">
    ```java
    // Aluno.java (proprietário)
    @ManyToMany
    @JoinTable(
        name = "aluno_curso",
        joinColumns = @JoinColumn(name = "aluno_id"),
        inverseJoinColumns = @JoinColumn(name = "curso_id")
    )
    private List<Curso> cursos = new ArrayList<>();

    // Curso.java (inverso)
    @ManyToMany(mappedBy = "cursos")
    private List<Aluno> alunos = new ArrayList<>();
    ```
  </Accordion>
</Accordions>

## Remover uma relação [#remover-uma-relação]

No campo da relação, abre **Definir Relação** e clica em **Remover Relação**; depois grava o esquema.

<StudioPreview imageName="removerRelacaoOpcao1" alt="Painel Configurações de Relação com o botão Remover Relação realçado" />

<Callout type="error" title="Corrigir uma relação errada">
  Se escolheste o tipo ou a entidade errados, remove a relação, grava, e cria-a de novo com os valores certos — não alteres só o tipo de uma relação já gerada.
</Callout>


# Model Designer

URL: /pt/docs/developer/studio/backend/model-designer

O designer de APIs de um projeto Spring Boot no iGRP Studio — módulos, esquemas (entidades), contracts (DTOs) e endpoints, e o código que o Studio gera a partir deles.

Num projeto Spring Boot, o Studio abre o **designer de APIs**: constróis a aplicação de forma visual — módulos por domínio, **esquemas** (entidades JPA), **contracts** (DTOs com validações) e **endpoints** REST — e o Studio gera o código correspondente.

<Grid cols={3}>
  <Feature icon="Package" color="blue" eyebrow="1" title="Módulo" />
  <Feature icon="Database" color="blue" eyebrow="2" title="Esquemas" />
  <Feature icon="FileText" color="blue" eyebrow="3" title="Contracts" />
  <Feature icon="Activity" color="blue" eyebrow="4" title="Endpoints" />
  <Feature icon="Check" color="green" eyebrow="✓" title="Código gerado" />
</Grid>

## A árvore de APIs [#a-árvore-de-apis]

À esquerda, o painel **APIs** mostra os módulos do projeto. Cada módulo tem **Endpoints**, **Contract**, **Esquemas** e **GraphQL**; **Componentes** é o módulo partilhado (`shared`).

Para criar, passa o rato sobre um nó e usa o **+** que aparece à direita — por exemplo, no nó **Esquemas**: **Novo Schema**, **Importar tabela de base de dados**, **Importar** (ficheiros JSON Schema) e **Diagrama ERD**. O **+** da barra de separadores abre também um separador **New…** com **Novo Esquema** (`⌘M`), **Novo Endpoint** (`⌘E`) e **Novo Contract** (`⌘O`).

<StudioPreview imageName="criarEndpoint" alt="Árvore de APIs com o módulo pedidos, o endpoint POST criarPedido e o módulo partilhado Componentes" />

<Callout type="warning" title="Esquemas e contracts criados pelo separador New… vão para o módulo partilhado">
  O editor não pergunta o módulo: o que crias a partir do **+** da barra de separadores fica em **Componentes** (`shared`). Para criar dentro de um módulo, usa o **+** do nó desse módulo na árvore.
</Callout>

## 1. Criar um módulo [#1-criar-um-módulo]

Organiza a aplicação em módulos por domínio. O **+** no topo do painel **APIs** abre **Criar novo módulo**: dá-lhe um nome (por exemplo `pedidos`) e **Guardar módulo**.

<StudioPreview imageName="formCriarModulo" alt="Diálogo Criar novo módulo com o nome pedidos" />

## 2. Esquemas [#2-esquemas]

Os esquemas são as entidades do domínio, mapeadas para tabelas da base de dados.

<StudioPreview imageName="criarSchema" alt="Editor do esquema Pedido com os campos id, requerente, data e estado" />

| Campo | |
|---|---|
| **Nome** / **Nome da Tabela** | Nome da entidade (`Pedido`) e da tabela (`t_pedido`, preenchido a partir do nome) |
| **Auditoria** | Regista automaticamente as datas de criação e alteração |
| **Gerar APIs CRUD** | Gera os endpoints CRUD do esquema |
| **OData** | Em breve |

Os separadores **Campos**, **Índices** e **Restrições Únicas** definem a estrutura. Cada campo tem nome e tipo — **Tipo de dados** (agrupados em Timing, Numeric, Text, Media, Json, Misc, Generic, Uri), **Enum** ou **Relação** (ver [Diagrama ER](./diagrama-er.mdx)). O botão ao lado do tipo abre as propriedades:

<StudioPreview imageName="propriedadeSchema" alt="Propriedades do campo requerente com Obrigatório ativo e comprimento 150" />

<Checklist cols={2}>
  <Item title="Chave Primária">Identifica o campo como chave primária (o `id` já vem marcado)</Item>
  <Item title="Obrigatório">Campo `NOT NULL`</Item>
  <Item title="Único">Valor único na tabela</Item>
  <Item title="Comprimento">Comprimento máximo</Item>
  <Item title="Valor Padrão">Valor predefinido</Item>
</Checklist>

## 3. Contracts [#3-contracts]

Os **contracts** (DTOs) transportam dados entre as camadas — por exemplo o corpo de um pedido à API.

<StudioPreview imageName="criarDTO" alt="Editor do Contract PedidoRequest com os atributos requerente e tipo" />

| Campo | |
|---|---|
| **Kind** | Data Transfer Object |
| **Nome** | Nome do contract (`PedidoRequest`) |
| **Template** | Default |
| **extends** | Contract a estender (opcional) |
| **Ativar Validação Personalizada** / **Somente Leitura** | Validação própria do contract / contract só de leitura |

Cada atributo tem validações próprias, no botão ao lado do tipo:

<StudioPreview imageName="formCriarDTO" alt="Validações do atributo requerente com Obrigatório e tamanho máximo 150" />

<Checklist cols={2}>
  <Item title="Obrigatório">Campo obrigatório</Item>
  <Item title="Antes / Depois">Datas no passado ou no futuro</Item>
  <Item title="Email / URL">Formato de email ou URL</Item>
  <Item title="Tamanho Mínimo / Máximo">Limites de comprimento</Item>
  <Item title="Regex">Expressão regular</Item>
</Checklist>

## 4. Endpoints [#4-endpoints]

Um endpoint agrupa **ações** (operações HTTP). **Novo Endpoint** abre o editor de uma ação:

<Steps>
  <Step>Escolhe o **Método** (GET, POST, PUT, PATCH, DELETE…), o **path** da ação (`pedidos`) e o **Nome da Ação** (`criarPedido`).</Step>
  <Step>Em **Request**, define **Parâmetros**, **Corpo** e **Cabeçalhos**. Num corpo **JSON**, **Set Type → Reference other Object** referencia um contract (`PedidoRequest`).</Step>
  <Step>Em **Response**, define a resposta; em **Security**, as permissões e funções (ver abaixo).</Step>
  <Step>**Salvar**. Na primeira ação, o Studio pede os dados do endpoint.</Step>
</Steps>

<StudioPreview imageName="configValidacaoEndpoint" alt="Ação POST pedidos com o corpo JSON a referenciar o contract PedidoRequest" />

O diálogo **Criar Novo Endpoint** pede o **Nome do Endpoint** (`Pedidos`, sem espaços nem hífenes, até 20 caracteres), o **Caminho Base**, o **Módulo** e a **Descrição**.

<StudioPreview imageName="novoEndpoint" alt="Diálogo Criar Novo Endpoint com o nome Pedidos, caminho base api/v1, módulo pedidos e descrição" />

<Callout type="warning" title="Caminho Base sem barra inicial">
  Escreve `api/v1`, não `/api/v1`. Com a barra, a gravação falha (*basePath: must match pattern*) e a ação fica bloqueada até fechares o separador.
</Callout>

### Security [#security]

O separador **Security** de uma ação define quem lhe pode aceder:

- **Permissões** — escreve o nome (por exemplo `pedidos.criar`) e **Adicionar**. O **Operador de Permissões** decide se é preciso **qualquer** (OR) ou **todas** (AND).
- **Funções** — funções exigidas (por exemplo `OPERADOR`); basta o utilizador ter uma.

O Studio gera as anotações de permissão no controlador — ver [Permissões no backend](/docs/developer/framework/backend/permissoes).

## Código gerado [#código-gerado]

Ao gravar, o Studio gera o código do projeto Spring Boot. No estilo **Technical** (o predefinido):

<Grid cols={3}>
  <Feature icon="Database" color="blue" title="Entidades JPA">Uma por esquema, com auditoria opcional</Feature>
  <Feature icon="Archive" color="blue" title="Repositórios">Spring Data JPA; expostos em REST com **Gerar APIs CRUD**</Feature>
  <Feature icon="FileText" color="blue" title="DTOs">A partir dos contracts, com as validações</Feature>
  <Feature icon="Route" color="blue" title="Controladores">A partir dos endpoints, com as permissões do separador Security</Feature>
  <Feature icon="Workflow" color="green" title="Serviços">Interface gerada; a implementação é tua</Feature>
</Grid>

Onde fica cada ficheiro, e o que podes editar, está em [Projeto Spring Boot](/docs/developer/framework/backend/springboot).

---

## Módulo partilhado [#módulo-partilhado]

O projeto inclui o módulo **Componentes** (`shared`), criado automaticamente, para o que é comum a vários domínios — esquemas, contracts, utilitários, constantes e exceções.

<StudioPreview imageName="moduloShared" alt="Módulo Componentes na árvore de APIs com o contract PedidoRequest e os esquemas Pedido e TipoPedido" />

## Eliminar recursos [#eliminar-recursos]

Um esquema, contract ou ação abertos têm o botão de **eliminar** (lixo) ao lado de **Salvar**.

<StudioPreview imageName="eliminarRecurso" alt="Botão de eliminar realçado no cabeçalho do editor de um contract" />

<Callout type="warning" title="Verifica as dependências">
  Antes de eliminar, confirma que nada depende do recurso — um contract usado no corpo de uma ação, uma entidade referenciada numa relação.
</Callout>


# Run Preview

URL: /pt/docs/developer/studio/backend/run-preview

Aprenda a executar e pré-visualizar a aplicação Spring Boot, configurar o banco de dados, testar endpoints e operações CRUD.

# Run Preview [#run-preview]

O processo de Execução e Pré-visualização seguem um fluxo definido, garantindo a correta geração de componentes essenciais antes do arranque da aplicação.

## Objetivo [#objetivo]

<Callout type="objective" title="Objetivo">
  Executar a aplicação Spring Boot e pré-visualizar as funcionalidades implementadas, incluindo endpoints de controladores personalizados e operações CRUD automáticas.
</Callout>

<Grid cols={2}>
  <Feature icon="FlaskConical" color="blue" title="Funcionalidades Testadas">
    - Conectividade com banco de dados
    - Endpoints de controladores
    - Operações CRUD automáticas
    - Documentação da API
  </Feature>
  <Feature icon="ClipboardCheck" color="green" title="Validações Realizadas">
    - Inicialização da aplicação
    - Acesso aos endpoints
    - Resposta das APIs
    - Monitoramento da aplicação
  </Feature>
</Grid>

---

## Fluxo de Execução [#fluxo-de-execução]

<Grid cols={2}>
  <Feature icon="Database" color="blue" eyebrow="Passo 1" title="Configuração do Banco de Dados">
    Configurar conexão com o banco de dados no application.properties
  </Feature>
  <Feature icon="Play" color="green" eyebrow="Passo 2" title="Iniciar a Aplicação">
    Executar a aplicação Spring Boot
  </Feature>
  <Feature icon="Globe" color="orange" eyebrow="Passo 3" title="Pré-visualização - Controladores">
    Aceder aos endpoints definidos nos controladores
  </Feature>
  <Feature icon="Code" color="purple" eyebrow="Passo 4" title="Pré-visualização - CRUD">
    Testar os métodos CRUD gerados automaticamente
  </Feature>
</Grid>

---

### Configuração do Banco de Dados [#configuração-do-banco-de-dados]

**Editar application.properties**

Configure a conexão com o banco de dados no arquivo application.properties:

```properties
# Database Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/minha_db
spring.datasource.username=postgres
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
```

<Callout type="info" title="Configuração Opcional">
  Se não desejar usar um banco de dados, comente ou remova a dependência spring-boot-starter-data-jpa no arquivo pom.xml.
</Callout>

**Variáveis de Ambiente (.env)**

Alternativamente, use variáveis de ambiente para maior segurança:

```
DB_HOST=localhost
DB_PORT=5432
DB_NAME=minha_db
DB_USERNAME=postgres
DB_PASSWORD=password
```

### Iniciar a Aplicação [#iniciar-a-aplicação]

**Executar Spring Boot**

Execute a aplicação Spring Boot usando um dos comandos abaixo:

<Grid cols={2}>
  <Feature icon="Terminal" color="blue" title="Via Maven">
    `mvn spring-boot:run`
  </Feature>
  <Feature icon="MonitorPlay" color="blue" title="Via IDE">
    `Run main class`
  </Feature>
</Grid>

**Verificar Inicialização**

A aplicação estará disponível na URL base:

<Checklist>
  <Item status="ok" title={<a href="http://localhost:8080" target="_blank" rel="noopener noreferrer">http://localhost:8080</a>}>
    URL base da aplicação
  </Item>
</Checklist>

<Callout type="success" title="Log de Inicialização">
  Verifique no console se a aplicação iniciou corretamente. Você deve ver mensagens indicando que o Tomcat está rodando na porta 8080 e que a aplicação foi iniciada com sucesso.
</Callout>

### Pré-visualização - Controladores [#pré-visualização---controladores]

**Acesso aos Endpoints**

Acesse os endpoints definidos nos controladores. Exemplo:

<Checklist>
  <Item status="warn" title={<a href="http://localhost:8080/greetings/hello" target="_blank" rel="noopener noreferrer">http://localhost:8080/greetings/hello</a>}>
    Endpoint de exemplo do controlador
  </Item>
</Checklist>

<Callout type="warning" title="Implementação Necessária">
  NOTA: Para aceder aos endpoints gerados, é necessário implementar os métodos declarados na interface do controlador. Esta implementação é realizada pelo programador.
</Callout>

**Padrão de URL dos Controladores**

Uma vez implementados os métodos declarados, os endpoints podem ser acedidos da seguinte forma:

```
http://<HOST>:<PORT>/<CONTROLLER_PATH>/<ACTION_PATH>
```

### Pré-visualização - CRUD [#pré-visualização---crud]

**Acesso aos Métodos CRUD**

Para acessar os métodos CRUD, utilize a URL base do modelo. Exemplo:

<Checklist>
  <Item status="info" title={<a href="http://localhost:8080/pessoas" target="_blank" rel="noopener noreferrer">http://localhost:8080/pessoas</a>}>
    URL base para CRUD do modelo Pessoa
  </Item>
</Checklist>

<Callout type="info" title="CRUD Automático">
  IMPORTANTE: Quando o CRUD é gerado, para aceder aos métodos (GET, POST, DELETE, PUT) não é necessário implementar uma classe de controlador, acedemos simplesmente a: `HOST:PORT/CRUD_PATH`.
</Callout>

**Métodos HTTP Disponíveis**

| Método | Descrição |
|---|---|
| <Badge color="green">GET</Badge> | Obter dados |
| <Badge color="blue">POST</Badge> | Criar novos registros |
| <Badge color="amber">PUT</Badge> | Atualizar registros existentes |
| <Badge color="red">DELETE</Badge> | Remover registros |

**Exemplos de Uso CRUD**

```http
GET    /pessoas    - Listar todas as pessoas
GET    /pessoas/1  - Obter pessoa com ID 1
POST   /pessoas    - Criar nova pessoa
PUT    /pessoas/1  - Atualizar pessoa com ID 1
DELETE /pessoas/1  - Excluir pessoa com ID 1
```

## URLs de Pré-visualização [#urls-de-pré-visualização]

Links úteis para testar e monitorar a aplicação:

<Grid cols={2}>
  <Feature icon="ExternalLink" color="blue" title="Controller Endpoint" href="http://localhost:8080/greetings/hello">
    `http://localhost:8080/greetings/hello` — Exemplo de endpoint de controlador personalizado
  </Feature>
  <Feature icon="ExternalLink" color="blue" title="CRUD Base" href="http://localhost:8080/pessoas">
    `http://localhost:8080/pessoas` — URL base para operações CRUD do modelo Pessoa
  </Feature>
  <Feature icon="ExternalLink" color="blue" title="API Documentation" href="http://localhost:8080/swagger-ui.html">
    `http://localhost:8080/swagger-ui.html` — Documentação automática da API (Swagger)
  </Feature>
  <Feature icon="ExternalLink" color="blue" title="Actuator Health" href="http://localhost:8080/actuator/health">
    `http://localhost:8080/actuator/health` — Endpoint de monitoramento da aplicação
  </Feature>
</Grid>

---

## Ferramentas Recomendadas para Testes [#ferramentas-recomendadas-para-testes]

<Grid cols={3}>
  <Feature icon="Send" color="orange" title="Postman">Cliente REST para testar APIs</Feature>
  <Feature icon="Moon" color="purple" title="Insomnia">Cliente HTTP para testes de API</Feature>
  <Feature icon="Terminal" color="neutral" title="cURL">Ferramenta de linha de comando</Feature>
  <Feature icon="BookOpen" color="green" title="Swagger UI">Interface web automática</Feature>
  <Feature icon="Chrome" color="blue" title="Browser DevTools">Ferramentas do navegador</Feature>
  <Feature icon="Zap" color="amber" title="HTTPie">Cliente HTTP moderno</Feature>
</Grid>


# Testing

URL: /pt/docs/developer/studio/backend/testing

Aprenda a testar projetos Spring Boot no IGRP Studio com testes unitários, de integração e funcionais para modelos, operações CRUD, relacionamentos e controladores.

# Testing [#testing]

O processo de Execução e Pré-visualização seguem um fluxo definido, garantindo a correta geração de componentes essenciais antes do arranque da aplicação.

<Callout type="objective" title="Objetivo">
  Garantir que o sistema funciona corretamente em várias circunstâncias e cumpre os requisitos estabelecidos através de testes automatizados que verificam a criação de APIs, modelos, controladores e funcionalidades CRUD.
</Callout>

<Grid cols={2}>
  <Feature icon="ListChecks" color="blue" title="Tipos de Teste">
    - Testes de criação de modelos
    - Testes de operações CRUD
    - Testes de relacionamentos
    - Testes de controladores
  </Feature>
  <Feature icon="Award" color="green" title="Benefícios">
    - Garantia de qualidade
    - Detecção precoce de erros
    - Documentação viva
    - Refatoração segura
  </Feature>
</Grid>

---

## Princípios de Teste [#princípios-de-teste]

<Grid cols={3}>
  <Feature icon="Target" color="blue" title="Testes Unitários">
    Testam componentes isolados

    **Exemplos:** <Badge>Services</Badge> <Badge>Repositories</Badge> <Badge>Entities</Badge> <Badge>DTOs</Badge>
  </Feature>
  <Feature icon="Shield" color="blue" title="Testes de Integração">
    Testam interação entre componentes

    **Exemplos:** <Badge>Controllers</Badge> <Badge>Database</Badge> <Badge>External APIs</Badge> <Badge>Full flow</Badge>
  </Feature>
  <Feature icon="Zap" color="blue" title="Testes Funcionais">
    Testam funcionalidades completas

    **Exemplos:** <Badge>CRUD operations</Badge> <Badge>Business rules</Badge> <Badge>Validations</Badge> <Badge>Workflows</Badge>
  </Feature>
</Grid>

---

## Tipos de Testes Implementados [#tipos-de-testes-implementados]

<Grid cols={2}>
  <Feature icon="Database" color="blue" eyebrow="Tipo 1" title="Testes de Modelo">
    Verificar criação correta de modelos a partir de configurações

    **Cenários Testados:** Geração de modelo · Mapeamento JPA · Validação de atributos · Configurações de tabela
  </Feature>
  <Feature icon="Code" color="green" eyebrow="Tipo 2" title="Testes CRUD">
    Garantir funcionamento das operações CRUD automáticas

    **Cenários Testados:** Create operations · Read operations · Update operations · Delete operations
  </Feature>
  <Feature icon="Target" color="orange" eyebrow="Tipo 3" title="Testes de Relacionamentos">
    Validar relacionamentos entre modelos existentes

    **Cenários Testados:** OneToOne · OneToMany · ManyToOne · ManyToMany
  </Feature>
  <Feature icon="Globe" color="purple" eyebrow="Tipo 4" title="Testes de Controladores">
    Verificar geração e funcionamento dos controladores

    **Cenários Testados:** Endpoint generation · Service interface · Configuration files · Response validation
  </Feature>
</Grid>

---

## Cenários de Teste Detalhados [#cenários-de-teste-detalhados]

### Criar um Novo Modelo [#criar-um-novo-modelo]

**Processo de Teste**

<Steps>
  <Step>Fornecido um ficheiro de configuração de modelo (ModelConfig)</Step>
  <Step>A função de geração de modelos é executada</Step>
  <Step>É validado que o ficheiro do modelo é corretamente gerado</Step>
</Steps>

<Callout type="success" title="Resultado Esperado">
  O modelo é criado com sucesso na API, e todos os atributos especificados são refletidos no ficheiro gerado.
</Callout>

### Adicionar Operações CRUD [#adicionar-operações-crud]

**Processo de Teste**

<Steps>
  <Step>É utilizado um modelo previamente gerado como base</Step>
  <Step>A funcionalidade para adicionar operações CRUD é executada</Step>
  <Step>É validado que o repositório inclui os métodos CRUD solicitados</Step>
</Steps>

<Callout type="success" title="Resultado Esperado">
  Um repositório que estende CrudRepository é criado, contendo todos os métodos CRUD necessários.
</Callout>

### Adicionar Relações Entre Modelos [#adicionar-relações-entre-modelos]

**Processo de Teste**

<Steps>
  <Step>São criados vários modelos que representam diferentes entidades</Step>
  <Step>A configuração é atualizada para incluir relações</Step>
  <Step>O modelo é gerado novamente com as relações incluídas</Step>
</Steps>

<Callout type="success" title="Resultado Esperado">
  As relações entre modelos são configuradas e refletidas apropriadamente no código gerado.
</Callout>

### Testes de Controladores [#testes-de-controladores]

Verificam se os controladores estão gerando os endpoints corretamente e se as respostas estão de acordo como esperado.

<Grid cols={3}>
  <Feature icon="FileCode" color="blue" title="Criação de Controlador">
    **Objetivo:** Testar que o controlador é gerado corretamente a partir do contexto fornecido e que o ficheiro resultante é guardado no local esperado.
  </Feature>
  <Feature icon="Layers" color="blue" title="Interface de Serviço">
    **Objetivo:** Verificar que a interface de serviço é criada corretamente com os métodos declarados e é guardada no diretório especificado.
  </Feature>
  <Feature icon="Settings" color="blue" title="Ficheiro de Configuração">
    **Objetivo:** Garantir que o ficheiro de configuração do controlador é guardado no diretório `.studio/controllers`, validando que a configuração está correta.
  </Feature>
</Grid>

---

## Estrutura de Testes [#estrutura-de-testes]

**Organização dos Testes**

<Files>
  <Folder name="src/test/java/com/igrp" defaultOpen>
    <Folder name="model" defaultOpen>
      <File name="ModelGenerationTest.java" />
      <File name="EntityValidationTest.java" />
      <File name="RepositoryTest.java" />
    </Folder>
    <Folder name="controller" defaultOpen>
      <File name="ControllerGenerationTest.java" />
      <File name="EndpointTest.java" />
      <File name="ResponseValidationTest.java" />
    </Folder>
    <Folder name="service" defaultOpen>
      <File name="CrudOperationsTest.java" />
      <File name="BusinessLogicTest.java" />
      <File name="IntegrationTest.java" />
    </Folder>
    <Folder name="relationship" defaultOpen>
      <File name="OneToOneTest.java" />
      <File name="OneToManyTest.java" />
      <File name="ManyToOneTest.java" />
      <File name="ManyToManyTest.java" />
    </Folder>
  </Folder>
</Files>

---

## Ferramentas e Frameworks de Teste [#ferramentas-e-frameworks-de-teste]

<Grid cols={3}>
  <Feature icon="FlaskConical" color="green" title="JUnit 5">Framework principal para testes unitários</Feature>
  <Feature icon="Copy" color="green" title="Mockito">Framework para criação de mocks</Feature>
  <Feature icon="Leaf" color="green" title="Spring Boot Test">Testes de integração Spring</Feature>
  <Feature icon="Container" color="green" title="TestContainers">Testes com banco de dados real</Feature>
  <Feature icon="CheckCircle" color="green" title="AssertJ">Biblioteca de assertions fluentes</Feature>
  <Feature icon="Network" color="green" title="WireMock">Mock de serviços externos</Feature>
</Grid>

---

## Exemplo de Teste Unitário [#exemplo-de-teste-unitário]

**Teste de Criação de Modelo**

```java
@ExtendWith(MockitoExtension.class)
class ModelGenerationTest {
  @Mock
  private ModelConfigRepository configRepository;
  @InjectMocks
  private ModelGenerationService generationService;
  @Test
  void deveGerarModeloComSucesso() {
    // Arrange
    ModelConfig config = new ModelConfig("Pessoa");
    when(configRepository.save(any())).thenReturn(config);
    // Act
    Model result = generationService.generateModel(config);
    // Assert
    assertThat(result.getName()).isEqualTo("Pessoa");
    verify(configRepository).save(config);
  }
}
```

<Callout type="tip" title="Boas Práticas de Teste">
  - **AAA Pattern:** Organize testes com Arrange, Act, Assert
  - **Nomes Descritivos:** Use nomes que descrevam o comportamento testado
  - **Testes Independentes:** Cada teste deve ser independente dos outros
  - **Cobertura:** Mantenha uma boa cobertura de código (\>80%)
  - **Testes Rápidos:** Testes unitários devem ser rápidos de executar
  - **Dados de Teste:** Use dados realistas mas não sensíveis
</Callout>

## Execução dos Testes [#execução-dos-testes]

<Tabs>
  <TabItem value="maven" label="Via Maven">
    ```bash
    # Executar todos os testes
    mvn test
    # Executar com relatório de cobertura
    mvn test jacoco:report
    # Executar testes específicos
    mvn test -Dtest=ModelGenerationTest
    ```
  </TabItem>
  <TabItem value="ide" label="Via IDE">
    - Executar classe de teste individual
    - Executar método de teste específico
    - Executar todos os testes do projeto
    - Debug de testes com breakpoints
    - Visualizar cobertura de código
    - Relatórios de execução detalhados
  </TabItem>
</Tabs>


# Bundles

URL: /pt/docs/developer/studio/cli/bundles

Instalar, atualizar e remover bundles — pacotes declarativos que juntam dependências npm, páginas, metadata do Studio e templates num projeto iGRP.

# `igrp bundle` — instalar bundles num projeto [#igrp-bundle--instalar-bundles-num-projeto]

Um **bundle** é um pacote declarativo que se aplica a um projeto aberto: dependências npm, ficheiros copiados (páginas, layouts), metadata do Studio (`.igrpstudio/…`), skills e migrações. O mesmo catálogo aparece no separador **Bundles** do projeto no Studio IDE; o comando `igrp bundle` (alias `b`) faz o mesmo no terminal ou em CI.

Exemplos do catálogo atual:

| Bundle | Tipo | O que faz |
| --- | --- | --- |
| `process-management-ui` | `platform-ui` | Páginas de runtime dos processos BPMN e pacotes `@igrp/platform-process-management-*` — ver [Formulários BPMN](/docs/developer/process/forms#instalar-o-runtime-de-processos) |
| `form-crud-template` | `form-template` | Formulário CRUD (id, nome, descrição, ativo) num módulo, parametrizado por nome, rótulo e recurso |
| `template-migrator` | `migration-pack` | Aplica as migrações pendentes do template Next.js |

<Callout type="info" title="Versão">
`igrp bundle` existe na `@igrp/cli` 1.7.6 e a partir da **1.7.8** (a 1.7.7 não o inclui). Atualiza com `npm i -g @igrp/cli@latest --registry=https://sonatype.nosi.cv/repository/npm-group/`.
</Callout>

---

## Subcomandos [#subcomandos]

```bash
igrp bundle search [query]   [--framework <fw>] [--kind <k>] [--json] [--force]
igrp bundle info <bundle>    [--json]
igrp bundle add <bundle>     [--project <path>] [--version <semver>] [-p key=valor …] [--dry-run] [--force] [--skip-existing]
igrp bundle list             [--project <path>] [--json]
igrp bundle upgrade <bundle> [--project <path>] [--to <semver>] [-p key=valor …] [--dry-run]
igrp bundle remove <bundle>  [--project <path>] --yes
igrp bundle doctor           [--project <path>] [--json]
igrp bundle adopt <bundle>   [--project <path>] [--version <semver>] [--force]
```

`<bundle>` é um nome do catálogo (`nome` ou `nome@versão`) ou um caminho local para a pasta de um bundle (também `--from <path>`).

| Flag | Onde | Efeito |
| --- | --- | --- |
| `--project <path>` | `add`, `list`, `upgrade`, `remove`, `doctor`, `adopt` | Raiz do projeto. Default: diretório atual. |
| `-p, --param <key=valor>` | `add`, `upgrade` | Parâmetro do bundle (repetível). No `upgrade`, os guardados na instalação são o default. |
| `--dry-run` | `add`, `upgrade`, `remove` | Mostra o plano (`[planned] …`) sem escrever. |
| `--force` | `add` | Sobrepõe uma instalação existente. |
| `--skip-existing` | `add` | Salta ficheiros cujo destino já existe (completa instalações parciais). |
| `--yes` | `remove` | Confirma; sem ela o `remove` só mostra o plano. |
| `--to <semver>` | `upgrade` | Versão alvo; default a mais recente. |
| `--framework <fw>` · `--kind <k>` | `search` | Filtros (`nextjs`, `spring`, `dotnet`, `agnostic`; `platform-ui`, `form-template`, …). |
| `--source <url>` | `add`, `search`, `upgrade`, `adopt` | Catálogo. Default: env `IGRP_BUNDLE_REGISTRY`, senão `https://sonatype.nosi.cv/repository/igrp-templates/@igrp/bundles/`. |
| `--json` · `--quiet` | vários | Saída para scripts · só erros. |

---

## Exemplo [#exemplo]

```bash
# O que existe no catálogo
igrp bundle search
# form-crud-template      0.1.0   form-template    nextjs   Form CRUD (Template)
# process-management-ui   0.1.0   platform-ui      nextjs   Process Management UI
# template-migrator       0.1.0   migration-pack   nextjs   Template Migrator

# Planear um form CRUD no módulo registos (sem escrever)
igrp bundle add form-crud-template --project . --dry-run \
  -p name=cliente -p label=Cliente -p module=registos -p resource=clientes
# [planned] metadata    + .igrpstudio/registos/forms/cliente.json
# [planned] igrpstudio  + .igrpstudio/features.json → form-crud-template

# Aplicar e verificar
igrp bundle add form-crud-template --project . -p name=cliente -p label=Cliente -p module=registos -p resource=clientes
igrp bundle list
igrp bundle doctor
```

Sem os parâmetros obrigatórios, `add` diz quais faltam (`Faltam params obrigatórios: label, module, resource.`) e termina com exit `1`.

---

## Como funciona [#como-funciona]

Cada bundle tem um `bundle.json` com a receita de instalação, aplicada sempre por esta ordem: **npm → copy → metadata → skill → migrator → igrpstudio**. Todos os destinos têm de ficar dentro do projeto (sem caminhos absolutos nem `..`).

O que foi instalado fica em `.igrpstudio/bundles.json` (versão, parâmetros e o SHA-256 de cada ficheiro escrito). É isso que permite:

- `list` — mostrar o que está instalado e por quem (CLI ou Studio IDE);
- `doctor` — detetar ficheiros alterados desde a instalação (*drift*);
- `upgrade` e `remove` — refazer ou desfazer a receita;
- `adopt` — registar uma instalação feita à mão, sem reaplicar.

O catálogo é guardado em cache em `~/.igrp/` (`bundles-index-cache.json` e `bundles-cache/`); `search --force` ignora a cache.

<Callout type="warning" title="pnpm 11">
Os passos `npm` usam `pnpm add`. Com pnpm 11, dependências com scripts de build por aprovar fazem o `pnpm add` terminar com erro e o passo falha. Declara-as em `allowBuilds` no `pnpm-workspace.yaml` do projeto antes de instalar.
</Callout>

---

## Códigos de saída [#códigos-de-saída]

| Código | Significado |
| --- | --- |
| `0` | Sucesso (inclui dry-run). |
| `1` | Validação — parâmetros em falta, bundle inexistente no catálogo, manifesto inválido. |
| `2` | Catálogo indisponível, falha de download ou de um passo da receita. |


# Gerar artefactos

URL: /pt/docs/developer/studio/cli/generate

Gerar páginas, componentes e passos de processo com o mesmo pipeline que o IGRP Studio usa no Page Builder.

# `igrp generate` — gerar artefactos a partir de manifestos JSON [#igrp-generate--gerar-artefactos-a-partir-de-manifestos-json]

`igrp generate` (alias `igrp gen`) invoca o **mesmo pipeline de geração** que o IGRP Studio dispara quando gravas no Page Builder — a CLI delega no pacote `@igrp/igrp-studio-nextjs-engine`. É útil para reuso interno do Studio (a partir de um protótipo de especificações) e para pipelines externos — CI, agentes de IA, demos — que partem de um manifesto JSON em vez de passar pela UI.

```bash
igrp generate page         --manifest page.json --project ./my-app
igrp generate component    --manifest component.json --project ./my-app
igrp generate process-step --manifest step.json --project ./my-app
```

---

## Flags comuns [#flags-comuns]

| Flag | Efeito |
| --- | --- |
| `--manifest <path>` | Ficheiro JSON com o manifesto (`PageConfig` / `ComponentConfig` / `ProcessStepConfig`). |
| `--stdin` | Lê o manifesto via stdin — mutuamente exclusivo com `--manifest`. Útil quando o JSON vem de um LLM ou de um script. |
| `--project <path>` | Raiz do projeto iGRP de destino. Se omitido, a CLI sobe na hierarquia a partir do `cwd` à procura de `.igrpstudio/` — o mesmo comportamento do `git`. |
| `--init` | Cria a estrutura `.igrpstudio/{pages,components,process}` se ainda não existir. Sem esta flag, gerar contra uma pasta sem marker falha com exit `1` — é um guardrail contra escritas acidentais. |
| `--dry-run` | Valida e mostra o caminho de saída sem escrever nada. |
| `--quiet` | Suprime logs de sucesso; útil quando estás a fazer pipe da saída. |

---

## Como funciona a deteção de projeto [#como-funciona-a-deteção-de-projeto]

O ficheiro `.igrpstudio/` na raiz do projeto é o **marker oficial** que identifica uma pasta como projeto iGRP — o mesmo papel que `.git/` desempenha para o git. O engine mantém lá dentro o source-of-truth JSON de cada artefacto (`pages/<id>.json`, `components/<id>.json`, `process/<id>.json`). O `.tsx` gerado vai para a zona gerida pelo Studio, `src/app/(igrp)/(generated)/`:

| Tipo | JSON (`.igrpstudio/`) | Código gerado |
| --- | --- | --- |
| `page` | `pages/<pageName>.json` | `src/app/(igrp)/(generated)/<path>/page.tsx` |
| `component` | `components/<name>.json` | `src/app/(igrp)/(generated)/<pagePath>/components/<name>.tsx`, ou `src/components/<name>.tsx` sem `pagePath` |
| `process-step` | `process/<processKey>/<key>.json` | `src/app/(igrp)/(generated)/process/[...process]/(<processKey>)/<key>.tsx` |


<GenerateProjectDetection />

| Cenário | Resolução |
| --- | --- |
| Corres `igrp gen page ...` dentro do projeto | `--project` é opcional — a CLI sobe até encontrar `.igrpstudio/`. |
| Pipeline/CI noutro contexto | Passa `--project /caminho/projecto` explicitamente. |
| Pasta nova, ainda sem `.igrpstudio/` | Passa `--project ./novo --init` para criar a estrutura. |

---

## Exemplo ponta a ponta [#exemplo-ponta-a-ponta]

O engine precisa de um projeto Next.js iGRP (criado pelo Studio ou pelo template): lê `src/app/igrp.config.ts` e valida o manifesto. Numa pasta vazia com `--init` a geração falha com `ENOENT … src/app/igrp.config.ts` (exit `2`).

```bash
cd ~/igrp/demo/projects/registos   # projeto Next.js criado no Studio

cat > hello.json <<'EOF'
{
  "type": "page",
  "pageName": "hello",
  "path": "hello",
  "components": {},
  "types": [],
  "imports": [],
  "states": [],
  "functions": []
}
EOF

igrp gen page --manifest hello.json
ls "src/app/(igrp)/(generated)/hello/page.tsx"
ls .igrpstudio/pages/hello.json
```

<Callout type="info" title="path sem barra inicial">
`path` segue a regra do Page Manager: segmentos separados por `/`, sem `/` no início (`hello`, `clientes/[id]`, `(admin)/relatorios`). `"/hello"` falha a validação com exit `2`.
</Callout>

O mesmo vale para passos de processo — por exemplo, regenerar um passo a partir do JSON que o Studio guardou:

```bash
igrp gen process-step --manifest .igrpstudio/process/registo-entidade/qualificacao.v1.json
# ✓ wrote src/app/(igrp)/(generated)/process/[...process]/(registo-entidade)/qualificacao.v1.tsx
```

O ficheiro resultante é idêntico ao que o Studio gera ao gravar (mesmo engine, `@igrp/igrp-studio-nextjs-engine` 0.2.0-beta.38 na CLI 1.7.9).

---

## Mais exemplos [#mais-exemplos]

```bash
# Forma curta com o alias 'gen', dentro do projeto (auto-deteta .igrpstudio/)
cd ~/projects/my-app
igrp gen page --manifest specs/dashboard.json

# Pipeline/CI: caminhos absolutos, sem auto-deteção
igrp generate page \
  --manifest /tmp/specs/users.json \
  --project /var/builds/my-app

# Via stdin — útil quando o manifesto sai de um LLM ou de um script
cat page.json | igrp gen page --stdin --project ./my-app

# Diretamente de um endpoint HTTP
curl -s https://api.exemplo.cv/specs/page/42 | igrp gen page --stdin --project ./my-app

# Gerar várias páginas a partir de um glob (pipeline shell)
for f in specs/*.json; do
  igrp gen page --manifest "$f" --project ./my-app --quiet
done

# Dry-run: ver o que seria escrito sem tocar no filesystem
igrp gen page --manifest page.json --project ./my-app --dry-run

# Modo silencioso para piping (só erros saem em stderr)
igrp gen page --manifest page.json --project ./my-app --quiet \
  && echo "ok" || echo "falhou com exit $?"
```

<Callout type="tip" title="Exatamente o mesmo pipeline do Studio">
`igrp generate` não é um gerador paralelo simplificado — é literalmente o mesmo código que o Studio corre em "Page Builder save". Um manifesto válido produz sempre o mesmo `.tsx` que produziria clicar em Guardar no Studio.
</Callout>

---

## Códigos de saída [#códigos-de-saída]

| Código | Significado |
| --- | --- |
| `0` | Sucesso. |
| `1` | Validação falhou — JSON inválido, `type` errado, identificador inválido, ou pasta sem `.igrpstudio/` e sem `--init`. |
| `2` | O engine falhou — validação do manifesto pelo engine (p.ex. `path` inválido), projeto sem `src/app/igrp.config.ts`, erro de filesystem ou exceção interna. |

---

## A seguir [#a-seguir]

- [Skills](./skills.mdx) — instalar cápsulas de conhecimento que ensinam um agente de IA a escrever estes manifestos corretamente.


# Visão geral

URL: /pt/docs/developer/studio/cli

Referência do @igrp/cli — comandos de ambiente (doctor, studio), criação de projetos, geração de artefactos, skills, bundles e migrações do template.

# CLI — `@igrp/cli` [#cli--igrpcli]

`@igrp/cli` (binário `igrp`) é a linha de comandos oficial do ecossistema iGRP. Esta página refere a versão **1.7.9**. Cobre estas áreas:

| Área | Comandos | Documentação |
| --- | --- | --- |
| Ambiente e Studio | `doctor`, `studio`, `studio-check-update`, `studio-install`, `new` | esta página |
| Geração de artefactos | `generate` (alias `gen`) | [Generate](./generate.mdx) |
| Gestão de skills | `skill` (alias `sk`) | [Skills](./skills.mdx) |
| Bundles (páginas, metadata, dependências) | `bundle` (alias `b`) | [Bundles](./bundles.mdx) |
| Atualizar o template de uma app | `migrate` | [Migrações do template](./migrate.mdx) |

---

## Instalação [#instalação]

O CLI instala-se no [primeiro arranque](/docs/developer/getting-started/primeiro-arranque) do Studio (botão **Install CLI**) ou no terminal:

```bash
npm install -g @igrp/cli --registry=https://sonatype.nosi.cv/repository/npm-group/
```

Requer Node.js `22.12+`. Os pacotes iGRP vivem no registry da NOSi, não no npm público — por isso o `--registry` é obrigatório (ou configurado uma vez no `.npmrc`). Confirma a versão com `igrp --version`.

<Callout type="info" title="Ajuda de cada comando">
`igrp <comando> --help` (ou `igrp help <comando>`) mostra as opções; por exemplo `igrp generate page --help`. As opções globais vêm antes do comando: `igrp -v` funciona, `igrp skill -v` dá erro.
</Callout>

---

## Ambiente e diagnóstico [#ambiente-e-diagnóstico]

### `igrp doctor` [#igrp-doctor]

Executa um diagnóstico do ambiente local, em três grupos (Frontend, Backend e Infraestrutura): Node.js, npm, pnpm, yarn, Java, Docker e Git. Corre isto sempre que algo "não faz sentido" antes de reportar um bug.

```bash
igrp doctor
```


---

## IGRP Studio Desktop [#igrp-studio-desktop]

```bash
igrp studio              # abre o Studio na pasta atual
igrp studio .            # equivalente
igrp studio /caminho/projecto
igrp studio-check-update # verifica se há uma versão do Studio compatível
igrp studio-install      # descarrega e instala essa versão
```

`igrp studio-check-update` resolve a versão certa por plataforma (macOS/Windows/Linux) e canal de atualização — a mesma lógica que o próprio Studio usa para se auto-atualizar.

---

## Criar um projeto novo [#criar-um-projeto-novo]

```bash
igrp new meu-projecto
```

`igrp new` **não gera código**: faz duas perguntas e cria a pasta `meu-projecto/` apenas com um `igrp.project.json`.

| Pergunta | Opções |
| --- | --- |
| Tipo de projeto | `web` (IGRP Web / Java), `springboot-api`, `nextjs` |
| Group ID | Texto; default `cv.nosi` |

```json
{
  "name": "meu-projecto",
  "type": "nextjs",
  "groupId": "cv.nosi",
  "createdAt": "2026-10-26T15:00:00.000Z"
}
```

No fim sugere `cd meu-projecto` e `igrp studio .`. O código do projeto é criado no Studio ([Novo Projeto](/docs/developer/workspace/primeiro-projeto)). O `type` serve ao [`igrp skill init`](./skills.mdx#igrp-skill-init) para escolher as skills recomendadas:

| `type` | Skills instaladas por `igrp skill init` |
| --- | --- |
| `nextjs` | `igrp-studio-metadata` |
| `springboot-api` | `igrp-spring-backend` (meta: instala as skills Spring Boot) |
| `web` | `igrp-studio-metadata` e `igrp-spring-backend` |

Num projeto criado pelo Studio não precisas de `igrp new`: o `skill init` lê o tipo de `.igrpstudio/baseApp.json`.

<Callout type="info" title="Versão">
`igrp new` funciona a partir da **1.7.9** (nas 1.7.0–1.7.8 falhava antes da primeira pergunta).
</Callout>


---

## Atualizar o template da app [#atualizar-o-template-da-app]

```bash
igrp migrate status        # migrações aplicadas e pendentes
igrp migrate plan          # o que vai mudar
igrp migrate apply         # aplica
igrp migrate@0.2.0-beta.16 status   # fixa a versão do migrator
```

Corre o `@igrp/template-migrator` (via `pnpm dlx` ou `npx`) na pasta da app, sempre na versão que o registry indica. Ver [Migrações do template](./migrate.mdx).

---

## A seguir [#a-seguir]

- [Generate](./generate.mdx) — gerar páginas, componentes e passos de processo a partir de manifestos JSON, o mesmo pipeline que o Studio usa ao gravar no Page Builder.
- [Skills](./skills.mdx) — instalar cápsulas de conhecimento para agentes IA (Claude Code, Cursor, Studio Prototype builder) num projeto.
- [Bundles](./bundles.mdx) — instalar pacotes declarativos (runtime de processos, templates de formulário, migrações) num projeto.


# Migrações do template

URL: /pt/docs/developer/studio/cli/migrate

Atualizar uma app Next.js iGRP às novas versões do template com igrp migrate (ou @igrp/template-migrator) — estado, plano, aplicação, rollback e CI.

# Atualizar a app com `igrp migrate` [#atualizar-a-app-com-igrp-migrate]

Quando sai uma nova versão do iGRP Framework, o `@igrp/template-migrator` aplica à tua app as alterações do template — novos ficheiros, middleware atualizado, bumps de dependências e novas chaves `.env.example` — como **migrações** numeradas e registadas em `.igrp-migrations-lock.json`.

O teu `.env` real **nunca é tocado** — ver [Variáveis de ambiente](#variáveis-de-ambiente) para o único passo manual.

---

## Correr o migrator [#correr-o-migrator]

Corre o comando `igrp migrate` (CLI **1.7.7** ou superior; recomendada a mais recente) na pasta da app:

```bash
cd minha-app
igrp migrate status            # o que falta
igrp migrate plan              # o que vai mudar (não escreve nada)
igrp migrate apply             # aplica, com confirmação por migração
pnpm install                   # se alguma migração mudou dependências
git add -A && git commit -m "chore: aplica migrações do template"
```

```
A usar @igrp/template-migrator 0.2.0-beta.16

Template: demo-v1  CLI: 0.2.0-beta.16

  ✓ applied  01-preview-mode-not-found
  …
  ✓ applied  72-home-overview-title-size (→ 0.2.0-beta.13)
  • pending  73-session-first-switcher-rollback-docs (→ 0.2.0-beta.13)
  • pending  74-framework-next-beta14-i18n-deps (→ 0.2.0-beta.14)
  • pending  75-multi-app-auth-sessions
  • pending  76-resync-framework-next-beta15 (→ 0.2.0-beta.15)

73 applied, 4 pending
```

Antes de correr, o CLI pergunta ao registry qual é a versão exata do migrator e mostra-a (*A usar @igrp/template-migrator …*), por isso `latest` é sempre a última publicada, mesmo com uma cópia antiga em cache. Para fixar uma versão:

```bash
igrp migrate@0.2.0-beta.16 status
igrp migrate --use 0.2.0-beta.16 status   # forma equivalente
```

Tudo o que vem depois do comando é passado ao migrator. O registry por defeito é `https://sonatype.nosi.cv/repository/igrp/` (variável `IGRP_REGISTRY`). Uma nova versão do migrator não exige nova versão do CLI.

<Callout type="info" title="Sem o CLI">
O mesmo migrator corre diretamente com `pnpm dlx` (ou `npx`): `pnpm dlx @igrp/template-migrator@latest status`. No Studio, o bundle **Template Migrator** (separador **Bundles** do projeto) aplica tudo de uma vez — equivale a `apply --yes --force`, por isso sobrepõe alterações locais aos ficheiros geridos.
</Callout>

<Callout type="info" title="Projetos novos">
Uma app criada pelo Studio já traz o `.igrp-migrations-lock.json`. O Studio descarrega sempre o template Next.js mais recente publicado, mas cada migração nova só chega aos projetos novos quando o template é republicado. Corre `igrp migrate status` depois de criar o projeto: se aparecerem pendentes, aplica-as como em qualquer outra app.
</Callout>

---

## Comandos [#comandos]

### `status` — ver o estado atual [#status--ver-o-estado-atual]

```bash
igrp migrate status
```

Lista as migrações aplicadas (`✓ applied`, ignoradas no próximo `apply`) e as pendentes (`• pending`), com o total no fim — ver o exemplo em [Correr o migrator](#correr-o-migrator).

---

### `plan` — pré-visualizar sem escrever [#plan--pré-visualizar-sem-escrever]

```bash
igrp migrate plan
```

Imprime cada operação de ficheiro que cada migração pendente irá executar. Nenhum ficheiro é escrito — seguro de correr a qualquer momento.

```
Pending migrations (1):

── 07-data-access-layer ─────────────────────────────────────
  file.create src/lib/dal.ts
  file.write  src/lib/auth.ts  (replace)
  deps.bump   package.json

No files were written.
```

---

### `apply` — correr as migrações [#apply--correr-as-migrações]

```bash
igrp migrate apply
```

O CLI percorre cada migração pendente por ordem. Para cada passo mostra o que vai fazer e pede confirmação antes de escrever. Após cada migração concluída regista uma entrada em `.igrp-migrations-lock.json`.

Se alguma `package.json` foi atualizada, o CLI imprime:

```
Next steps:
  pnpm install
```

Corre sempre `pnpm install` quando essa mensagem aparecer.

#### Opções úteis [#opções-úteis]

```bash
# Sem prompts — CI / scripts
igrp migrate apply --yes

# Aplicar só até uma migração específica
igrp migrate apply --to 04-multi-auth-provider
```

Se um ficheiro gerido pelo template foi alterado por ti, o `apply` pára nessa migração. Revê a alteração; para a sobrepor, repete com `--force`.

---

### `rollback` — desfazer uma migração [#rollback--desfazer-uma-migração]

```bash
igrp migrate rollback 04-multi-auth-provider
```

Restaura os ficheiros que a migração escreveu e remove a entrada do lock file. Os passos de desfazer correm em ordem inversa.

<Callout type="warning" title="Rollback pode recusar-se">
Se uma migração sobrescreveu ou apagou um ficheiro e o lock entry não tem cópia do conteúdo anterior (entradas escritas por versões mais antigas do CLI), o rollback lista esses paths e para. Usa `--force` para ignorar esses ficheiros e reverter o resto:

```bash
igrp migrate rollback 04-multi-auth-provider --force
```
</Callout>

<Callout type="info" title="Migrações que vieram com a app não podem ser revertidas">
Se a tua app foi criada a partir do template zip, as migrações já estão registadas mas o CLI nunca as correu aqui — não há nada para reverter. Usa `git` para desfazer manualmente esses ficheiros.
</Callout>

---

### `list` — listar todas as migrações disponíveis [#list--listar-todas-as-migrações-disponíveis]

```bash
igrp migrate list
```

Mostra todas as migrações incluídas nesta versão do CLI.

---

### `check` — gate de CI [#check--gate-de-ci]

```bash
igrp migrate check
```

Sai com código `1` se houver migrações pendentes, `0` se tudo estiver atualizado. Adiciona ao teu pipeline:

```yaml
- name: Check for pending IGRP migrations
  run: pnpm dlx @igrp/template-migrator@latest check
```

---

### `convert` — migrar lock file legado [#convert--migrar-lock-file-legado]

Versões antigas guardavam o estado em `.igrpmigrations/lock.json`. Se tens um, os outros comandos param e apontam para aqui:

```bash
igrp migrate convert
```

Move o estado para `.igrp-migrations-lock.json` na raiz do projeto e remove o diretório antigo. Commit o resultado e re-executa o que estavas a fazer.

---

## Variáveis de ambiente [#variáveis-de-ambiente]

As migrações só escrevem no **`.env.example`**. O teu `.env` real nunca é lido nem modificado.

Isso significa que uma migração que introduz uma nova configuração deixa a tua app sem ela. Após um `apply` que reporte novas chaves, compara os dois ficheiros e copia o que precisas:

```bash
git diff .env.example
```

| Tipo de chave | Urgência |
|---|---|
| Com default sensato (`IGRP_SYNC_ACCESS=false`) | Podes deixar até precisares |
| **Sem default** (`AUTENTIKA_CLIENT_ID`, etc.) | **Preenche antes de arrancar** |

---

## O lock file [#o-lock-file]

Após cada migração bem-sucedida o CLI escreve `.igrp-migrations-lock.json` na raiz do projeto.

<Callout type="warning" title="Faz commit deste ficheiro">
É a fonte de verdade de quais migrações a tua app já aplicou. Re-correr `apply` salta sempre as entradas completas.
</Callout>

---

## Resolução de problemas [#resolução-de-problemas]

**Migração parcialmente aplicada** — Re-corre `apply`. Se a execução foi interrompida (Ctrl-C, timeout de CI, crash), o CLI deixa um `.igrp-migration-journal.json` na raiz a registar o que já correu. O próximo `apply` reverte esses passos e re-aplica a migração desde a base limpa:

```
Recovering from an interrupted run of 29-permissions-catalog-sync...
  reverted 3 step(s) from the interrupted migration
  ✓ recovered
```

**Build falha após aplicar** — Reverte, verifica o guia em prosa daquela migração para passos manuais, e re-aplica:

```bash
igrp migrate rollback <id>
# ajustes manuais
igrp migrate apply --to <id>
```

**Lock file em falta mas migrações já aplicadas** — Re-corre `apply`. As sobrescritas são idempotentes e as entradas ficam registadas corretamente.

---

## Que migrações existem [#que-migrações-existem]

```bash
igrp migrate list
```

Lista todas as migrações da versão do migrator em uso, com a data e a versão do framework a que levam (`→ 0.2.0-beta.15`). Na 0.2.0-beta.16 são **76**, de `01-preview-mode-not-found` a `76-resync-framework-next-beta15`; funciona também fora de uma app. Para o que cada uma muda, usa `igrp migrate plan` na tua app.

---

## Códigos de saída de `igrp migrate` [#códigos-de-saída-de-igrp-migrate]

| Código | Significado |
| --- | --- |
| `0` | Sucesso. |
| `1` | Validação — versão inválida, pasta que não é uma app iGRP, nem `pnpm` nem `npx` no `PATH`. |
| `2` | Registry — versão não encontrada, registry inalcançável. |
| outro | Código de saída do próprio migrator, passado tal e qual (`check` sai com `1` se houver migrações pendentes ou alteradas). |


# Skills

URL: /pt/docs/developer/studio/cli/skills

Instalar, listar e atualizar cápsulas de conhecimento (skills) que ensinam agentes de IA a trabalhar corretamente num projeto iGRP.

# `igrp skill` — gerir skills de agentes IA num projeto [#igrp-skill--gerir-skills-de-agentes-ia-num-projeto]

O comando `igrp skill` (alias `sk`) gere **skills** num projeto iGRP — cápsulas de conhecimento em formato Anthropic que vivem em `<projeto>/.agents/skills/<nome>/` e são consumidas por agentes de IA (Claude Code, Cursor, o Prototype builder do Studio). Cada skill cobre **um único framework** (Next.js, Spring, .NET, Process) ou é uma `meta` que agrega outras via `dependencies`.

Uma skill é uma pasta com `SKILL.md` (frontmatter `name` + `description`), ficheiros companheiros carregados sob demanda (subpastas como `references/`, `assets/` ou `scripts/` são preservadas na instalação) e um `skill.json` — o manifesto com `version`, `framework`, `tags`, `dependencies` e a lista exata de `files`. As skills publicadas vivem em `studio/_skills/` no workspace.

<Callout type="tip" title="Não é o mesmo layout do Claude Code">
As skills do iGRP usam o mesmo formato `SKILL.md` das skills do Claude Code, mas instalam-se em `.agents/skills/<nome>/`, não em `.claude/skills/`. É um formato-ponte pensado para ser lido por vários agentes diferentes, não só pelo Claude Code.
</Callout>

---

## Subcomandos [#subcomandos]

```bash
igrp skill add <name>       --project <path> [--version <semver>] [--source <url>] [--force] [--dry-run]
igrp skill list             --project <path> [--json]
igrp skill remove <name>    --project <path> --yes [--force]
igrp skill update [<name>]  --project <path> [--allow-major] [--source <url>]
igrp skill search [<query>] [--framework <fw>] [--tag <a,b>] [--source <url>] [--json]
igrp skill info <name>      [--version <semver|range>] [--source <url>] [--json]
igrp skill init             --project <path> [--type <type>] [--source <url>] --yes
```

| Flag | Onde se aplica | Efeito |
| --- | --- | --- |
| `--project <path>` | todos | Raiz do projeto. Default: `cwd`. As skills instalam-se em `<projeto>/.agents/skills/<nome>/`. |
| `--source <url>` | `add`, `search`, `info`, `update`, `init` | URL base do registry. Default: env `IGRP_SKILL_REGISTRY`, senão `https://sonatype.nosi.cv/repository/igrp-templates/@igrp/skills/`. |
| `--version <semver>` | `add`, `info` | Versão exata ou range (`^1.2.0`); default `latest` — a maior versão estável (pre-releases só se as pedires). |
| `--force` | `add`, `remove` | `add`: sobrepõe uma skill já instalada e reinstala as dependências. `remove`: remove mesmo havendo skills que dependem dela. |
| `--yes` | `remove`, `init` | Confirma a operação. Sem esta flag, `remove` só mostra o que faria (dry-print) — é não-interativo por design. |
| `--allow-major` | `update` | Permite saltos MAJOR. Por omissão só MINOR/PATCH atualizam automaticamente. |
| `--framework <fw>` | `search` | Filtra por `nextjs`, `spring`, `dotnet`, `process` ou `meta`. |
| `--tag <a,b>` | `search` | Filtra por tags (separadas por vírgula; todas obrigatórias). Os termos da query têm de casar todos com nome, descrição ou tags. |
| `--type <type>` | `init` | Tipo do projeto (`nextjs`, `springboot-api`, `web`). Default: lê `.igrpstudio/baseApp.json` (Studio) ou `igrp.project.json`. |
| `--quiet` | `add`, `remove`, `update`, `init` | Suprime os logs de sucesso. |
| `--json` | `list`, `search`, `info` | Saída *machine-readable*. |
| `--dry-run` | `add`, `remove` | Valida e mostra o plano sem instalar / apagar. |


### Dependências [#dependências]

`add`, `init` e `update` resolvem as `dependencies` de forma recursiva:

- Para cada range escolhem a maior versão que o satisfaz e instalam as dependências antes de quem as pede.
- Uma dependência já instalada numa versão dentro do range é mantida (`= <nome>@x.y.z já instalada`). Fora do range, a instalação pára com exit `3` até correres `igrp skill update <dep>` ou repetires com `--force`.
- Duas skills a pedir ranges incompatíveis da mesma dependência → exit `1` (*Conflito de versões*). Ciclos de dependências também dão exit `1`.
- Antes de pôr a pasta no lugar, o tarball é verificado (sha256 e conteúdo exatamente igual a `files`); se falhar, nada é escrito.

Uma skill `meta` não tem ficheiros próprios — só um `skill.json` com dependências. É o caso de `igrp-spring-backend`, que instala de uma vez as skills Spring Boot:

```bash
igrp skill add igrp-spring-backend --project .
ls .agents/skills/spring-data-jpa/    # SKILL.md, skill.json, references/, assets/
```

---

## Exemplo ponta a ponta [#exemplo-ponta-a-ponta]

```bash
# Descobre o que existe
igrp skill search --json | jq '.skills[].name'
igrp skill search --framework nextjs

# Instala a skill canónica de metadata do Studio (Next.js) no projeto atual
igrp skill add igrp-studio-metadata --project .

# Verifica
ls .agents/skills/igrp-studio-metadata/    # SKILL.md, *.md, skill.json
cat .agents/skills/.installed.json
igrp skill list --project .
igrp skill info igrp-studio-metadata

# Mantém atualizada
igrp skill update --project .

# Num projeto novo, instala as recomendadas para o tipo de projeto
igrp skill init --project . --yes          # lê igrp.project.json.type

# Remove
igrp skill remove igrp-studio-metadata --project . --yes
```

`.agents/skills/.installed.json` é o registo local do que está instalado:

```json
{
  "skills": {
    "igrp-studio-metadata": {
      "version": "1.5.3",
      "framework": "nextjs",
      "source": "https://sonatype.nosi.cv/repository/igrp-templates/@igrp/skills/",
      "dependencies": {},
      "installedAt": "2026-05-13T10:00:00.000Z"
    }
  }
}
```

Override do registry via variável de ambiente:

```bash
export IGRP_SKILL_REGISTRY=https://sonatype.nosi.cv/repository/igrp-templates/@igrp/skills/
```

### `igrp skill init` [#igrp-skill-init]

Lê `.igrpstudio/baseApp.json` (projetos Studio) ou `igrp.project.json` (criado por [`igrp new`](./index.mdx#criar-um-projeto-novo)) para saber o **tipo** de projeto e instala as skills recomendadas para esse tipo — ver a [tabela de recomendações](./index.mdx#criar-um-projeto-novo). Sem `--yes`, só mostra o plano:

```bash
igrp skill init --project .
# Tipo do projecto: nextjs (via .igrpstudio/baseApp.json)
# Skills recomendadas: igrp-studio-metadata
# Re-corre com --yes para instalar.
```


---

## A skill `igrp-studio-metadata` [#a-skill-igrp-studio-metadata]

A skill de referência para o lado Next.js: ensina um agente a escrever os ficheiros JSON de metadata do Studio (`.igrpstudio/pages/`, `components/`, `process/`) que compilam corretamente através do engine — e a fazer troubleshooting do código gerado em `src/app/(igrp)/(generated)/`.

Ficheiros companheiros, carregados sob demanda (não pré-carregados de uma vez):

| Ficheiro | Quando carregar |
| --- | --- |
| `patterns.md` | Ao começar uma página nova — templates prontos a adaptar (listagens, formulários, modais, filtros, cartões de estatísticas). |
| `component-reference.md` | Ao escolher que componente usar — catálogo completo com propriedades típicas. |
| `process-steps.md` | Ao escrever ou editar qualquer coisa em `.igrpstudio/process/` — as regras de página/componente sozinhas não chegam para passos de processo. |
| `troubleshooting.md` | Ao corrigir um erro — mapeia mensagens de erro para causas no JSON. |
| `project-conventions.md` | Referência de convenções do projeto (hooks, funções auxiliares, cores de estado). |

<Callout type="note" title="Já cobre permission rules">
A versão 1.5.x desta skill (a mais recente é a 1.5.3) ensina o agente a gerar regras de permissão (`type: "permission"` — `hide`/`disable`/`replace`/`assert`) nos ficheiros de metadata — o mesmo mecanismo declarativo do painel *Permission rule* do Page Builder (Hide/Disable/Replace with…/Assert). Requer engine ≥0.2.0-beta.28.
</Callout>

---

## Códigos de saída [#códigos-de-saída]

| Código | Significado |
| --- | --- |
| `0` | Sucesso. |
| `1` | Validação — nome inválido, frontmatter mal-formado, path traversal, dependentes bloqueiam a remoção, ciclo ou conflito de dependências, range sem versão disponível em `info`. |
| `2` | Registry/rede/integridade — 404, host inalcançável, sha256 não corresponde, tarball com ficheiros em falta ou não declarados. |
| `3` | Skill já instalada, ou dependência instalada fora do range (sem `--force`). |

---

## A seguir [#a-seguir]

- [Generate](./generate.mdx) — o comando que a skill `igrp-studio-metadata` ensina a preparar corretamente.


# Widgets

URL: /pt/docs/developer/studio/ide/components

Catálogo dos widgets da Paleta de Widgets do Page Builder — categorias e elementos visuais para construir páginas e componentes.

Os **Widgets** são os elementos visuais pré-construídos disponíveis na **Paleta de Widgets** do [Page Builder](./page-builder.mdx). São os blocos base (micro) que arrastas para o canvas para construir **Páginas** e **[Componentes](./page-manager.mdx#componentes)** (os blocos macro que crias no Page Manager).

Cada widget é gerado como um componente do [Design System](/docs/developer/framework/design-system) — por exemplo, o widget **Table** passa a `IGRPDataTable` e o **Input Text** a `IGRPInputText`. Esta página explica como configurá-los no Studio; o Design System documenta as props e o uso em código.

---

## Arquitetura [#arquitetura]

O FrontEnd do iGRP segue uma arquitetura modular e escalável, com foco em:

<Grid cols={3}>
  <Feature icon="Puzzle" color="blue" title="Componentização">Uso extensivo de componentes reutilizáveis</Feature>
  <Feature icon="Zap" color="amber" title="Renderização Híbrida">SSR (Server-Side Rendering) e SSG (Static Site Generation)</Feature>
  <Feature icon="Shield" color="green" title="Otimização de Performance">Code-splitting, lazy loading e otimização de imagens</Feature>
  <Feature icon="Type" color="purple" title="Tipagem Forte">Suporte completo a TypeScript</Feature>
  <Feature icon="Palette" color="teal" title="Temas e Estilização">Suporte a múltiplos temas e sistemas de design</Feature>
</Grid>

---

## Categorias de Widgets [#categorias-de-widgets]

Na **Paleta de Widgets** do Page Builder, os widgets estão agrupados por categoria:

<Accordions>
  <Accordion title="Elementos Básicos (20)">
    <Badge color="blue">Accordion</Badge> <Badge color="blue">Alert</Badge> <Badge color="blue">Alert Dialog</Badge> <Badge color="blue">Avatar</Badge> <Badge color="blue">Badge</Badge> <Badge color="blue">Button</Badge> <Badge color="blue">Card Details</Badge> <Badge color="blue">Chat</Badge> <Badge color="blue">CopyTo</Badge> <Badge color="blue">Dropdown</Badge> <Badge color="blue">Form List</Badge> <Badge color="blue">Image</Badge> <Badge color="blue">Info Card</Badge> <Badge color="blue">Input Search</Badge> [<Badge color="blue" outline>Modal Dialog</Badge>](#widget-modal) <Badge color="blue">Separator</Badge> <Badge color="blue">Stats Card</Badge> <Badge color="blue">Status Banner</Badge> <Badge color="blue">Video Embed</Badge> <Badge color="blue">Repetitive Component</Badge>
  </Accordion>
  <Accordion title="Visualização de Dados (12)">
    <Badge color="green">Area Chart</Badge> <Badge color="green">Horizontal Bar Chart</Badge> <Badge color="green">Line Chart</Badge> <Badge color="green">PDF Viewer</Badge> <Badge color="green">Pie Chart</Badge> <Badge color="green">Radar Chart</Badge> <Badge color="green">Radial Bar Chart</Badge> [<Badge color="green" outline>Table</Badge>](#widget-tabela) <Badge color="green">Vertical Bar Chart</Badge> <Badge color="green">Stats Card Mini</Badge> <Badge color="green">Stats Card (Top Border)</Badge> <Badge color="green">User Avatar</Badge>
  </Accordion>
  <Accordion title="Elementos de Formulário (29)">
    <Badge color="purple">Calendar Single</Badge> <Badge color="purple">Calendar SingleTime</Badge> <Badge color="purple">Calendar Range</Badge> <Badge color="purple">Calendar RangeTime</Badge> <Badge color="purple">Calendar Multiple</Badge> <Badge color="purple">Calendar MultipleTime</Badge> <Badge color="purple">Checkbox</Badge> <Badge color="purple">Combobox</Badge> <Badge color="purple">Date Range</Badge> <Badge color="purple">Date Picker Single</Badge> <Badge color="purple">Date Multiple</Badge> <Badge color="purple">Icon</Badge> <Badge color="purple">Add On</Badge> <Badge color="purple">Color</Badge> <Badge color="purple">Date Picker</Badge> <Badge color="purple">Date Picker Input Single</Badge> <Badge color="purple">Upload File</Badge> <Badge color="purple">Input Hidden</Badge> <Badge color="purple">Number</Badge> <Badge color="purple">Password</Badge> <Badge color="purple">Phone</Badge> <Badge color="purple">Input Text</Badge> <Badge color="purple">Textarea</Badge> <Badge color="purple">Time</Badge> <Badge color="purple">URL</Badge> <Badge color="purple">Label</Badge> <Badge color="purple">Radio</Badge> <Badge color="purple">Select</Badge> <Badge color="purple">Switch</Badge>
  </Accordion>
  <Accordion title="Layout (6)">
    <Badge color="orange">Card</Badge> <Badge color="orange">Menu Navigation</Badge> <Badge color="orange">Page Header</Badge> <Badge color="orange">Aspect Ratio</Badge> <Badge color="orange">Scroll Area</Badge> <Badge color="orange">Tabs</Badge>
  </Accordion>
  <Accordion title="Estrutura (6)">
    <Badge color="teal">Columns</Badge> <Badge color="teal">Container</Badge> <Badge color="teal">Flex</Badge> <Badge color="teal">Fragment</Badge> <Badge color="teal">Grid</Badge> <Badge color="teal">Section</Badge>
  </Accordion>
  <Accordion title="Contentores (1)">
    [<Badge color="red" outline>Form</Badge>](#widget-formulário)
  </Accordion>
  <Accordion title="Tipografia (7)">
    <Badge color="primary">Headline</Badge> <Badge color="primary">Paragraph</Badge> <Badge color="primary">Span</Badge> <Badge color="primary">Field Description</Badge> <Badge color="primary">Keyboard Key</Badge> <Badge color="primary">Text</Badge> <Badge color="primary">Text List</Badge>
  </Accordion>
  <Accordion title="Feedback (10)">
    <Badge color="amber">Notification</Badge> <Badge color="amber">Loading Spinner</Badge> <Badge color="amber">Banner</Badge> <Badge color="amber">Progress</Badge> <Badge color="amber">Skeleton</Badge> <Badge color="amber">Spinner</Badge> <Badge color="amber">Tooltip</Badge> <Badge color="amber">Hover Card</Badge> <Badge color="amber">Empty State</Badge> <Badge color="amber">Toaster</Badge>
  </Accordion>
  <Accordion title="Navegação (6)">
    <Badge color="cyan">Page Header Back Button</Badge> <Badge color="cyan">Menubar</Badge> <Badge color="cyan">Breadcrumb</Badge> <Badge color="cyan">Pagination</Badge> <Badge color="cyan">Stepper (UI)</Badge> <Badge color="cyan">Navigation Menu</Badge>
  </Accordion>
  <Accordion title="Formulários (4)">
    <Badge color="green">Date Time Input</Badge> <Badge color="green">Toggle</Badge> <Badge color="green">Input OTP</Badge> <Badge color="green">Input Group</Badge>
  </Accordion>
  <Accordion title="Multimédia (1)">
    <Badge color="purple">Image Cropper</Badge>
  </Accordion>
  <Accordion title="Overlays (4)">
    <Badge color="neutral">Popover</Badge> <Badge color="neutral">Sheet</Badge> <Badge color="neutral">Drawer</Badge> <Badge color="neutral">Command (Search Palette)</Badge>
  </Accordion>
</Accordions>

<Callout type="info" title="Pesquisa rápida">
  Usa o campo **Search** (`⌘F` / `Ctrl+F`) no topo da paleta para encontrar um widget pelo nome.
</Callout>

---

## Widget Tabela [#widget-tabela]

O widget **Table** (gera um `IGRPDataTable`) mostra dados em linhas e colunas, com colunas tipadas, filtros, ações por linha e binding aos dados.

### Colunas [#colunas]

<Steps>
  <Step>Arrasta o widget **Table** para o canvas.</Step>
  <Step>Clica em **Manage Columns** — abre a **Gestão de Componentes** das colunas.</Step>
  <Step>Em **Adicionar Componente**, escolhe o tipo de coluna: **Text**, **Amount**, **Date**, **Badge**, **Link**, **Tooltip**, **Check**, **Expandable**, **Actions** ou **Hidden Column**.</Step>
  <Step>Em **Edit**, dá à coluna o nome do campo (por exemplo `requerente`) e o **Header Title** que aparece no cabeçalho.</Step>
</Steps>

<StudioPreview imageName="actionTable" alt="Gestão de Componentes da tabela com colunas Requerente, Tipo de pedido, Data, Estado e a coluna de Ações" />

Ao passar o rato sobre a tabela aparece a barra de ferramentas do widget, com **Add Comp** (colunas, filtros ou um *Row Subcomponent*) e **Binding Config**.

<StudioPreview imageName="addComp" alt="Barra de ferramentas da tabela e o diálogo Adicionar Componente com Table Column, Table Filter e Row Subcomponent" />

### Binding [#binding]

**Binding Config** gera o tipo dos dados da tabela a partir das colunas. Dá-lhe um **Nome** (por exemplo `Pedido`) e confirma o **Nome** e o **Tipo de dado** de cada campo — têm de coincidir com o que a API devolve.

<StudioPreview imageName="bindingConfig" alt="Binding Configuration da tabela com o nome Pedido e os campos requerente, tipo, data e estado" />

### Carregar os dados [#carregar-os-dados]

<Steps>
  <Step>Abre as configurações da página (engrenagem no canto superior direito do canvas).</Step>
  <Step>No separador **Interações**, edita o gatilho **On load**.</Step>
  <Step>Em **Custom Code**, carrega os dados e passa-os ao estado da tabela (`setContentTabletable1`). O painel **Function Settings** lista os estados, funções e snippets disponíveis — **Name** insere a variável, **Set** insere o setter.</Step>
  <Step>**Save changes** e guarda a página.</Step>
</Steps>

<StudioPreview imageName="interactions" alt="Separador Interações da página com o gatilho On load" />
<StudioPreview imageName="configInteractions" alt="Edit Interaction do On load com código que carrega os pedidos e Function Settings com os estados" />

O código gerado fica assim:

```typescript
const { data, isLoading } = useGetTodos();

useEffect(() => {
  if (!data || isLoading) return;
  setContentTabletable1(data || []);
}, [isLoading, data]);
```

### Coluna de ações [#coluna-de-ações]

<Steps>
  <Step>Em **Manage Columns**, junta uma **Actions Column**.</Step>
  <Step>Na linha da coluna de ações, **Adicionar Componente** junta as ações: **Link Action**, **Modal Action**, **Alert Action** ou **Dropdown Column**.</Step>
  <Step>Em **Edit**, configura cada ação: **Label Trigger** (texto), e as props próprias — destino do link, título e botões do modal, título e mensagem do alerta.</Step>
</Steps>

Cada ação recebe `rowData` — os dados da linha em que foi clicada (`rowData.id`, `rowData.estado`…), para usar nos handlers.

### Ações com dados dinâmicos [#ações-com-dados-dinâmicos]

Numa **Link Action**, escolhe **Href → Page** para navegar para uma página do projeto. Se a página tiver segmentos dinâmicos (`/pedidos/[id]`), mapeia cada **Route Segment** ao **Data Field** da linha (`id` → `id`). Em **Navigation Parameters** podes juntar parâmetros de query.

<StudioPreview imageName="configuringActionsWithDynamicData" alt="Link Action com Href em modo Page e o segmento id mapeado para o campo id" />

---

## Widget Formulário [#widget-formulário]

O widget **Form** (gera um `IGRPForm` com um esquema Zod) agrupa os campos, valida-os e trata a submissão.

### Configuração do formulário [#configuração-do-formulário]

<Steps>
  <Step>Arrasta o widget **Form** para o canvas — já traz um **Headline** e uma **Grid**.</Step>
  <Step>Arrasta os campos para dentro da grelha (Input Text, Select, Date Picker Single, Textarea…).</Step>
  <Step>Ajusta as props de cada campo na sidebar de edição — **Label**, **Placeholder**, **Required**…</Step>
  <Step>Junta um **Button** ao formulário para a submissão.</Step>
</Steps>

<StudioPreview imageName="form" alt="Formulário Novo pedido com os campos Requerente, Tipo de pedido, Data do pedido e Observações e a barra de ferramentas do Form" />

### Binding [#binding-1]

**Binding Config** na barra do Form gera o esquema de validação a partir dos campos.

<Checklist>
  <Item title="Nome do campo">É a chave enviada à API (`requerente`, `tipo`, `data`…). Por defeito é o nome técnico do componente (`inputText1`) — muda-o para o nome que a API espera.</Item>
  <Item title="Tipo de dado">`string`, `number`, `date`… define a validação gerada.</Item>
  <Item title="Required, Nullable, IsList, Valor Padrão">Regras de cada campo no esquema.</Item>
</Checklist>

<StudioPreview imageName="bindingConfigurationForm" alt="Binding Configuration do formulário com os campos requerente, tipo, data e observacoes" />

### Submissão [#submissão]

<Steps>
  <Step>Seleciona o botão e abre o separador **Interações** — o gatilho **Mouse click (tap)** já existe.</Step>
  <Step>Edita-o e, em **Action Type**, escolhe **Form Submit**.</Step>
  <Step>Em **Target Form**, escolhe o formulário a submeter.</Step>
</Steps>

<StudioPreview imageName="interactionEvent" alt="Separador Interações do botão Submeter pedido com o gatilho Mouse click (tap)" />
<StudioPreview imageName="triggerFormSubmit" alt="Edit Interaction com Action Type Form Submit e o formulário de destino" />

Para tratar o resultado (chamar a API, mostrar um toast de sucesso ou de erro), usa **Action Type → Function** com uma função do [Custom Code](./customCode.mdx).

<StudioPreview imageName="editInteraction" alt="Edit Interaction com Action Type Function, a escolha de função e a função inline" />

---

## Widget Modal [#widget-modal]

O widget **Modal Dialog** (gera um `IGRPModalDialog`) traz cabeçalho (título e descrição), rodapé com botão de fechar, uma zona de conteúdo e um **trigger** — um botão que o abre e fecha sem configuração adicional.

<StudioPreview imageName="modalDialog" alt="Widget Modal Dialog no canvas com título, descrição, fecho, zona de conteúdo e o botão trigger" />

Arrasta para a zona de conteúdo os widgets do modal — por exemplo um **Textarea** para o motivo do cancelamento.

<StudioPreview imageName="modalDialog_AddComponent" alt="Modal Dialog com um Textarea na zona de conteúdo" />

### Controlo por estado [#controlo-por-estado]

Para abrir o modal a partir de outro elemento (um botão noutra zona da página, uma ação da tabela), controla-o com um estado:

<Steps>
  <Step>No [Custom Code](./customCode.mdx#criar-estados), cria um estado **Boolean** (por exemplo `modalCancelarAberto`) e uma função que o põe a `true`.</Step>
  <Step>Nas propriedades do Modal Dialog, passa o rato sobre **Open**, clica em **⋮** e escolhe o estado em **State**.</Step>
  <Step>No separador **Interações** do modal, junta o gatilho **On open** com a função inline `(open) => setModalCancelarAberto(open)` — mantém o estado certo quando o modal é fechado.</Step>
  <Step>No botão que abre o modal, em **Interações → Mouse click (tap)**, escolhe **Action Type → Function** e a função criada.</Step>
</Steps>

<StudioPreview imageName="associateState" alt="Prop Open do Modal Dialog ligada ao estado modalCancelarAberto" />
<StudioPreview imageName="editInteractions_setOpen" alt="Gatilho On open do modal com a função inline que atualiza o estado" />
<StudioPreview imageName="addButton_InfigureInteraction" alt="Separador Interações do botão Cancelar pedido com o gatilho Mouse click (tap)" />
<StudioPreview imageName="callSate" alt="Edit Interaction do botão com a função abrirCancelamento selecionada" />

### A partir da tabela [#a-partir-da-tabela]

Numa coluna de ações, a **Modal Action** abre um modal próprio por linha: configura o **Modal Title** e os botões **Cancel**/**Confirm** em **Edit**.

<StudioPreview imageName="actionsColumn" alt="Modal Action Editar com Modal Title Editar pedido e os botões Cancelar e Guardar" />


# Custom Code

URL: /pt/docs/developer/studio/ide/customCode

Funções, estados e snippets em TypeScript nas páginas do iGRP Studio — o painel Código Personalizado do Page Builder.

O painel **Código Personalizado** do [Page Builder](./page-builder.mdx) junta à página lógica própria: **funções** reutilizáveis e **estados** reativos, além de **snippets** prontos a inserir. É o sítio para carregar dados, tratar submissões ou controlar widgets (abrir um modal, mostrar um toast).

<Grid cols={3}>
  <Feature icon="SquareFunction" color="blue" title="Functions">Funções com argumentos, tipo de retorno e imports</Feature>
  <Feature icon="Zap" color="green" title="States">Variáveis reativas da página — também as que os widgets criam (`contentTabletable1`…)</Feature>
  <Feature icon="FileCode" color="purple" title="Snippets">Form Reference Usage, Use Effect e os toasts (Custom, Error, Info, Promise, Success, Warning)</Feature>
</Grid>

<StudioPreview imageName="customcode" alt="Painel Código Personalizado com a função abrirCancelamento, os estados da página e os snippets" />

O **+** no topo do painel cria uma **Function** ou um **State**.

---

## Criar uma função [#criar-uma-função]

<Steps>
  <Step>**+ → Function**.</Step>
  <Step>Em **Function Settings → Props**, dá-lhe um **Nome** e define **IsAsync**, o **Return Type** (Void, String, Number…), **Is List** / **Is Optional** e os argumentos (**Add Argument**).</Step>
  <Step>Escreve o corpo em **Function body**. A assinatura (`function nome() → tipo`) atualiza-se em tempo real.</Step>
  <Step>Se precisares de bibliotecas, junta-as em **Importações**.</Step>
  <Step>**Create Function**.</Step>
</Steps>

<StudioPreview imageName="criarFuncao" alt="Diálogo Create Function com a função abrirCancelamento e o painel Function Settings" />

<Callout type="tip" title="Reutilizar estados, funções e snippets">
  Os separadores **States**, **Functions**, **Snippets** e **Types** do Function Settings listam o que a página já tem. Em cada estado, **Name** insere a variável e **Set** insere o setter (`setModalCancelarAberto`); nas funções e snippets, **Insert** insere a chamada no editor.
</Callout>

Uma função criada aqui fica disponível nas interações dos widgets: **Interações → Action Type → Function → Function**.

---

## Criar estados [#criar-estados]

Os estados são variáveis reativas: quando mudam, os widgets ligados a eles atualizam-se.

<Steps>
  <Step>**+ → State**.</Step>
  <Step>Dá-lhe um **Nome** e escolhe o **Tipo** (String, Number, Boolean…).</Step>
  <Step>Define o **Valor Padrão** (num Boolean é uma checkbox).</Step>
  <Step>Marca **É array** se guarda uma lista, ou **É opcional** se pode ficar vazio.</Step>
  <Step>Se depender de tipos ou bibliotecas externas, junta-os em **Importações**.</Step>
  <Step>**Salvar Alterações**.</Step>
</Steps>

<StudioPreview imageName="criarEstado" alt="Diálogo Create State com o estado modalCancelarAberto do tipo Boolean" />

Para ligar um estado a uma prop de um widget (por exemplo **Open** de um Modal Dialog), passa o rato sobre a prop na sidebar de edição, clica em **⋮** e escolhe o estado — ver [Widget Modal](./components.mdx#widget-modal).


# Custom Components

URL: /pt/docs/developer/studio/ide/customComponents

Escrever componentes, funções e tipos próprios em código e registá-los em igrp.config.ts para os usar no Page Builder do iGRP Studio.

Quando a [Paleta de Widgets](./components.mdx) não tem o que precisas, escreve-o em código no projeto e regista-o em `src/app/igrp.config.ts`. O Studio lê esse registo e passa a oferecer:

<Grid cols={3}>
  <Feature icon="Puzzle" color="purple" title="Componentes">Na paleta, na categoria **Componentes Personalizados**, com as props editáveis na sidebar</Feature>
  <Feature icon="SquareFunction" color="blue" title="Funções">No [Código Personalizado](./customCode.mdx) e nas interações (**Action Type → Function**)</Feature>
  <Feature icon="Braces" color="teal" title="Tipos">Para tipar estados, funções e argumentos</Feature>
</Grid>

## Onde ficam os ficheiros [#onde-ficam-os-ficheiros]

Tudo o que registas vive em `src/app/(myapp)/`. O exemplo deste guia:

<Files>
  <Folder name="src/app" defaultOpen>
    <File name="igrp.config.ts" />
    <Folder name="(myapp)" defaultOpen>
      <Folder name="components" defaultOpen>
        <File name="tag-estado.tsx" />
      </Folder>
      <Folder name="functions" defaultOpen>
        <File name="pedidos.ts" />
      </Folder>
      <Folder name="types" defaultOpen>
        <File name="pedido.ts" />
      </Folder>
    </Folder>
  </Folder>
</Files>

## 1. Escrever o componente [#1-escrever-o-componente]

Um componente é uma função React exportada, de preferência construída com o [Design System](/docs/developer/framework/design-system) — assim herda o tema e o dark mode. As props declaradas na assinatura passam a ser editáveis no Studio.

```tsx title="src/app/(myapp)/components/tag-estado.tsx"
"use client";

import { IGRPBadge } from "@igrp/igrp-framework-react-design-system";

import type { EstadoPedido } from "../types/pedido";

const CORES = {
  pendente: "warning",
  aprovado: "success",
  rejeitado: "destructive",
} as const;

type TagEstadoProps = {
  estado: EstadoPedido;
  mostrarPonto?: boolean;
};

export function TagEstado({ estado, mostrarPonto = true }: TagEstadoProps) {
  return (
    <IGRPBadge variant="soft" color={CORES[estado]} dot={mostrarPonto}>
      {estado}
    </IGRPBadge>
  );
}
```

<Callout type="tip" title="Partir de um componente existente">
  Podes copiar um componente do [shadcn/ui](https://ui.shadcn.com) ou gerá-lo com IA: os primitives do shadcn já existem no Design System (`Button`, `Dialog`…), por isso basta trocar os `import` para `@igrp/igrp-framework-react-design-system`. Ver [Primitives e Custom](/docs/developer/framework/design-system/primitives).
</Callout>

## 2. Funções e tipos (opcional) [#2-funções-e-tipos-opcional]

Funções e tipos registam-se da mesma forma — úteis para a lógica que os componentes e as páginas partilham:

```ts title="src/app/(myapp)/types/pedido.ts"
export type EstadoPedido = "pendente" | "aprovado" | "rejeitado";

export interface Pedido {
  id: number;
  requerente: string;
  tipo: string;
  data: string;
  estado: EstadoPedido;
}
```

```ts title="src/app/(myapp)/functions/pedidos.ts"
import type { Pedido } from "../types/pedido";

export async function listarPedidos(estado?: string): Promise<Pedido[]> {
  const query = estado ? `?estado=${estado}` : "";
  const response = await fetch(`/api/pedidos${query}`);
  if (!response.ok) throw new Error("Não foi possível carregar os pedidos");
  return response.json();
}
```

## 3. Registar em igrp.config.ts [#3-registar-em-igrpconfigts]

```ts title="src/app/igrp.config.ts"
export default {
  types: ["types/pedido"],
  actions: [],
  functions: ["functions/pedidos"],
  components: ["components/tag-estado"],
};
```

<Checklist>
  <Item title="Caminhos relativos a src/app/(myapp)">Sem `./`, sem o prefixo `src/app/(myapp)` e sem extensão — `components/tag-estado`, não `./components/tag-estado.tsx`.</Item>
  <Item title="Componentes em .tsx, o resto em .ts">O Studio procura `.tsx` para componentes e `.ts` para tipos, funções e actions.</Item>
  <Item title="Ficheiros fora de (myapp) são ignorados">Só é lido o que está dentro de `src/app/(myapp)`.</Item>
  <Item title="actions">Server actions do projeto, registadas da mesma forma.</Item>
</Checklist>

<Callout type="info" title="O comentário do ficheiro gerado está desatualizado">
  O `igrp.config.ts` do template ainda fala em `src/app/[locale]/(myapp)`. A pasta certa é `src/app/(myapp)`.
</Callout>

## 4. Usar no Studio [#4-usar-no-studio]

Reabre a página no Page Builder. O componente aparece no fim da paleta, em **Componentes Personalizados**, com o nome tirado da função (`TagEstado` → **Tag Estado**).

<StudioPreview imageName="customComponent" alt="Paleta de Widgets com a categoria Componentes Personalizados e o widget Tag Estado" />

Arrasta-o para a página como qualquer widget. No canvas é mostrado como um bloco com o nome — o resultado vê-se na aplicação a correr.

<StudioPreview imageName="dragDropComponent" alt="Tag Estado largado na zona de ações da Page Header, mostrado como bloco TagEstado" />

Na sidebar de edição, as props da assinatura aparecem em **Custom Properties** (`estado` → **Estado**, `mostrarPonto` → **Mostrar Ponto**). Como qualquer prop, podem ser ligadas a um estado pelo **⋮**.

<StudioPreview imageName="componentFunction" alt="Custom Properties do Tag Estado com Estado igual a pendente e Mostrar Ponto" />

<Callout type="warning" title="Os tipos das props ainda não são usados">
  O Studio mostra todas as props como texto, mesmo as booleanas ou com valores fixos (`mostrarPonto`, `estado`). Escreve o valor tal como no código (`true`, `pendente`).
</Callout>

A função registada fica disponível no [Código Personalizado](./customCode.mdx), em **Functions**, ao lado das funções criadas no Studio:

<StudioPreview imageName="customFunctionRegistada" alt="Painel Código Personalizado com a função registada listarPedidos realçada" />


# Header Slots

URL: /pt/docs/developer/studio/ide/header-slots

Como renderizar componentes próprios no cabeçalho iGRP — substituir a pesquisa, notificações ou definições, ou adicionar ações personalizadas.

# Header Slots [#header-slots]

Como renderizar **componentes próprios** no cabeçalho do template — substituindo a pesquisa integrada, as notificações ou o controlo de definições, ou adicionando slots inteiramente novos.

---

## O modelo [#o-modelo]

O cabeçalho é renderizado pelo framework, não pela tua app:

```
(igrp)/layout.tsx  [server]
  → IGRPLayoutFull            @igrp/framework-next          [server]
      → HeaderDataProvider    (dentro de <Suspense>)        [server]
            getHeaderData()   → flags, logo, URLs
            fetchCurrentUser() → utilizador autenticado
        → IGRPTemplateHeader  @igrp/framework-next-ui       ['use client']
```

Um **slot** entrega ao framework um nó para renderizar numa posição que ele gere. O framework continua a gerir **todos** os dados do cabeçalho — utilizador, logo, breadcrumbs, sidebar trigger. Um slot substitui *o que renderiza numa posição*, nunca *de onde vêm os dados*. Não abres mão de nada ao usá-lo.

---

## Os slots [#os-slots]

Passa-os como `headerSlots` em `IGRPLayoutFull`:

| Slot | Posição | Renderiza quando |
|---|---|---|
| `start` | Região esquerda, após o logo/título, antes dos breadcrumbs | passas um nó |
| `search` | Cluster direito, primeiro — substitui a command palette integrada | `showSearch: true` |
| `settings` | Cluster direito — substitui o link de definições integrado | `showSettings: true` |
| `notifications` | Cluster direito — substitui o dropdown de notificações integrado | `showNotifications: true` |
| `actions` | Cluster direito, após notificações, antes do theme switcher | passas um nó |

Dois estilos de controlo, e a diferença importa:

- `search`, `settings` e `notifications` ocupam posições que o framework já conhece, pelo que a flag `show*` correspondente em `getHeaderData()` decide se a posição renderiza **de todo**. Definir `showSearch: false` e passar um slot `search` não renderiza nada — a flag ganha.
- `start` e `actions` não têm flag. Passar o nó é o contrato completo.

As flags `show*` vivem em `src/igrp.template.config.ts` sob `layoutMockData.getHeaderData()`.

---

## Quick start [#quick-start]

```tsx
// src/app/(igrp)/layout.tsx
import { IGRPLayoutFull } from "@igrp/framework-next";

import { EnvBadge } from "@/components/header/env-badge";
import { MyNotifications } from "@/components/header/my-notifications";

export default async function Layout({ children }: { children: React.ReactNode }) {
  const config = await createConfig(layoutConfig);

  return (
    <IGRPLayoutFull
      config={config}
      headerSlots={{
        start: <EnvBadge />,
        notifications: <MyNotifications />,
      }}
    >
      {children}
    </IGRPLayoutFull>
  );
}
```

Os slots são **elementos** (`<MyThing />`), não referências a componentes (`MyThing`). Ver [Regras e armadilhas](#regras-e-armadilhas).

---

## Exemplo de referência: command palette no slot `search` [#exemplo-de-referência-command-palette-no-slot-search]

O template de referência implementa um slot real: a **command palette**, ligada através de `search`. O padrão divide-se em dois ficheiros:

| Ficheiro | Papel |
|---|---|
| `src/lib/header-search.ts` | **Servidor.** Resolve os menus da aplicação em dados serializáveis para os comandos. Usa `isAuthBypass()` — menus mock em bypass, `fetchMenusAction(appCode)` caso contrário. Retorna `[]` em falha. |
| `src/components/header/app-search.tsx` | **Cliente (`'use client'`).** Mapeia esses dados para `IGRPCommandItem[]`, constrói cada handler `onSelect`, e renderiza `IGRPTemplateCommandSearch`. |
| `src/app/(igrp)/layout.tsx` | Aguarda os comandos e passa o elemento como slot `search`. |

A ligação completa no layout resume-se a três linhas:

```tsx
const searchCommands = await getHeaderSearchCommands(config);

<IGRPLayoutFull
  config={config}
  headerSlots={{ search: <AppSearch commands={searchCommands} /> }}
>
```

<Callout type="info" title="Porquê dois ficheiros?">
`IGRPTemplateHeader` monta o `IGRPTemplateCommandSearch` sem prop `commands`, por isso `⌘K` / `Ctrl+K` abre uma palette vazia. O slot `search` é como a app lhe dá conteúdo.

A separação entre os dois ficheiros não é estilística: decidir se um URL é externo requer comparar com a origem atual, que só o browser conhece (`igrpIsExternalUrl` lê `window.location.origin`). O servidor passa `pageSlug` / `url` / `target` sem resolver, e o cliente decide no clique.
</Callout>

---

## Receita: substituir as notificações [#receita-substituir-as-notificações]

O dropdown integrado renderiza o que `getHeaderData()` retorna em `notifications` — um array estático. Para o alimentar com a tua própria API:

```tsx
// src/components/header/my-notifications.tsx
"use client";

import { useQuery } from "@tanstack/react-query";
import { IGRPTemplateNotifications } from "@igrp/framework-next-ui";

export function MyNotifications() {
  const { data } = useQuery({
    queryKey: ["notifications"],
    queryFn: fetchNotifications,
    refetchInterval: 60_000,
  });

  return (
    <IGRPTemplateNotifications
      notifications={data ?? []}
      onMarkAllRead={markAllRead}
    />
  );
}
```

Reutiliza `IGRPTemplateNotifications` para manter o visual do framework, ou renderiza algo inteiramente teu. `IGRPQueryProvider` já está montado acima de `IGRPLayoutFull`, por isso react-query funciona dentro de um slot.

<Callout type="info" title="Comportamento no nav-user">
`IGRPTemplateNavUser` tem uma entrada de link "Notificações" no seu dropdown, controlada pela mesma flag `showNotifications`. Quando passas um slot `notifications`, o framework **suprime** essa entrada — evitando dois pontos de entrada para o mesmo recurso. O teu slot **não** é ocultado em ecrãs pequenos; o sino integrado é (`hidden md:block`) precisamente porque o nav-user cobre mobile.
</Callout>

---

## Receita: adicionar ações próprias [#receita-adicionar-ações-próprias]

`actions` é o slot aberto. Compõe um fragment e controlas a ordem interna:

```tsx
headerSlots={{
  actions: (
    <>
      <HelpButton />
      <LanguageSwitcher />
      <IGRPAuthorization permission="manage_access">
        <AdminShortcut />
      </IGRPAuthorization>
    </>
  ),
}}
```

É um único `ReactNode`, não um array, por isso o framework nunca itera sobre nós que não keyed. Aplica os teus próprios permission-gates a cada item individualmente.

---

## Receita: badge junto ao logo [#receita-badge-junto-ao-logo]

`start` situa-se na região esquerda, entre o logo/título e os breadcrumbs, e recebe um separador automático quando algo o precede:

```tsx
headerSlots={{ start: <IGRPBadge variant="outline">STAGING</IGRPBadge> }}
```

É colocado **antes** dos breadcrumbs propositadamente: os breadcrumbs colapsam sob pressão de layout, e um slot depois deles seria o primeiro a desaparecer num viewport estreito.

---

## Slots servidor assíncronos [#slots-servidor-assíncronos]

Um elemento de slot pode ser produzido por um **Server Component assíncrono** — útil quando os dados estão atrás de um cliente server-only:

```tsx
async function TenantSwitcher() {
  const tenants = await fetchTenants(); // server-side
  return <TenantSwitcherClient tenants={tenants} />;
}

headerSlots={{ actions: <TenantSwitcher /> }}
```

Cada slot renderiza dentro da **sua própria `<Suspense>`** com um placeholder do tamanho de um ícone, por isso um slot lento não atrasa o logo, breadcrumbs ou o menu do utilizador. Para controlar o fallback, envolve o teu slot:

```tsx
headerSlots={{
  actions: (
    <Suspense fallback={<MySkeleton />}>
      <TenantSwitcher />
    </Suspense>
  ),
}}
```

---

## Regras e armadilhas [#regras-e-armadilhas]

**Passa elementos, não componentes.** `IGRPTemplateHeader` é um Client Component renderizado a partir de um Server Component. Elementos cruzam essa fronteira; funções não.

```tsx
headerSlots={{ search: <AppSearch /> }}  // ✅
headerSlots={{ search: AppSearch }}      // ❌ função não é serializável
```

**A mesma regra aplica-se às props que passas para dentro de um slot.** `IGRPCommandItem.onSelect` é um callback — um módulo *servidor* não pode construir os itens. Passa dados simples do servidor e constrói os handlers dentro do componente cliente.

**Navega com o router, não com `window.open`.** `router.push()` aplica `basePath` (`NEXT_PUBLIC_BASE_PATH`); `window.open()` não, por isso um path interno aberto dessa forma dá 404. Reserva `window.open` para URLs genuinamente noutro origin.

**Um `url` absoluto não é automaticamente externo.** Um item de menu pode endereçar uma página interna por URL (ex.: `/reports`). Decide com `igrpIsExternalUrl(url)` (ou um `target: "_blank"` explícito), nunca por ter `url` sem `pageSlug`.

**`typedRoutes` está ativo.** `next.config.ts` define `typedRoutes: true`, por isso `router.push()` é tipado contra rotas estaticamente conhecidas. Targets resolvidos de dados de runtime precisam de um cast:

```tsx
router.push(href as Parameters<typeof router.push>[0]);
```

**Slots são código client-boundary.** Qualquer coisa que importe do design system precisa de `'use client'`.

---

## O que não é injetável [#o-que-não-é-injetável]

| Não é slot | Porquê |
|---|---|
| Theme switcher | Comportamento do framework ligado ao `next-themes` |
| Nav-user menu | Ligado à sessão e ao sign-out; substituí-lo é como a UX de auth se parte |
| Logo, título, breadcrumbs, sidebar trigger | Configurados via flags de `getHeaderData()` e props `breadcrumbs` / `breadcrumbRouteLabels` |
| O cabeçalho como um todo | Não existe prop de substituição completa — isso entregaria-te a gestão de dados (`getHeaderData()`, `fetchCurrentUser()` e o re-seed do access-client que `HeaderDataProvider` faz). Usa slots. |

Para **ocultar** um elemento integrado em vez de substituir, muda a sua flag em `getHeaderData()` (`showSearch`, `showSettings`, `showNotifications`, `showThemeSwitcher`, `showUser`, `showBreadcrumb`, …).

---

## Preview mode [#preview-mode]

Os slots são indiferentes à autenticação — renderizam da mesma forma com `IGRP_PREVIEW_MODE=true` ou `AUTH_PROVIDER=none` e com backend real. Qualquer *dado* que um slot resolva é responsabilidade tua de ramificar, e `isAuthBypass()` de `@/lib/utils` é a única forma correta de verificar.

---

## Referência da API [#referência-da-api]

```ts
// @igrp/framework-next-ui
export type IGRPHeaderSlots = {
  start?: React.ReactNode;
  search?: React.ReactNode;
  notifications?: React.ReactNode;
  settings?: React.ReactNode;
  actions?: React.ReactNode;
};

// @igrp/framework-next
export type IGRPLayoutFullArgs = {
  readonly children: React.ReactNode;
  readonly config: IGRPConfigArgs;
  readonly showSidebar?: boolean;
  readonly breadcrumbs?: BreadcrumbItem[];
  readonly breadcrumbRouteLabels?: Record<string, string>;
  readonly headerSlots?: IGRPHeaderSlots;
};
```

Componentes reutilizáveis dentro de um slot, todos de `@igrp/framework-next-ui`:

| Componente | Props principais |
|---|---|
| `IGRPTemplateCommandSearch` | `commands: IGRPCommandItem[]` |
| `IGRPTemplateNotifications` | `notifications`, `notificationsUrl`, `onMarkAllRead` |
| `IGRPAuthorization` | `permission` |
| `usePermissions` | hook — lista de permissões do utilizador atual |


# Page Builder

URL: /pt/docs/developer/studio/ide/page-builder

O editor visual do iGRP Studio para páginas e componentes — Paleta de Widgets, arrastar e largar, barra de ações dos widgets e sidebar de edição.

O **Page Builder** é o editor visual do iGRP Studio — constróis a interface arrastando **widgets** da Paleta de Widgets para o canvas, e configuras props, estilos e interações de cada um. Abre-se a partir do [Page Manager](./page-manager.mdx) (**Abrir editor**), para **Páginas** e para **Componentes**.

<StudioPreview imageName="areaDeDesign" alt="Page Builder com a Paleta de Widgets à esquerda e a página Pedidos no canvas" />

No topo, o editor tem quatro vistas e o botão de gravar:

<Grid cols={2}>
  <Feature icon="Code" color="blue" title="Code">O código React gerado</Feature>
  <Feature icon="Eye" color="green" title="Design">O canvas visual, com arrastar e largar</Feature>
  <Feature icon="ListTree" color="teal" title="Tree">A árvore de widgets da página, com as propriedades de cada nó</Feature>
  <Feature icon="FileJson" color="purple" title="Json">A estrutura interna da página em JSON</Feature>
</Grid>

<Callout type="success" title="Grava com frequência">
  **Salvar** grava a página e regenera o código. Alterações por gravar perdem-se se fechares o separador.
</Callout>

---

## Barra lateral [#barra-lateral]

A barra à esquerda dá acesso aos painéis do editor:

<Grid cols={3}>
  <Feature icon="LayoutPanelLeft" color="primary" title="Paleta de Widgets">Os widgets disponíveis, por categoria — ver [Widgets](./components.mdx)</Feature>
  <Feature icon="SquareFunction" color="primary" title="Código Personalizado">Funções, estados e snippets da página — ver [Custom Code](./customCode.mdx)</Feature>
  <Feature icon="FileText" color="primary" title="Explorer">Os ficheiros da aplicação gerada</Feature>
  <Feature icon="Settings" color="primary" title="Configurações">Configurações do projeto</Feature>
  <Feature icon="GitBranch" color="primary" title="Git">Controlo de versões do projeto</Feature>
</Grid>

<StudioPreview imageName="barraLateral" alt="Barra lateral do Page Builder com a Paleta de Widgets selecionada" />

---

## Arrastar e largar [#arrastar-e-largar]

<Steps>
  <Step>Arrasta um widget da paleta para o canvas. As zonas que o aceitam mostram **DROP HERE COMPONENTS**.</Step>
  <Step>Alguns widgets já trazem filhos: o **Form** vem com um Headline e uma Grid, o **Modal Dialog** com cabeçalho, rodapé, conteúdo e trigger, a **Page Header** com uma zona de ações.</Step>
  <Step>Larga os campos e outros widgets dentro das zonas certas (por exemplo os inputs dentro da Grid do Form).</Step>
  <Step>Configura o widget na sidebar de edição.</Step>
</Steps>

---

## Barra de ações do widget [#barra-de-ações-do-widget]

Ao passar o rato sobre um widget no canvas aparece a sua barra de ações, com o nome do widget:

<Grid cols={2}>
  <Feature icon="Move" color="purple" title="Mover">Arrasta o widget para outra posição</Feature>
  <Feature icon="Copy" color="green" title="Duplicar">Duplica o widget</Feature>
  <Feature icon="Settings" color="blue" title="Configurações">Abre a sidebar de edição do widget</Feature>
  <Feature icon="Trash2" color="red" title="Eliminar">Remove o widget</Feature>
</Grid>

Os widgets com filhos acrescentam ações próprias — **Add Comp** (Table, Modal Dialog…) e **Binding Config** (Form, Table).

<StudioPreview imageName="acaoNoComponente" alt="Barra de ações do widget Input Text no canvas" />

---

## Sidebar de edição [#sidebar-de-edição]

A engrenagem de um widget abre a sidebar **Configurações**, com o nome técnico do widget (que podes mudar), **Trocar componente** (troca o widget por outro compatível, mantendo as props) e quatro separadores:

<Grid cols={2}>
  <Feature eyebrow="1" title="Propriedades">Props do widget — texto, label, placeholder, obrigatório, comportamento. Uma prop pode ser ligada a um estado: passa o rato sobre ela e usa o **⋮**.</Feature>
  <Feature eyebrow="2" title="Estilo">Margens, tipografia, cores, espaçamentos e responsividade.</Feature>
  <Feature eyebrow="3" title="Interações">**Trigger Element** — eventos (clique, *On load*, *On open*…) e a ação de cada um. **Regras** — visibilidade e permissões do nó.</Feature>
  <Feature eyebrow="4" title="Copiar">Copia as propriedades para outras páginas, componentes ou passos de processo.</Feature>
</Grid>

<StudioPreview imageName="sidebarDeEdicao" alt="Sidebar de edição do Input Text com as propriedades Label e Placeholder" />

<span id="use-client" />

<Callout type="info" title="Usar Client">
  O **Usar Client** da página — definido no [Page Manager](./page-manager.mdx#criar-uma-página) ao criá-la — muda-se também aqui: abre as configurações da página (engrenagem no canto superior direito do canvas).
</Callout>


# Page Manager

URL: /pt/docs/developer/studio/ide/page-manager

Como criar, listar e gerir Páginas, Componentes, Processos BPMN, Permissões e Bundles de um projeto Next.js no iGRP Studio — campos dos formulários, vistas e ações disponíveis.

O **Page Manager** é a vista geral de um projeto Next.js no iGRP Studio — abre quando clicas num projeto frontend no workspace. Daqui crias e organizas as **Páginas** (rotas da aplicação) e os **Componentes** (blocos reutilizáveis) e abre-os no [Page Builder](./page-builder.mdx). Dá também acesso aos processos BPMN, às permissões, aos bundles e às configurações do projeto.

<StudioPreview imageName="pageBuilderHome" alt="Vista geral do projeto Portal Demo com os separadores Páginas, BPMN, Permissões, Bundles e Configurações" />

O Page Manager organiza-se em cinco separadores:

<Grid cols={3}>
  <Feature icon="FileText" color="blue" title="Páginas">Páginas e componentes da aplicação</Feature>
  <Feature icon="Workflow" color="teal" title="BPMN">Processos de workflow associados ao projeto</Feature>
  <Feature icon="KeyRound" color="amber" title="Permissões">Catálogo de permissões do projeto</Feature>
  <Feature icon="Package" color="purple" title="Bundles">Pacotes de funcionalidade prontos a instalar</Feature>
  <Feature icon="Settings" color="neutral" title="Configurações">Configurações do projeto gerado</Feature>
</Grid>

<Callout type="info" title="Skills IGRP">
  No topo da vista, o Studio sugere instalar a skill `igrp-studio-metadata` no projeto, para melhorar a geração por agentes de IA. **Instalar skill** instala-a no projeto (também disponível no separador Bundles); o **×** esconde a sugestão.
</Callout>

---

## Páginas [#páginas]

O separador **Páginas** lista as páginas e componentes da aplicação. O contador por baixo do título mostra `X pages • Y subpages • Z components` e reflete a pesquisa ou o filtro ativos.

### Barra de ferramentas [#barra-de-ferramentas]

<Grid cols={2}>
  <Feature title="Filtro por grupo">
    **Todos os grupos**, um grupo existente (por exemplo `(auth)`) ou sem grupo. Só aparece quando o projeto tem route groups.
  </Feature>
  <Feature title="Pesquisa">
    **Pesquisar páginas…** por nome, path, descrição, tipo ou grupo. A pesquisa inclui sub-páginas e componentes filhos.
  </Feature>
  <Feature title="Ordenação">
    **Última modificação** (por defeito), data de criação ou nome. Alterar um filho atualiza a data do pai.
  </Feature>
  <Feature title="Vista">
    Cartões (grelha) ou tabela (path, tipo e sub-páginas numa linha).
  </Feature>
</Grid>

### Criar uma página [#criar-uma-página]

Clica em **Adicionar → Criar Nova Página**.

<StudioPreview imageName="criarPagina" alt="Diálogo Criar Nova Página com Título, Nome, Grupo, Path, Forçar Dinâmico e Usar Client" />

| Campo | |
|---|---|
| **Título da Página** <Badge color="red">Obrigatório</Badge> | Nome legível, mostrado no Studio. O **Nome** é preenchido a partir dele |
| **Nome da Página** <Badge color="red">Obrigatório</Badge> | Nome técnico do componente React — sem espaços nem hífenes (por exemplo `pedidos`) |
| **Grupo** (opcional) | Route group do Next.js: `(nome)/path`, que não aparece no URL. **Sem grupo**, um grupo existente ou **Novo grupo…** |
| **Path** <Badge color="red">Obrigatório</Badge> | Rota da página, **sem a barra inicial**: `pedidos`, `pedidos/[id]`, `docs/[[...slug]]` |
| **Forçar Dinâmico** | Desativa o pre-render estático da página |
| **Usar Client** | Junta a diretiva `"use client"` ao ficheiro gerado. Ativo por defeito; pode ser mudado depois no [Page Builder](./page-builder.mdx#use-client) |

Quando o path tem segmentos dinâmicos (`[id]`), o Studio gera os argumentos da página automaticamente.

<Callout type="warning" title="Path sem barra inicial">
  `/pedidos` é recusado com *Invalid Next.js path format*, apesar de a mensagem dar exemplos com barra. Escreve `pedidos`.
</Callout>

### Ações nas páginas [#ações-nas-páginas]

Cada página tem um menu **⋮** no cartão (ou na linha da tabela):

| Ação | O que faz |
|---|---|
| **Editar** | Edita os metadados da página (título, nome, path, flags) |
| **Duplicar** | Duplica a página — pede novo título, nome e path |
| **Abrir editor** | Abre a página no [Page Builder](./page-builder.mdx) |
| **Criar Sub-página** | Cria uma rota filha, já com o path da página como prefixo |
| **Criar componente da página** | Cria um componente associado a esta página |
| **Mover…** | Muda a página de pai, ou põe-a no nível de topo |
| **Eliminar** | Remove a página e o ficheiro de rota gerado (pede confirmação) |

**Criar Sub-página**, **Criar componente da página** e **Mover…** só existem em páginas. Os componentes globais têm **Editar**, **Duplicar**, **Abrir editor** e **Eliminar**.

#### Duplicar [#duplicar]

O diálogo de duplicação pré-preenche os campos com o sufixo `Copy` — título `Pedidos Copy`, nome `pedidosCopy`, path `pedidos-copy`. A cópia herda toda a estrutura da original.

#### Mover [#mover]

**Mover…** lista as páginas de topo como destino, mais a opção de passar para o nível de topo. Os destinos que criariam ciclos (a própria página ou descendentes) não aparecem. Só muda os metadados — não renomeia ficheiros.

---

## Componentes [#componentes]

### Criar um componente [#criar-um-componente]

Clica em **Adicionar → Criar Novo Componente** (ou **Criar componente da página** no menu de uma página).

<StudioPreview imageName="criarComponente" alt="Diálogo Criar Novo Componente com Título, Nome, Páginas, ícone, assinatura gerada e argumentos" />

| Campo | |
|---|---|
| **Título do Componente** <Badge color="red">Obrigatório</Badge> | Nome legível. O **Nome** é gerado a partir dele |
| **Nome do Componente** <Badge color="red">Obrigatório</Badge> | Identificador técnico — sem espaços nem hífenes |
| **Páginas** | Associa o componente a uma página: fica na pasta `components` dessa rota. Sem página, o componente é global |
| **Select Icon** | Ícone do componente na paleta |
| **Define Arguments** | Props do componente — nome, tipo, opcional, lista ou função (com os seus próprios parâmetros) |
| **Generated Component Signature** | Pré-visualização, em tempo real, da assinatura TypeScript gerada |

---

## BPMN [#bpmn]

Lista os processos de workflow do projeto (ficheiros `.bpmn`), para criar, abrir e gerir as definições diretamente no Studio. Precisa da stack Process no workspace — ver [Integrações](/docs/developer/workspace/integracoes).

## Permissões [#permissões]

Catálogo de permissões do projeto, usado nas regras de permissão das páginas e componentes — ver [Permissões no frontend](/docs/developer/framework/frontend/permissoes). O número ao lado do separador é o total de entradas.

## Bundles [#bundles]

Catálogo de bundles prontos a instalar no projeto — **Discover** mostra o catálogo, **Installed** o que já está no projeto. Os bundles agrupam-se em **Platform**, **Páginas**, **Formulários**, **Componentes**, **Processos**, **Migrações** e **Skills**, por exemplo:

| Bundle | Tipo | O que instala |
|---|---|---|
| Form CRUD (Template) | Formulário | Scaffold de formulário CRUD (ID, nome, descrição, ativo + guardar/cancelar), parametrizável |
| igrp-studio-metadata | Skill | A skill que ensina agentes de IA a escrever os ficheiros `.igrpstudio/` do projeto |
| Process Management UI | Platform | As páginas de Process Management (catch-all do processo, view, resumo, form partilhado) |
| Template Migrator | Migração | As migrações pendentes do template Next.js (equivale a `igrp migrate apply --yes --force`) |

Os bundles marcados como incompatíveis não servem para a versão do projeto.

<Callout type="info" title="Manter o projeto atualizado com o template">
O projeto gerado segue o template Next.js do iGRP. Quando sai uma versão nova do framework, as alterações chegam como **migrações**: aplica-as com `igrp migrate` no terminal — ver [Migrações do template](/docs/developer/studio/cli/migrate) — ou com o bundle **Template Migrator** acima.
</Callout>

<StudioPreview imageName="bundlesProjeto" alt="Separador Bundles do projeto com o catálogo Discover e os filtros por categoria" />

## Configurações [#configurações]

Configurações do projeto gerado: nome, descrição, versão do engine e opções de build. É o mesmo painel que abre pela barra lateral do Page Builder.


# Autenticação no workspace

URL: /pt/docs/developer/workspace/autenticacao

O Access Management como Authorization Server local, os modos Keycloak e Autentika, e a configuração obrigatória antes de arrancar.

# Autenticação no workspace [#autenticação-no-workspace]

O workspace segue o mesmo contrato de autenticação do <a href="/docs/developer/cloud">igrp-stack</a> em Kubernetes. O <strong>Access Management é o Authorization Server OAuth2</strong> local: emite os tokens, guarda as sessões e federa para um IdP a montante. As aplicações autenticam sempre <strong>nele</strong>, nunca directamente no provedor de identidade.

```text
Browser / Next.js
        |
        v
Access Management (issuer, JWKS, tokens e sessões)
        |
        +-- Keycloak  (igrp-compose.yaml)
        `-- Autentika (igrp-autentika-compose.yaml)
```

Todos os frontends usam `AUTH_PROVIDER=igrp-auth` e apontam o `IGRP_AUTH_ISSUER` ao Access Management. A escolha entre Keycloak e Autentika faz-se **apenas** pelas variáveis `IGRP_OAUTH_EXTERNAL_IDP_*` do Access Management.

---

## Os dois modos [#os-dois-modos]

São mutuamente exclusivos — nunca os dois ao mesmo tempo.

| | Keycloak | Autentika |
| --- | --- | --- |
| Ficheiros | `.env`, `igrp-compose.yaml`, `process/.env_process`, `process/igrp-process-compose.yaml` | `.igrp-autentika.env`, `igrp-autentika-compose.yaml`, `process/.env_process_autentika`, `process/igrp-process-autentika-compose.yaml` |
| `AUTH_PROVIDER` | `igrp-auth` | `igrp-auth` |
| IdP externo | `IGRP_OAUTH_EXTERNAL_IDP_*` do realm Keycloak local | `IGRP_OAUTH_EXTERNAL_IDP_*` do issuer Autentika |

Os composes dos projetos são **iguais nos dois modos** — só muda o env-file que lhes passas.

---

## O que mudou da v1 para a v2 [#o-que-mudou-da-v1-para-a-v2]

| Área | v1 | v2 |
| --- | --- | --- |
| Access Management | cliente/resource server do IdP | **Authorization Server** com `IGRP_OAUTH_ISSUER`, cliente seeded e service account |
| IdP externo | `IGRP_KEYCLOAK_*` / `AUTENTIKA_*` no frontend | `IGRP_OAUTH_EXTERNAL_IDP_*` só no Access Management |
| JWT | issuer do Keycloak/Autentika | `AUTH_JWT_ISSUER` = issuer público do Access Management |
| M2M | token estático (`IGRP_M2M_TOKEN`) | `client_credentials` por serviço |
| Persistência OAuth | tabelas da aplicação | tarefa `-sas-schema` cria `oauth2_authorization*` de forma idempotente |
| Base de dados do AM | `POSTGRES_*` | `DATABASE_URL` + `SPRING_DATASOURCE_*`, `SPRING_FLYWAY_ENABLED=false`, DDL `update` |
| Redis | `SPRING_DATA_REDIS_*` | `REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD` |
| Process APIs | token estático, issuer do IdP | issuer do AM, `SPRING_APPLICATION_NAME` = client ID, M2M `client_credentials` |
| Proxy | prefixo `/gateway-api` removido | o Nginx preserva `X-Forwarded-Prefix` para o AM construir URLs públicas correctas |

<Callout type="warning" title="Não há token estático">
Todo o M2M é <code>client_credentials</code>. As variáveis <code>IGRP_ACCESS_M2M_SYNC_TOKEN</code> e <code>IGRP_M2M_TOKEN</code> são resíduo da v1 e não são usadas por nenhum compose.
</Callout>

---

## Configuração obrigatória antes de arrancar [#configuração-obrigatória-antes-de-arrancar]

### 1. O segredo do cliente seeded [#1-o-segredo-do-cliente-seeded]

`ACCESS_MANAGEMENT_OAUTH_CLIENT_SECRET` tem de ser o **mesmo valor** em `.env`, `.igrp-autentika.env` e nos dois env-files do Process. É o segredo do cliente usado pelo login OIDC de todos os frontends — se divergir, o login falha.

### 2. As chaves RSA [#2-as-chaves-rsa]

Sem `IGRP_OAUTH_PUBLIC_KEY` / `IGRP_OAUTH_PRIVATE_KEY`, o Access Management usa as chaves de desenvolvimento embutidas na imagem.

<Callout type="danger" title="As chaves embutidas são públicas">
Só aceitável localmente. Num ambiente partilhado, monta um par RSA estável e aponta estas duas variáveis a <code>file:/.../public.pem</code> e <code>file:/.../private.pem</code>.
</Callout>

### 3. O hostname [#3-o-hostname]

`DOCKER_IP` é o hostname público do issuer e dos callbacks, e tem de resolver no browser. Todo o acesso é `${DOCKER_IP}:${NGINX_HTTP_PORT}` — nunca um nome ou IP de contentor.

### 4. O cookie de sessão [#4-o-cookie-de-sessão]

O workspace corre em **HTTP simples**, logo `ACCESS_MANAGEMENT_SESSION_COOKIE_SECURE=false`.

<Callout type="warning" title="Um cookie Secure sobre HTTP é descartado">
O browser deita-o fora, cria sessão nova a cada pedido, e o login falha com <code>authorization_request_not_found</code>. Passa a <code>true</code> assim que servires por HTTPS.
</Callout>

### 5. Os clientes OAuth [#5-os-clientes-oauth]

São registados automaticamente pelas tarefas da stack raiz — não há passo manual. Ver [Registo de clientes OAuth](./registo-de-clientes.mdx).

---

## O defeito do primeiro arranque [#o-defeito-do-primeiro-arranque]

Numa base de dados **nova**, o Access Management arranca a reportar sucesso mas fica funcionalmente incompleto. É reproduzível a 100% e vale a pena perceber, porque o sintoma aparece longe da causa.

### O que acontece [#o-que-acontece]

O `AuthorizationSyncService` tenta registar as suas próprias 43 permissões e falha com `404 IGRP_AUTH_RESOURCE_NOT_FOUND_BY_NAME`, porque o *resource* `igrp-access-management` de que precisa **ainda não existe** — só é criado cerca de 3,6 segundos mais tarde, pelo `DefaultOAuthClientBootstrap`.

O erro é registado como `ERROR` mas **engolido**: o boot continua e o serviço reporta `Started ... in 29.929 seconds`. Em consequência, o `DefaultServiceAccountBootstrap` não encontra nenhuma das 12 permissões que tem de conceder, e concede zero.

```text
Permission 'igrp.client.list' not found in t_permission — skipping grant. Will retry on next boot.  (×12)
granted=0, alreadyPresent=0, missingPermissionRow=12
```

### O sintoma [#o-sintoma]

Um serviço aparentemente saudável — `/actuator/health` → `UP` — cujo service account seeded devolve **403 em `GET /api/clients`**. Por arrasto, nenhum backend ou frontend consegue ser provisionado com credenciais M2M, e o utilizador vê apenas a página de erro do nginx.

### Como o workspace o contorna [#como-o-workspace-o-contorna]

A tarefa `-ready` (`wait-for-catalog.sh`) espera pelo health do Access Management, deteta este estado e **reinicia o serviço uma vez**, pelo socket do Docker. No segundo boot o resource já existe, o catálogo é semeado e o service account fica com as 12 permissões (`granted=12, missingPermissionRow=0`). Um `docker compose up -d` puro basta.

<Callout type="danger" title="Montar /var/run/docker.sock dá controlo do daemon">
Na prática, root na máquina. É deliberado para desenvolvimento local e <strong>nunca</strong> aceitável em ambiente partilhado ou em produção. Aí põe <code>ACCESS_MANAGEMENT_READY_AUTO_RESTART=false</code>, remove o mount e faz o restart de fora.
</Callout>

<Callout type="note" title="Porque é que a prontidão é externa">
A imagem do Access Management é <strong>distroless</strong>: o <code>ENTRYPOINT</code> é <code>java -jar /app/app.jar</code>, sem shell nem <code>curl</code>. Um <code>command:</code> seria entregue à JVM como argumentos e nunca correria, e um healthcheck <code>CMD-SHELL</code> falha sempre com <code>stat /bin/sh: no such file or directory</code>. Daí a tarefa externa.
</Callout>

---

## Keycloak já existente [#keycloak-já-existente]

O import de realm só se aplica quando o realm ainda não existe. Em volumes já inicializados, acrescenta à mão ao cliente `access-management` o redirect URI da federação:

```text
http://<DOCKER_IP>:<PORTA>/gateway-api/<ws>-access-management/login/oauth2/code/external-idp
```

<Callout type="tip" title="Evita apagar volumes para forçar o reimport">
Actualizar pelo Admin Console preserva os utilizadores.
</Callout>

---

## Verificação [#verificação]

```bash
curl http://demoteste-igrp:2575/gateway-api/demoteste-access-management/.well-known/openid-configuration
curl http://demoteste-igrp:2575/api/auth/providers
```

O segundo endpoint deve expor `igrp-auth` — **não** `keycloak` nem `autentika`.

Nos backends, `SPRING_APPLICATION_NAME` tem de ser **exactamente** o respectivo client ID, senão o sync de permissões falha com recurso não encontrado. Ver [Registo de clientes OAuth](./registo-de-clientes.mdx#nomes-três-coisas-diferentes).

---

## Limitações conhecidas [#limitações-conhecidas]

| Limitação | Detalhe |
| --- | --- |
| Login directo numa UI sem sessão prévia dá `401` no `/oauth2/authorize` | É defeito da aplicação Access Management, não do Compose. Até sair imagem corrigida, valida pelo caminho App Center → AM → IdP |
| `GET /api/applications` devolve 403 ao service account seeded | Falta-lhe as permissões `igrp.application.*`. As aplicações **estão** registadas — confirmável em `t_application`; é só a leitura pela API que falha |
| O discovery anuncia `scopes_supported: ["openid"]` | Apesar de o cliente seeded ter também `email` e `profile` |
| As variáveis `NEXT_PUBLIC_*` são embutidas no `next build` | Uma imagem construída sem os valores locais precisa de rebuild com esses build args |
| O `.igrpstudio/workspace.json` não modela o schema SAS, a federação v2 nem os clientes OAuth | **Os ficheiros Compose são a fonte operacional** |


# Criar workspace

URL: /pt/docs/developer/workspace/criar-workspace

O workspace é a plataforma iGRP a correr no teu computador, em Docker, mais os teus projetos. Cria-o com o assistente do Studio em três passos.

Um **workspace** é uma pasta com a plataforma iGRP completa — autenticação, Access Management, Application Center, gateway, bases de dados, storage — descrita em Docker Compose, mais a pasta `projects/` onde vão viver os teus projetos. O Studio gere-o por ti: cria-o, arranca os serviços e mostra o seu estado.

<Callout type="info" title="Quando aparece">
  No [primeiro arranque](/docs/developer/getting-started/primeiro-arranque), **Enter Studio Workspace** abre este assistente se ainda não tiveres nenhum workspace. Depois disso, cria outros pelo seletor de workspaces da barra lateral → **Adicionar workspace** (`Cmd/Ctrl + N`).
</Callout>

## O assistente [#o-assistente]

<Steps>
  <Step>
    ### Criar Novo Workspace [#criar-novo-workspace]

    | Campo | O que é |
    |---|---|
    | **Nome** | Nome legível do workspace (por defeito `My Workspace`) |
    | **Slug** | Identificador em minúsculas, dígitos e hífens — é também o **hostname** da plataforma local |
    | **Descrição** | Opcional |
    | **Localização** | Pasta onde o workspace é criado — tem de estar **vazia**; os ficheiros são escritos diretamente nela |

    <StudioPreview imageName="criarWorkspace" alt="Passo Criar Novo Workspace, com Nome, Slug, Descrição e Localização" />
  </Step>
  <Step>
    ### Docker Orchestration [#docker-orchestration]

    A stack principal é sempre incluída. Aqui escolhes as **stacks opcionais**:

    <Grid cols={2}>
      <Feature icon="Activity" color="cyan" title="Monitoring">
        Observabilidade: OpenTelemetry Collector, Prometheus, Loki, Tempo e Grafana.
      </Feature>
      <Feature icon="Workflow" color="purple" title="Process">
        Process Management e Process Studio (API + UI) — necessária para [processos BPMN](/docs/developer/process).
      </Feature>
    </Grid>

    Não precisas de decidir já: ambas podem ser acrescentadas depois, em **Serviços → Docker → Add Stack**.

    <StudioPreview imageName="criarWorkspaceDocker" alt="Passo Docker Orchestration, com as stacks opcionais de monitoring e process" />
  </Step>
  <Step>
    ### DNS & Platform Access [#dns--platform-access]

    A plataforma responde em `http://<slug>:<porta>`. Acrescenta o slug ao ficheiro `hosts` do teu sistema (o assistente mostra o comando para copiar):

    ```text
    127.0.0.1  <slug>
    ```

    Clica em **Criar Workspace**. Se o Docker estiver a correr, o Studio arranca logo a stack principal; se não, avisa e podes arrancá-la mais tarde.

    <StudioPreview imageName="criarWorkspaceDns" alt="Passo DNS & Platform Access, com a linha a acrescentar ao ficheiro hosts e os endereços da plataforma" />
  </Step>
</Steps>

O workspace abre no separador **Projetos**, ainda vazio:

<StudioPreview imageName="homeWorkspace" alt="Workspace recém-criado no separador Projetos, sem projetos" />

## O que fica no disco [#o-que-fica-no-disco]

<Files>
  <Folder name="<localização>" defaultOpen>
    <Folder name=".igrpstudio">
      <File name="workspace.json" />
    </Folder>
    <File name="igrp-compose.yaml" />
    <File name=".env" />
    <File name="nginx.conf" />
    <Folder name="monitoring" />
    <Folder name="process" />
    <Folder name="projects" />
  </Folder>
</Files>

| Ficheiro / pasta | Papel |
|---|---|
| `igrp-compose.yaml` + `.env` | Stack principal: nginx, PostgreSQL, Keycloak, Access Management, Application Center, gateway, Eureka, MinIO, Redis, pgAdmin |
| `monitoring/`, `process/` | Stacks opcionais (só existem se as escolheste) |
| `projects/` | Os teus projetos — ver [Primeiro projeto](./primeiro-projeto.mdx) |
| `.igrpstudio/workspace.json` | Identifica a pasta como workspace — é o que o Studio procura em **Open Workspace** |

Cada workspace novo recebe uma porta HTTP própria (a partir de `2575`), escrita no `.env`, para poderes ter vários lado a lado.

<NextStep href="/docs/developer/workspace/executar-workspace" title="Executar o workspace">
  Arrancar e parar os serviços e abrir a plataforma no browser.
</NextStep>


# Executar o workspace

URL: /pt/docs/developer/workspace/executar-workspace

Arrancar e parar os serviços da plataforma no separador Serviços do Studio, acompanhar os logs e abrir a plataforma local no browser.

Ao abrir um workspace, o Studio mostra três separadores: **Projetos**, **Serviços** e **Configurações**. Os serviços da plataforma gerem-se em **Serviços**.

<Callout type="info" title="Docker">
  Os serviços correm em Docker — o Docker Desktop (ou o daemon, em Linux) tem de estar a correr.
</Callout>

## Serviços [#serviços]

O separador **Serviços** tem três vistas:

| Vista | O que mostra |
|---|---|
| **Visão Geral** | Os serviços agrupados em **IGRP Stack**, **Monitoring**, **Process** e **Project**, com o estado de cada um |
| **Diagrama** | A arquitetura do workspace e as dependências entre serviços |
| **Docker** | Os ficheiros Compose do workspace e o menu **Add Stack** |

<Tabs>
  <TabItem value="overview" label="Visão Geral">
    <StudioPreview imageName="servicosOverview" alt="Vista Visão Geral do separador Serviços, com a IGRP Stack agrupada por tipo de serviço" />
  </TabItem>
  <TabItem value="diagram" label="Diagrama">
    <StudioPreview imageName="servicosDiagrama" alt="Vista Diagrama do separador Serviços, com os serviços e as suas dependências" />
  </TabItem>
  <TabItem value="docker" label="Docker">
    <StudioPreview imageName="servicosDocker" alt="Vista Docker do separador Serviços, com o igrp-compose.yaml e os ficheiros das stacks opcionais" />
  </TabItem>
</Tabs>

<Steps>
  <Step>
    ### Arrancar [#arrancar]

    Em **Visão Geral**, o botão ▶ de cada grupo (*Iniciar todos os serviços deste grupo*) arranca o grupo inteiro. Começa pela **IGRP Stack** — as outras stacks e os projetos dependem dela.
  </Step>
  <Step>
    ### Acompanhar [#acompanhar]

    A saída do Docker aparece na consola **Depurar** da barra inferior do Studio. Cada serviço tem um menu com **Start/Stop**, **Edit Service** e **Open in Browser**.
  </Step>
  <Step>
    ### Parar [#parar]

    O botão ■ do grupo (*Parar todos os serviços deste grupo*) para o grupo; o menu de cada serviço para só esse serviço.
  </Step>
</Steps>

<Callout type="tip" title="Acrescentar uma stack depois">
  Se não escolheste Monitoring ou Process ao criar o workspace, abre **Serviços → Docker → Add Stack** — o Studio instala e arranca a stack.
</Callout>

## A plataforma no browser [#a-plataforma-no-browser]

Com a IGRP Stack a correr, a plataforma responde em `http://<slug>:<porta>` (a porta do workspace está no `.env`; o primeiro workspace usa `2575`):

| Caminho | Serviço |
|---|---|
| `/` | Application Center — gestão de aplicações, menus, utilizadores e perfis |
| `/auth` | Keycloak |
| `/pgadmin` | pgAdmin |
| `/minio` | Consola do MinIO |
| `/eureka` | Registo de serviços |
| `/grafana` | Grafana (stack Monitoring) |
| `/apps/<projeto>` | Os teus frontends |
| `/gateway-api/<projeto>` | Os teus backends, via gateway |

## Pela linha de comandos [#pela-linha-de-comandos]

Sem o Studio, cada stack arranca com `docker compose`, a partir da pasta do workspace — começando sempre pela stack principal:

```bash
docker compose --env-file .env -f igrp-compose.yaml up -d
docker compose --env-file .env -f igrp-compose.yaml ps
```

Como as stacks se ligam e a ordem de arranque estão na [Visão geral da plataforma](./plataforma.mdx); os comandos e serviços de cada uma, nas páginas [Stack principal](./stack-principal.mdx), [Stack Process](./stack-process.mdx), [Stack Monitoring](./stack-monitoring.mdx) e [Projetos](./projetos.mdx).

<NextStep href="/docs/developer/workspace/primeiro-projeto" title="Primeiro projeto">
  Com a plataforma a correr, cria o teu primeiro projeto frontend ou backend.
</NextStep>


# Integrar com a plataforma

URL: /pt/docs/developer/workspace/integracoes

Como um projeto do workspace se liga ao Access Management (automático), aos processos BPMN (stack Process) e às notificações (integração manual).

Um projeto criado no Studio já nasce ligado à plataforma do workspace. O que muda de serviço para serviço é **quanto é automático**:

| Serviço | Integração | O que fazes |
|---|---|---|
| Access Management | Automática | Declarar permissões e geri-las no Application Center |
| Processos (BPMN) | Requer a stack Process | Ativar a stack e configurar a ligação no designer BPMN |
| Notificações | Manual | Seguir o guia de integração do serviço |

## Access Management [#access-management]

Não há nada a ativar:

<Steps>
  <Step>
    ### Cliente OAuth [#cliente-oauth]

    A cada arranque da stack principal, uma tarefa *one-shot* descobre os projetos em `projects/` e regista um cliente OAuth por projeto (e os callbacks de login dos frontends), escrevendo os segredos nos env-files. Um projeto novo é apanhado no **próximo arranque da stack principal** — ver [Registo de clientes OAuth](./registo-de-clientes.mdx).
  </Step>
  <Step>
    ### Configuração injetada [#configuração-injetada]

    No **Deploy Docker**, o Studio passa ao projeto o endereço do Access Management, o issuer dos tokens, o código e o caminho da aplicação e as credenciais M2M (`IGRP_ACCESS_MANAGEMENT_API`, `AUTH_JWT_ISSUER`, `IGRP_APP_CODE`, `IGRP_APP_BASE_PATH`, `IGRP_M2M_*`). A sincronização (`IGRP_SYNC_ACCESS`) vem ligada por defeito.
  </Step>
  <Step>
    ### Permissões e menus [#permissões-e-menus]

    O projeto declara as suas permissões e, ao arrancar, sincroniza-as (e os menus, nos frontends) com o Access Management:

    - **Frontend** — catálogo de permissões do projeto Next.js → [Permissões no Frontend](/docs/developer/framework/frontend/permissoes)
    - **Backend** — `@IgrpPermission` no código Spring Boot → [Permissões no Backend](/docs/developer/framework/backend/permissoes)
  </Step>
  <Step>
    ### Atribuir acessos [#atribuir-acessos]

    Abre o **Application Center** (`http://<slug>:<porta>/`) para atribuir as permissões a perfis e departamentos — ver [Gestão de Acessos](/docs/utilizador/access-manager/application-center).
  </Step>
</Steps>

## Processos (BPMN) [#processos-bpmn]

<Steps>
  <Step>
    ### Ativar a stack Process [#ativar-a-stack-process]

    Escolhe **Process** ao [criar o workspace](./criar-workspace.mdx), ou acrescenta-a depois em **Serviços → Docker → Add Stack → Process Stack**. Arranca o Process Management e o Process Studio (API + UI).
  </Step>
  <Step>
    ### Ligar o designer BPMN [#ligar-o-designer-bpmn]

    No designer BPMN do Studio, cria uma configuração de ligação: **Configuration Name**, **URL** e **Base Path** do Process Studio, **Access Token** (opcional) e marca-a como **Active Configuration**.
  </Step>
  <Step>
    ### Modelar e publicar [#modelar-e-publicar]

    Desenha o processo, publica-o no Process Management e liga os passos a formulários — ver [Processos BPMN](/docs/developer/process).
  </Step>
</Steps>

## Notificações [#notificações]

O workspace ainda não inclui o serviço de notificações nem o integra a partir do Studio. Para enviar notificações (email, etc.) a partir de um projeto, integra-o manualmente:

<Grid cols={2}>
  <Feature icon="Bell" color="amber" title="Guia de integração" href="/docs/developer/notification/integracao">
    Como ligar uma aplicação ao serviço de notificações — API REST ou SDKs Java e TypeScript.
  </Feature>
  <Feature icon="Mail" color="blue" title="Access Management → Notificações" href="/docs/developer/access-management/notification-service">
    Delegar os emails de convite e OTP do Access Management ao serviço de notificações.
  </Feature>
</Grid>


# Visão geral

URL: /pt/docs/developer/workspace/plataforma

A plataforma iGRP a correr em Docker no teu computador — a pasta do workspace, as quatro stacks, como se ligam, a ordem de arranque e os dois modos de autenticação.

Um workspace criado pelo Studio traz a **plataforma iGRP completa** descrita em Docker Compose. Não é uma aplicação: é uma plataforma local que aloja vários serviços e os teus projetos em conjunto. Podes geri-la pelo [separador Serviços do Studio](./executar-workspace.mdx) ou diretamente com `docker compose`.

## As quatro stacks [#as-quatro-stacks]

<Grid cols={2}>
  <Feature icon="Layers" color="blue" title="Stack principal" eyebrow="Sempre" href="/docs/developer/workspace/stack-principal">
    `igrp-compose.yaml` — nginx, PostgreSQL, Keycloak, Access Management, Application Center, gateway, Eureka, MinIO, Redis, pgAdmin.
  </Feature>
  <Feature icon="Workflow" color="purple" title="Stack Process" eyebrow="Opcional" href="/docs/developer/workspace/stack-process">
    `process/` — APIs e UIs de Process Management e Process Studio.
  </Feature>
  <Feature icon="Activity" color="cyan" title="Stack Monitoring" eyebrow="Opcional" href="/docs/developer/workspace/stack-monitoring">
    `monitoring/` — OpenTelemetry Collector, Prometheus, Loki, Tempo, Grafana, cAdvisor.
  </Feature>
  <Feature icon="FolderGit2" color="green" title="Projetos" eyebrow="Os teus" href="/docs/developer/workspace/projetos">
    `projects/<projeto>/` — um compose por projeto, ligado à stack principal.
  </Feature>
</Grid>

## A pasta do workspace [#a-pasta-do-workspace]

```text
<workspace>/
|-- .igrpstudio/                    # workspace.json, scripts de inicialização, realm do Keycloak
|-- igrp-compose.yaml               # stack principal, modo Keycloak
|-- igrp-autentika-compose.yaml     # stack principal, modo Autentika
|-- .env                            # ambiente da stack principal, modo Keycloak
|-- .igrp-autentika.env             # ambiente da stack principal, modo Autentika
|-- nginx.conf                      # reverse proxy central
|-- redis.conf
|-- process/                        # stack Process (se escolhida)
|-- monitoring/                     # stack Monitoring (se escolhida)
|-- projects/                       # os teus projetos
`-- logs/
```

<Callout type="warning" title="Os env-files têm segredos em texto simples">
  `projects/` está no `.gitignore`; os env-files da raiz **não**. Revê-os antes de versionar ou partilhar o workspace.
</Callout>

## Como se ligam [#como-se-ligam]

Todas as stacks partilham a mesma rede Docker e a infraestrutura da stack principal. Todo o tráfego HTTP entra pelo **nginx**, em `http://<slug>:<porta>`:

<StackArchitecture variant="workspace" />

| Caminho | Destino |
|---|---|
| `/` | Application Center |
| `/auth` | Keycloak |
| `/apps/<app>` | frontends (os teus e os da stack Process) |
| `/gateway-api/<serviço>` | APIs Spring, pelo gateway |
| `/pgadmin`, `/minio`, `/eureka`, `/grafana` | ferramentas |

As APIs nunca são expostas diretamente: o nginx encaminha `/gateway-api/*` para o **gateway**, que resolve o serviço pelo **Eureka**. Um backend novo ganha rota ao registar-se no Eureka, sem mexer no `nginx.conf`. Num cluster é igual, mas o gateway descobre os serviços pelo Kubernetes e as entradas são três domínios — ver [Cloud › Visão geral](/docs/developer/cloud).

<Callout type="tip" title="Sempre pelo hostname do workspace">
  Acede a tudo por `<slug>:<porta>` (o `DOCKER_IP` e a `NGINX_HTTP_PORT` do `.env`) — nunca pelo nome ou IP de um contentor. O hostname entra no issuer dos tokens e nos callbacks de login.
</Callout>

## A ordem de arranque [#a-ordem-de-arranque]

<Steps>
  <Step>
    ### Stack principal [#stack-principal]
    Primeiro, sempre. Cria a rede, as bases de dados e regista os clientes OAuth de que tudo o resto depende.
  </Step>
  <Step>
    ### Projetos [#projetos]
    Só os que precisares.
  </Step>
  <Step>
    ### Process e Monitoring [#process-e-monitoring]
    Extensões opcionais, por esta ordem.
  </Step>
</Steps>

No Studio, é o botão ▶ de cada grupo em **Serviços**; pela linha de comandos, os comandos estão na página de cada stack. Para parar, a ordem inversa.

## Modos de autenticação [#modos-de-autenticação]

O workspace suporta dois fornecedores de identidade, **mutuamente exclusivos**. Em ambos, as aplicações autenticam no Access Management local — nunca diretamente no fornecedor:

| Modo | Stack principal | Stack Process |
|---|---|---|
| **Keycloak** (por defeito) | `.env` + `igrp-compose.yaml` | `.env_process` + `igrp-process-compose.yaml` |
| **Autentika** | `.igrp-autentika.env` + `igrp-autentika-compose.yaml` | `.env_process_autentika` + `igrp-process-autentika-compose.yaml` |

Detalhes em [Autenticação](./autenticacao.mdx).

## Regras a preservar [#regras-a-preservar]

Se adaptares este workspace, mantém o modelo: uma stack principal para a infraestrutura partilhada; stacks opcionais separadas; projetos em `projects/` com o seu próprio compose; frontends em `/apps/<projeto>`; APIs pelo gateway com descoberta no Eureka; valores operacionais em env-files; um par compose/env por modo de autenticação.


# Primeiro projeto

URL: /pt/docs/developer/workspace/primeiro-projeto

Criar o primeiro projeto no workspace — um frontend Next.js ou um backend Spring Boot — e pô-lo a correr ao lado da plataforma.

Com o workspace a correr, cria o teu primeiro projeto no separador **Projetos**. O Studio gera o código, liga-o à plataforma e põe-no a correr em Docker.

## Criar o projeto [#criar-o-projeto]

Em **Projetos**, clica em **Novo Projeto**. Para um projeto que já existe, o menu **⋮** ao lado tem **Abrir Projeto** e **Clonar Projeto**.

<Steps>
  <Step>
    ### Tipo e gerador [#tipo-e-gerador]

    Escolhe o **Tipo de projeto** e o **Gerador**:

    <Grid cols={3}>
      <Feature icon="Layout" color="green" title="Frontend → NextJs">
        Aplicação web com o Page Builder, a correr em `/apps/<projeto>`.
      </Feature>
      <Feature icon="Server" color="blue" title="Backend → Spring Boot">
        API REST com o Model Designer, exposta em `/gateway-api/<projeto>`.
      </Feature>
      <Feature icon="Sparkles" color="purple" title="Especificação → AI Specification">
        Documentar e prototipar um projeto com IA, antes de gerar código.
      </Feature>
    </Grid>

    Os restantes geradores (Vue, Angular, .NET, Django, Go) aparecem marcados como **Em breve**.

    <StudioPreview imageName="novoProjetoTipo" alt="Passo Framework do Novo Projeto, com os tipos Frontend, Backend e Especificação e os geradores" />
  </Step>
  <Step>
    ### Configuração [#configuração]

    | Campo | |
    |---|---|
    | **Nome do Projeto** | Só letras, números e espaços (por exemplo `Portal Demo`) — hífens e outros símbolos são recusados |
    | **Descrição** | Opcional |
    | **Local do projecto** | **Dentro do workspace** — em `projects/<nome>` (recomendado) · **Local personalizado** — numa pasta à tua escolha, útil em monorepos |

    Campos de cada gerador:

    <Tabs>
      <TabItem value="next" label="NextJs">
        **Nome da Aplicação** — o nome técnico da app (por exemplo `portal-demo`).

        <StudioPreview imageName="njsProjectConfig" alt="Configuração de um projeto NextJs, com Nome do Projeto, Local do projecto e Nome da Aplicação" />
      </TabItem>
      <TabItem value="spring" label="Spring Boot">
        | Campo | Por defeito |
        |---|---|
        | Grupo / Artefato | `cv.igrp` / obrigatório (por exemplo `servico-demo`) |
        | DB Engine | PostgreSQL (ou MySQL, Oracle) |
        | Structure Style | Technical (ou Domain-Driven Design) |
        | Features | Observabilidade, Entity Revision, Graal VM (opcionais) |
        | Dependências | Dependências Spring adicionais — as do iGRP já são incluídas |

        <StudioPreview imageName="sbProjectConfig" alt="Configuração de um projeto Spring Boot, com Grupo, Artefato, DB Engine, Structure Style e Features" />
      </TabItem>
    </Tabs>
  </Step>
  <Step>
    ### Criar [#criar]

    Clica em **Criar Projeto**. O Studio gera o código e abre o projeto: um frontend abre na visão geral com **Páginas**, **BPMN**, **Permissões**, **Bundles** e **Configurações**; um backend abre no designer de **APIs**.

    <StudioPreview imageName="njsVerProjetoCriado" alt="Projeto NextJs acabado de criar, com os separadores Páginas, BPMN, Permissões, Bundles e Configurações" />
  </Step>
</Steps>

## Pô-lo a correr [#pô-lo-a-correr]

No cartão do projeto, em **Projetos**, o menu **⋮** tem **Deploy Docker**, que constrói a imagem e arranca o projeto ligado à stack principal (`docker compose up -d --build` do compose do projeto). Depois, **Abrir no Navegador**:

<StudioPreview imageName="projetoMenu" alt="Menu do cartão do projeto com Edit project, Abrir no Navegador, Deploy Docker e Remover Projeto" />

| Tipo | URL |
|---|---|
| Frontend | `http://<slug>:<porta>/apps/<projeto>` |
| Backend | `http://<slug>:<porta>/gateway-api/<projeto>` |

O projeto passa a aparecer no grupo **Project** do separador [Serviços](./executar-workspace.mdx), onde se para e arranca como os restantes serviços.

## Continuar a construir [#continuar-a-construir]

<Grid cols={2}>
  <Feature icon="LayoutTemplate" color="green" title="Frontend" href="/docs/developer/studio/ide/page-builder">
    Page Builder, Page Manager, widgets, custom code.
  </Feature>
  <Feature icon="Database" color="blue" title="Backend" href="/docs/developer/studio/backend/model-designer">
    Model Designer, Diagrama ER, DDD, Run Preview, Testing.
  </Feature>
</Grid>

Os detalhes de como os projetos se ligam à plataforma (env-files, rotas, armadilhas) estão em [Projetos](./projetos.mdx).

<NextStep href="/docs/developer/workspace/integracoes" title="Integrar com a plataforma">
  Permissões e menus no Access Management, processos BPMN e notificações.
</NextStep>


# Projetos

URL: /pt/docs/developer/workspace/projetos

projects/ — como os teus projetos correm ao lado da plataforma em Docker — um compose por projeto, arranque, rotas, contrato de env e armadilhas.

Cada projeto em <code>projects/</code> (criado no Studio — ver [Primeiro projeto](./primeiro-projeto.mdx)) tem o seu próprio compose, `igrp-compose-<projeto>.yaml`, e liga-se à rede e à infraestrutura da [stack principal](./stack-principal.mdx). Os frontends autenticam no Access Management e os backends publicam nele os seus recursos. <strong>Não há token estático</strong> — todo o M2M é <code>client_credentials</code>.

| Tipo | Rota | Cliente OAuth |
| --- | --- | --- |
| Spring Boot | `/gateway-api/<projeto>` | próprio |
| Next.js | `/apps/<projeto>` | login pelo seeded · M2M próprio |

---

## Backends Spring Boot [#backends-spring-boot]

### O que muda na v2 [#o-que-muda-na-v2]

| Área | v1 | v2 |
| --- | --- | --- |
| Biblioteca auth | `core-spring-boot` beta-12 | `0.2.0-beta.8` |
| Spring Boot / Java | 3.5.10 / 21 | **3.5.16 / Java 26** |
| Beans do framework | nenhum registado | `@Import` explícito |
| M2M | `IGRP_ACCESS_M2M_SYNC_TOKEN` | `IGRP_ACCESS_M2M_CLIENT_ID` / `_SECRET` |
| `AUTH_JWT_ISSUER` | `/auth/realms/igrp` | `/gateway-api/<ws>-access-management` |
| `SPRING_APPLICATION_NAME` | nome livre | igual ao client ID |
| Converter JWT | próprio | `IgrpJwtAuthenticationConverter` da livraria |
| Adaptador | — | `igrp.authorization.service.adapter=igrp` |

### Java 26 é obrigatório [#java-26-é-obrigatório]

A `cv.igrp.framework.auth:core-spring-boot:0.2.0-beta.8` é publicada com bytecode **Java 26** (class file 70).

<Callout type="danger" title="O sintoma não aponta para a causa">
Com <code>release 21</code> o <code>javac</code> rejeita-a (<code>bad class file ... has wrong version 70.0</code>) e o erro aborta a ronda de <em>annotation processing</em> — o que se manifesta como <strong>centenas de <code>cannot find symbol</code> em getters do Lombok</strong>. Quem vir isso vai procurar o problema no Lombok, e não está lá.
</Callout>

Requisitos: Spring Boot **≥ 3.5.16**, `<java.version>26</java.version>`, e imagens de build e runtime com JDK/JRE ≥ 26.

### Beans que têm de ser importados [#beans-que-têm-de-ser-importados]

A auto-configuração da livraria só declara o bean `apiClient`. O resto vive em `cv.igrp.framework.auth.core`, **fora do component scan**, e sem `@Import` nada é criado:

```java
@Import({
  AuthorizationSyncRunner.class,          // publica recursos/permissões no arranque
  IgrpAuthorizationService.class,         // resolve roles/permissões no AM
  IgrpAuthorizationServiceAdapter.class,  // @ConditionalOnProperty(...="igrp")
  IgrpJwtAuthenticationConverter.class,   // fornece o JwtAuthenticationConverter
  MethodSecurityEnableConfig.class,       // @PreAuthorize sobre permissões IGRP
})
```

<Callout type="warning" title="Sem AuthorizationSyncRunner o sync falha em silêncio">
Não aparece nenhuma linha <code>[Authorization Sync]</code> nos logs, e nada acontece.
</Callout>

### SecurityConfig [#securityconfig]

A v2 exige o converter da livraria, que resolve roles pelo `IAuthorizationServiceAdapter`. Um `JwtAuthenticationConverter` próprio só lê authorities do JWT e tem de ser removido. O da livraria é injectado com `ObjectProvider`, porque é condicional:

```java
public SecurityFilterChain securityFilterChain(
        HttpSecurity http,
        ObjectProvider<JwtAuthenticationConverter> jwtAuthenticationConverter) { ... }
```

<Callout type="danger" title="Um SecurityConfig desactivado autentica tudo com HTTP Basic">
Se as anotações estiverem comentadas e a segurança excluída em <code>spring.autoconfigure.exclude</code>, sem uma <code>SecurityFilterChain</code> o Spring Boot cai no default. Reactiva-o.
</Callout>

### Annotation processor [#annotation-processor]

A `cv.igrp.framework.auth:core` gera o `PermissionsRegistry`. Tem de estar em `annotationProcessorPaths` **depois** do Lombok e com o Lombok excluído — dois Lombok no processor path quebram a geração de getters. O `maven-compiler-plugin` tem de ser ≥ 3.14.1: a 3.11.0 não resolve uma versão gerida num `annotationProcessorPath`.

### Propriedades [#propriedades]

```properties
spring.application.name=${SPRING_APPLICATION_NAME:<projeto>}
igrp.access.api.base-url=${IGRP_ACCESS_API_BASE_URL}
igrp.access.m2m.client-id=${IGRP_ACCESS_M2M_CLIENT_ID:}
igrp.access.m2m.client-secret=${IGRP_ACCESS_M2M_CLIENT_SECRET:}
igrp.access.m2m.scope=${IGRP_ACCESS_M2M_SCOPE:}
igrp.authorization.service.adapter=${IGRP_AUTHORIZATION_SERVICE_ADAPTER:igrp}
```

O `igrp.access.m2m.sync-token` foi removido de propósito: sem fallback, uma credencial em falta falha de forma visível em vez de degradar para o modo depreciado.

### Eureka: a chave do mapa não faz binding por env var [#eureka-a-chave-do-mapa-não-faz-binding-por-env-var]

<Callout type="warning" title="EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE não funciona">
O <code>eureka.client.service-url</code> é um <code>Map</code>. A env var produz a chave <strong><code>defaultzone</code></strong> em minúsculas, e o Eureka lê <code>defaultZone</code> — ficando com o default <code>http://localhost:8761/eureka/</code>. O sintoma é <code>Cannot execute request on any known server</code> <em>mesmo com o servidor acessível</em>.
</Callout>

Declara a zona no `application-development.properties`:

```properties
eureka.client.service-url.defaultZone=${EUREKA_SERVICE_URL:http://<slug>-eureka:8761/eureka/}
```

---

## Frontends Next.js [#frontends-nextjs]

### O migrador oficial faz o grosso [#o-migrador-oficial-faz-o-grosso]

Não é preciso escrever código de auth à mão: o `@igrp/framework-next-auth` já traz `withIGRPAuth()`, `createAuthProviderFromEnv()`, `refreshOidcAccessToken()` e `revokeOidcSession()`.

```powershell
cd projects/theque
pnpm dlx @igrp/template-migrator@latest status
pnpm dlx @igrp/template-migrator@latest plan
pnpm dlx @igrp/template-migrator@latest apply
```

<Callout type="warning" title="O design system exige peers exactos">
Sem eles o <code>npm install</code> recusa-se a resolver. E os lockfiles têm de ser regerados <strong>de raiz</strong> — um lockfile antigo bloqueia a resolução com um <code>ERESOLVE</code> que aponta para a versão velha. Se o projeto tiver <code>package-lock.json</code> <strong>e</strong> <code>yarn.lock</code>, deixa só o que o Dockerfile usa.
</Callout>

### Contrato de env [#contrato-de-env]

```text
AUTH_PROVIDER=igrp-auth
IGRP_AUTH_CLIENT_ID / _SECRET / _ISSUER / _SCOPES
NEXTAUTH_URL (com basePath E /api/auth) / NEXTAUTH_URL_INTERNAL / NEXTAUTH_SECRET
IGRP_ACCESS_MANAGEMENT_API
IGRP_SERVICE_ID / IGRP_M2M_CLIENT_ID / IGRP_M2M_CLIENT_SECRET
IGRP_APP_CODE / NEXT_PUBLIC_IGRP_APP_CODE
IGRP_PREVIEW_MODE / IGRP_SYNC_ACCESS / IGRP_SYNC_ON_CODE_MENUS / ..._MENU_ROLES
NEXT_IGRP_APP_CENTER_URL / NEXT_PUBLIC_BASE_PATH / NEXT_PUBLIC_IGRP_APP_HOME_SLUG
```

- **Login pelo cliente seeded, M2M pelo cliente próprio.** O OIDC usa `ACCESS_MANAGEMENT_OAUTH_CLIENT_ID/SECRET`, com os callbacks registados automaticamente; o sync usa o cliente próprio, com Basic em `${IGRP_ACCESS_MANAGEMENT_API}/oauth2/token` e o `IGRP_SERVICE_ID` no header `X-Machine-Service-ID`.
- `IGRP_AUTH_SCOPES=openid email`. O discovery anuncia `scopes_supported: ["openid"]`; pedir `profile` não é seguro sem o registar no Authorization Server.
- `IGRP_SYNC_ON_CODE_MENUS=true` empurra `src/temp/menus` e **sobrescreve** os menus no Access Management. Passa a `false` quando o AM for a fonte de verdade.
- O código lê `NEXT_IGRP_APP_CENTER_URL`, **não** `IGRP_APP_CENTER_URL`.
- O `NEXTAUTH_SECRET` tem de ser o mesmo em todos os frontends do host, senão trocar de app força novo login.

### Correções de código que o upgrade exige [#correções-de-código-que-o-upgrade-exige]

O migrador não toca no código próprio da app:

| O que mudou | Efeito |
| --- | --- |
| `IGRPUserDTO.id` passou de `number` a `string` | erro de tipos |
| `IGRPCombobox` e `IGRPDatePicker` perderam `gridSize` | só sobrevive em `IGRPRadioGroup`; o `className` adjacente já define o span |
| `IGRPDatePicker` passou a união discriminada | exige `mode`; `startDate`/`endDate` deram lugar a `disableBefore`/`disableAfter` |
| Guardas de sessão antigas | a função é hoje do `IGRPSessionWatcher` |

<Callout type="note" title="Dynamic Code Evaluation not allowed in Edge Runtime">
Sob webpack, o bundle Edge de <code>middleware.ts → lib/auth.ts</code> pode falhar com este erro. A correcção é passar o <code>build</code> a <code>next build --turbopack</code>.
</Callout>

### Correções no Dockerfile [#correções-no-dockerfile]

<Callout type="danger" title="O standalone fica aninhado">
Com <code>output: "standalone"</code> e o <code>turbopack.root</code> do template, o <code>server.js</code> sai em <code>.next/standalone/app/server.js</code>. Sem ajuste o contentor morre com <code>Cannot find module '/app/server.js'</code>. Os estáticos e o <code>public</code> vão para <code>./app/...</code> e o arranque passa a <code>node app/server.js</code> — também no <code>command</code> de warmup do compose.
</Callout>

<Callout type="warning" title="NEXTAUTH_SECRET é preciso no estágio de build">
O framework pré-renderiza <code>/_not-found</code>, esse render lê a sessão e falha com <code>NEXTAUTH_SECRET must be set in production</code>. O valor existe só no builder, que é descartado.
</Callout>

---

## Arrancar [#arrancar]

Com a stack principal a correr e o registo dos clientes OAuth concluído — um projeto novo só recebe o seu cliente no **próximo arranque da stack principal**.

<Tabs>
  <TabItem value="studio" label="No Studio">
    No cartão do projeto, **Deploy Docker** constrói a imagem e arranca o projeto; depois, **Open in Browser**. O projeto aparece no grupo **Project** de **Serviços → Visão Geral**.
  </TabItem>
  <TabItem value="cli" label="docker compose">
    ```bash
    cd projects/<projeto>
    docker compose --env-file ../../.env -f igrp-compose-<projeto>.yaml up -d --build
    ```

    No modo Autentika troca o env-file por `../../.igrp-autentika.env`. Os composes dos projetos são iguais nos dois modos, porque a federação é escolhida só pelas variáveis `IGRP_OAUTH_EXTERNAL_IDP_*` do Access Management.
  </TabItem>
</Tabs>

<Callout type="danger" title="Usa sempre --build nos projetos">
  Os composes de projeto usam `build:`, por isso têm imagem própria. Um `docker build -t <nome>` avulso produz uma tag que o compose nunca usa, e o contentor continua a correr o código antigo. Passa `--build` na primeira vez e depois de mexer no código ou no Dockerfile.
</Callout>

---

## Verificação [#verificação]

**Backends:**

```text
[Authorization Sync] Authenticating to Access Management via client_credentials grant (clientId=...)
[Authorization Sync] Resource synchronization completed.
[Permission Sync] Successfully synchronized N permissions.
```

`N=0` é esperado enquanto as apps não tiverem anotações de permissão. A linha `No client_credentials configured ... deprecated static-token mode` significa que os segredos M2M não chegaram ao contentor.

**Frontends:** `✓ Ready` + `Application <APP_CODE> metadata synced successfully.` + `Access Management sync completed.`

**Rotas** — sempre por `${DOCKER_IP}:${NGINX_HTTP_PORT}`:

```bash
curl http://<slug>:<porta>/apps/<frontend>
curl http://<slug>:<porta>/gateway-api/<backend>/actuator/health
curl -o /dev/null -w "%{http_code}" http://<slug>:<porta>/apps/<frontend>/igrp/logo.png
```

Um CSS ou imagem a 404 significa imagem antiga — falta o `--build`.

---

## Armadilhas da plataforma descobertas ao arrancar [#armadilhas-da-plataforma-descobertas-ao-arrancar]

Bugs pré-existentes que impediam a stack de subir, e que vale a pena reconhecer:

| Problema | Causa e correcção |
| --- | --- |
| `set: illegal option -` numa tarefa de init | **CRLF nos scripts montados** — o shebang fica `#!/bin/sh\r`. Normalizar para LF e fixar por `.gitattributes` |
| Healthcheck do Access Management sempre `unhealthy` | A imagem é **distroless**: sem shell nem `curl`. O `command:` era entregue à JVM como argumentos e nunca corria. Substituído pela tarefa `-ready` |
| `granted=0` e `Permission 'igrp.client.list' not found` | [Defeito do primeiro arranque](./autenticacao.mdx#o-defeito-do-primeiro-arranque) — um único restart resolve |
| Nginx sempre `unhealthy` | Healthcheck na porta errada (`:80` em vez de `${NGINX_HTTP_PORT}`) |
| **Redirect de login para um endereço interno** | No nginx, um `proxy_set_header` dentro de um `location` **descarta todos** os headers do nível `server`. O bloco do Access Management só declarava `X-Forwarded-Prefix`; sem `Host`/`X-Forwarded-Host` o `Host` passava a `$proxy_host`, e como o AM se regista no Eureka com `prefer-ip-address`, os redirects absolutos saíam pelo IP do contentor. Os seis headers têm de ser repetidos no bloco |
| `authorization_request_not_found` no callback | Cookie de sessão `Secure` sobre HTTP — a imagem traz `server.servlet.session.cookie.secure=true` e o browser descarta-o, criando sessão nova a cada pedido |
| Redirects pós-login do App Center para o contentor | `NEXTAUTH_URL_INTERNAL` apontava para o contentor. Tem de ser `http://${DOCKER_IP}:${NGINX_HTTP_PORT}` — confirmável em `GET /api/auth/providers` |

### E no lado dos projetos [#e-no-lado-dos-projetos]

| Problema | Correcção |
| --- | --- |
| Perfil dev pede uma variável que o compose não dá | `${SERVER_PORT:${SERVICE_PORT:8080}}` em vez de assumir uma delas |
| H2 fixo e exclusão da segurança em **todos** os perfis | mover para um perfil `local` explícito |
| `data.sql` com INSERTs sem guarda e `mode=always` | duplica dados a cada reinício. O default passa a `never` |
| Eureka apontado ao host errado | usar o nome do serviço do workspace |
| Bucket MinIO que o script de init nunca cria | derivar a configuração de `IGRP_OBJECT_STORAGE_*` no compose |
| `IGRP_PREVIEW_MODE=true` num frontend | **desliga a autenticação**. Nunca em nada que não seja uma demo |


# Registo de clientes OAuth

URL: /pt/docs/developer/workspace/registo-de-clientes

Como o workspace regista sozinho um cliente OAuth por microserviço e um callback de login por frontend, sem listas para manter.

# Registo de clientes OAuth [#registo-de-clientes-oauth]

O workspace regista <strong>sozinho</strong> tudo o que os seus serviços precisam no Access Management: um cliente OAuth por microserviço e um callback de login por frontend. Não há passo manual, e um projeto criado hoje pelo IGRP Studio é apanhado no próximo <code>up</code> sem se editar nenhuma lista.

---

## Porque tem de ser automático [#porque-tem-de-ser-automático]

Na v2 não existe token M2M estático: cada serviço autentica-se com o seu par `client_credentials` e identifica-se no header `X-Machine-Service-ID`. Sem cliente registado, um serviço arranca, autentica-se com o que tem e falha a publicação dos seus recursos — normalmente com `404 IGRP_AUTH_RESOURCE_NOT_FOUND_BY_NAME`, longe da causa real.

E há um detalhe que decide a arquitectura toda:

<Callout type="danger" title="O clientSecret só é devolvido no momento da criação">
Nunca mais é possível lê-lo. Logo não se pode escrever o segredo no <code>.env</code> antes de o cliente existir: tem de ser o próprio registo a criá-lo e a escrevê-lo. É isso que fixa a ordem de arranque do workspace.
</Callout>

---

## Os dois mecanismos [#os-dois-mecanismos]

| | Registo de clientes | Registo de redirects |
| --- | --- | --- |
| O que cria | cliente OAuth + service account **por serviço** | os callbacks de login **no cliente seeded** |
| Quem precisa | backends e frontends | só frontends |
| Tarefa | `-oauth-clients` | `-oauth-client-uri-sync` |
| Script | `register-oauth-clients.sh` | `sync-default-client-uris.sh` |
| Resultado | segredos escritos nos env-files | `redirectUris` do cliente seeded actualizados |

São separados porque respondem a perguntas diferentes. O cliente é **como o serviço fala com o Access Management** (máquina a máquina); o callback é **como o utilizador faz login na app** (browser) — e todas as UIs partilham o mesmo cliente OIDC, o seeded, pelo que cada app só precisa que o seu caminho esteja na lista dele.

### Onde isto vive no compose [#onde-isto-vive-no-compose]

Só nos ficheiros da raiz. Os composes do Process e dos projetos **não registam nada**, apenas consomem o resultado.

```text
access-management                    <- arranca
  |- access-management-ready         portão
      |- ...-oauth-clients           clientes
      |- ...-oauth-client-uri-sync   redirects
      |- application-center          e o resto da stack
```

As três últimas dependem do portão com `condition: service_completed_successfully`. O portão é o `wait-for-catalog.sh`, que espera pela saúde do serviço e contorna o [defeito de primeiro arranque](./autenticacao.mdx#o-defeito-do-primeiro-arranque).

---

## Parte 1 — Registo dos clientes [#parte-1--registo-dos-clientes]

O `register-oauth-clients.sh` corre uma vez e morre. Para cada serviço:

**1. Espera pela API através do gateway.** Pede token e faz `GET /api/clients` até 200, 30 tentativas de 10s. Em cada falha faz `POST` no `GATEWAY_REFRESH_URL` — depois do restart do portão o Access Management tem um IP novo e a rota antiga pode ficar em cache durante minutos, fazendo tudo em `/gateway-api` devolver 404.

**2. Decide o que fazer**, comparando o Access Management com o estado local:

| Cliente existe? | Segredo local | Acção |
| --- | --- | --- |
| sim | real | ignora |
| sim | `CHANGE_ME_*` | apaga e re-cria |
| não | qualquer | cria |

O caso do meio existe porque o segredo é irrecuperável: se o local ainda é placeholder, ninguém o está a usar e re-criar é seguro.

**3. Apaga na ordem certa** — primeiro o service account, só depois o cliente. O `DELETE /api/clients/{uuid}` não faz cascade, e a violação de chave estrangeira sai como **500**, não 409.

**4. Cria o cliente** — `POST /api/clients` com `clientId`, `clientName`, `description`, `active`, `requirePkce: false`, os três TTLs, `scopes: ["openid"]` e `grantTypes: ["client_credentials"]`. Sem `redirectUris`: os backends não precisam e os frontends fazem login pelo cliente seeded.

**5. Cria o service account** — `POST /api/service-accounts` com `oauthClientId` igual ao **UUID devolvido**, nunca a string do `clientId`. Sem isto o token autentica mas não consegue usar a API.

**6. Escreve o segredo** em cada env-file indicado. Acabou de criar o cliente, logo o valor em mão é o único válido e **ganha sempre**.

**7. Sai com 1 se algo falhou.** Sair 0 com "0 criados" deixaria o instalador arrancar os projetos com placeholders.

<Callout type="note" title="A escrita preserva o inode">
Os env-files são bind-mounted <strong>ficheiro a ficheiro</strong>, não a pasta. Um <code>sed -i</code> ou um <code>mv</code> substituiria o ficheiro e desligaria o mount — a alteração nunca chegaria ao host. Por isso a escrita é <code>awk &gt; tmp</code> seguido de <code>cat tmp &gt; ficheiro</code>.
</Callout>

### Lista fixa mais descoberta [#lista-fixa-mais-descoberta]

O que torna isto utilizável num workspace real: **os projetos não estão listados em lado nenhum**.

| Origem | O que cobre | Porquê |
| --- | --- | --- |
| `oauth-clients.spec` | serviços de plataforma | os env-files deles vivem fora de `projects/` |
| descoberta em runtime | tudo em `projects/` | são criados dinamicamente pelo IGRP Studio |

A spec tem prioridade: uma linha lá escrita sobrepõe-se à descoberta, pelo que fixar um projeto à mão continua possível.

O sinal da descoberta é o **compose do próprio projeto**, que já declara as variáveis de que precisa:

```yaml
- IGRP_ACCESS_M2M_CLIENT_ID=${DEMO_NOVO_M2M_CLIENT_ID}   # backend
- IGRP_M2M_CLIENT_ID=${THEQUE_M2M_CLIENT_ID}             # frontend
```

Dessa única linha sai tudo:

| Derivado | De onde |
| --- | --- |
| variável do segredo | `..._CLIENT_ID` → `..._CLIENT_SECRET` |
| tipo | `IGRP_ACCESS_M2M_*` é backend, `IGRP_M2M_*` é frontend |
| `clientId` | valor de `..._M2M_CLIENT_ID` no `.env`; se ainda não existir, **o nome do directório**, escrito de volta nos dois env-files da raiz |

O último ponto fecha o ciclo para um projeto acabado de criar, e o nome do directório mantém, por construção, a igualdade obrigatória `clientId == spring.application.name`.

Um projeto sem variáveis M2M é ignorado — não fala com o Access Management, não precisa de cliente.

### A regra anti-cópia [#a-regra-anti-cópia]

Um directório só é considerado projeto se tiver **`igrp-compose-<nome-do-directório>.yaml`**. Um directório cujo compose nomeia outro projeto é uma cópia, e registá-la reclamaria o cliente de outro:

```text
[OAuth Clients]   . gestao-utente has no igrp-compose-gestao-utente.yaml — skipping
```

### Como os outros stacks recebem o resultado [#como-os-outros-stacks-recebem-o-resultado]

Não há comunicação entre composes: o canal é o ficheiro `.env`.

**Backends Spring** — o `SPRING_APPLICATION_NAME` **tem de ser** o `clientId`. **Frontends Next.js** — o `IGRP_SERVICE_ID` vai no header `X-Machine-Service-ID`.

Dois serviços ficam **fora** deste mecanismo, de propósito:

- o **access-management** é o próprio servidor: semeia o seu cliente no arranque a partir de `IGRP_OAUTH_DEFAULT_CLIENT_ID`, e é com ele que as duas tarefas se autenticam;
- o **app-center** reutiliza esse mesmo cliente seeded.

---

## Parte 2 — Registo dos redirects [#parte-2--registo-dos-redirects]

### O problema [#o-problema]

Todas as UIs fazem login pelo **mesmo** cliente OIDC, o seeded. O Spring Authorization Server só aceita um `redirect_uri` registado nesse cliente; se o callback de uma app não estiver lá, o login falha **antes** de o utilizador ver seja o que for. A imagem do Access Management semeia **um** callback numa base de dados nova — todos os outros têm de ser acrescentados.

### O que a tarefa faz [#o-que-a-tarefa-faz]

O `sync-default-client-uris.sh`:

1. obtém token com o cliente seeded e encontra o UUID dele;
2. lê os `redirectUris` e `postLogoutRedirectUris` actuais;
3. **faz merge, não substitui** — acrescenta o que falta e preserva o resto;
4. se nada mudou, sai 0 sem escrever;
5. caso contrário faz `PUT /api/clients/{uuid}`, depois de remover os campos que o modelo de leitura tem e o endpoint de actualização rejeita (`id`, `clientSecret`, `applicationCode`, `createdAt`, `updatedAt`).

### Lista fixa mais descoberta, outra vez [#lista-fixa-mais-descoberta-outra-vez]

O caminho base vem do descritor que o IGRP Studio escreve em cada projeto — `projects/theque/.igrp.theque.env` → `NEXT_PUBLIC_BASE_PATH="/apps/theque"`. Daí saem os dois URIs, na forma que o NextAuth constrói:

| URI | Construção |
| --- | --- |
| callback | `${BASE_URL}<basePath>/api/auth/callback/${AUTH_PROVIDER}` |
| pós-logout | `${BASE_URL}<basePath>/login` |

<Callout type="info" title="A ausência de NEXT_PUBLIC_BASE_PATH identifica um backend">
Não tem callback de browser. Há um fallback: sem o descritor, o caminho é extraído do <code>NEXTAUTH_URL</code> do compose do projeto. Aplica-se a mesma regra anti-cópia.
</Callout>

```text
Discovering project frontends under /workspace/projects
  . bibliotheque declares no base path — treating it as a backend (no callback)
  . gestao-utente has no igrp-compose-gestao-utente.yaml — skipping
  + theque -> /apps/theque
  + utente -> /apps/utente
Default OAuth client callback URIs reconciled.
```

<Callout type="warning" title="Limite conhecido: o merge é aditivo">
A tarefa nunca <strong>remove</strong>. Um projeto apagado deixa o seu callback no cliente seeded indefinidamente. Não é grave — aponta para um caminho que já não responde — mas suja a configuração, e a limpeza é manual.
</Callout>

---

## Acrescentar um projeto novo [#acrescentar-um-projeto-novo]

Não há nada a configurar; basta seguir a convenção:

| Ficheiro | Necessário para |
| --- | --- |
| `projects/<nome>/igrp-compose-<nome>.yaml` | ser reconhecido como projeto |
| …com `IGRP_ACCESS_M2M_CLIENT_ID` (backend) ou `IGRP_M2M_CLIENT_ID` (frontend) | ter cliente registado |
| `projects/<nome>/.igrp.<nome>.env` com `NEXT_PUBLIC_BASE_PATH` | ter callback registado (só frontends) |

Depois:

```bash
docker compose --env-file .env -f igrp-compose.yaml up -d
docker wait demoteste-access-management-oauth-clients
```

### A armadilha da ordem de arranque [#a-armadilha-da-ordem-de-arranque]

<Callout type="danger" title="O docker wait não é decorativo">
<code>docker compose up -d</code> <strong>arranca</strong> tarefas one-shot mas só <strong>espera</strong> por elas quando outro serviço declara <code>service_completed_successfully</code>. Nada nos composes separados o declara — não pode, são ficheiros diferentes.
</Callout>

Arrancar um projeto demasiado cedo faz com que leia o `.env` antes da escrita e apanhe um `CHANGE_ME_*`. O sintoma aparece longe da causa: um backend que autentica e falha o sync com 404. É por isso que se espera pela tarefa de registo (`docker wait <slug>-access-management-oauth-clients`) antes de arrancar os projetos — ver [Stack principal](./stack-principal.mdx).

---

## Nomes: três coisas diferentes [#nomes-três-coisas-diferentes]

Confusão frequente, e a causa de 404s difíceis de ler:

| Nome | É | Definido por |
| --- | --- | --- |
| `clientId` | o cliente OAuth **e** o nome do resource no Access Management | a spec ou a descoberta |
| `spring.application.name` | o nome por que o `AuthorizationSyncRunner` procura o resource | `SPRING_APPLICATION_NAME`, **igual** ao `clientId` |
| nome no Eureka | o segmento da rota `/gateway-api/<x>` | `eureka.instance.appname` + os dois virtual-host names |

O nome no Eureka apenas **por omissão** coincide com o `spring.application.name`. É por isso que o workspace consegue ter a rota `demoteste-process-management-api` com o cliente `igrp-process-management-api`.

---

## Verificação [#verificação]

```bash
docker logs demoteste-access-management-oauth-clients
docker logs demoteste-access-management-oauth-client-uri-sync
```

Os clientes registados, pela API:

```bash
CID=$(sed -n 's/^ACCESS_MANAGEMENT_OAUTH_CLIENT_ID=//p' .env | head -1 | tr -d '\r')
CSEC=$(sed -n 's/^ACCESS_MANAGEMENT_OAUTH_CLIENT_SECRET=//p' .env | head -1 | tr -d '\r')
AM=http://localhost:2575/gateway-api/demoteste-access-management
TOK=$(curl -s -X POST "$AM/oauth2/token" -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode grant_type=client_credentials \
  --data-urlencode "client_id=$CID" --data-urlencode "client_secret=$CSEC" \
  | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
curl -s -H "Authorization: Bearer $TOK" "$AM/api/clients" \
  | sed 's/},{/}\n{/g' | sed -n 's/.*"clientId":"\([^"]*\)".*/\1/p' | sort
```

A prova final é o que cada serviço publicou. O `GET /api/applications` responde 403 ao service account seeded, pelo que a via fiável é a base de dados:

```bash
docker exec demoteste-database-postgres psql -U igrp -d igrp_db -tAc \
  "SELECT name FROM t_resource ORDER BY name;"
```

Com os quatro projetos e a stack Process a correr devolve **9**: os 8 serviços mais o `igrp-access-management`.

---

## Equivalência com o Helm chart [#equivalência-com-o-helm-chart]

No [igrp-stack](/docs/developer/cloud) cada serviço traz o seu Job `oauth-register` com hook `post-install,post-upgrade`. Aqui um único one-shot faz o mesmo para todos.

| Passo | Job do chart | Tarefa do compose |
| --- | --- | --- |
| já registado? | sai se o Secret alvo existir | sai se o cliente existir **e** o segredo local for real |
| espera pelo AM | `GET /.well-known/openid-configuration` | `POST /oauth2/token` + `GET /api/clients` até 200 |
| rota do gateway em falta | `POST /actuator/gateway/refresh` a cada tentativa | idem, via `GATEWAY_REFRESH_URL` |
| cliente sem segredo | falha, salvo `autoRegister.forceRecreate=true` | apaga e re-cria quando o valor local é placeholder |
| apagar o cliente | `DELETE /api/clients/{uuid}` | apaga primeiro o service account |
| payload | `clientId`, `clientName`, `description`, `active`, `requirePkce`, TTLs, `scopes`, `grantTypes` | o mesmo |
| service account | `POST /api/service-accounts`, `oauthClientId` = UUID | o mesmo |
| onde guarda o segredo | `kubectl create secret` | env-files, preservando o inode |
| lista de serviços | um Job por chart, no `values.yaml` | spec para a plataforma + descoberta em `projects/` |

A diferença de fundo é o destino do segredo. A última linha é a divergência deliberada: no chart cada serviço é uma release Helm conhecida à instalação, enquanto num workspace os projetos aparecem a qualquer momento.


# Resolução de problemas

URL: /pt/docs/developer/workspace/resolucao-de-problemas

Sintomas comuns da plataforma local em Docker e as suas causas — arranque, autenticação, gateway, projetos — e como confirmar que tudo está sincronizado.

## Antes de tudo [#antes-de-tudo]

- **Docker** com Compose v2, ~6 GB de RAM livres e acesso a `registry.nosi.cv` e `sonatype.nosi.cv`.
- **O hostname resolve?** O slug do workspace tem de estar no ficheiro `hosts` (`127.0.0.1  <slug>`; no Windows, `C:\Windows\System32\drivers\etc\hosts` como Administrador). Usa **sempre o mesmo** hostname no browser — se alternares entre ele e `localhost`, os callbacks não batem certo e o login falha.
- **A ordem foi respeitada?** [Stack principal](./stack-principal.mdx) primeiro, com o registo dos clientes OAuth concluído, e só depois projetos, Process e Monitoring.

## Confirmar que está sincronizado [#confirmar-que-está-sincronizado]

Um HTTP 200 não chega — a prova real é o **sync** com o Access Management:

| Tipo | Linha que tem de aparecer nos logs |
|---|---|
| Backends Spring Boot | `Resource synchronization completed` |
| Frontends Next.js | `Access Management sync completed` |

```bash
docker logs <contentor> 2>&1 | grep -c 'synchronization completed\|sync completed'
```

## Sintoma → causa [#sintoma--causa]

| Sintoma | Causa |
|---|---|
| Erro do nginx em `/` | Application Center em `Created`: a readiness não completou. Ver `docker logs <slug>-access-management-ready`. |
| `...-ready` sai com **3** | Self-heal desligado. `docker restart <slug>-access-management` e repetir o `up`. |
| Backend autentica e falha o sync com **404** | O segredo não corresponde ao cliente: ou é `CHANGE_ME_*` (o projeto arrancou antes de o registo escrever o segredo), ou é de um cliente que já não existe. |
| `No client_credentials configured ... static-token mode` | O segredo M2M não chegou ao contentor. |
| `set: illegal option -` numa tarefa de init | Script montado com fins de linha CRLF. |
| `/gateway-api/<x>` dá **404** ao arrancar | Cache de rotas do gateway, até ~2 min. Se persistir, o nome da rota não bate certo com o `vipAddress` no Eureka — ver [rota e client ID](./stack-process.mdx#rota-e-client-id-são-coisas-diferentes). |
| **401** em `/gateway-api/<x>/actuator/health` | Actuator protegido — prova que a rota existe. |
| `authorization_request_not_found` no login | Cookie `Secure` sobre HTTP. Põe `ACCESS_MANAGEMENT_SESSION_COOKIE_SECURE=false`. |
| Login redireciona para um IP de contentor | O nginx não propaga `Host`/`X-Forwarded-Host` na rota do Access Management. |
| Login das UIs da Process falha | `ACCESS_MANAGEMENT_OAUTH_CLIENT_SECRET` diferente entre `.env` e `process/.env_process`. |
| App sem CSS, imagens quebradas, código antigo | Imagem antiga do projeto. `up -d --build` — ver [Projetos](./projetos.mdx). |
| `Cannot find module '/app/server.js'` | Layout `standalone` aninhado; o arranque é `node app/server.js`. |
| Trocar de app força novo login | `NEXTAUTH_SECRET` diferente entre frontends do mesmo host. |
| `down` de um projeto apaga contentores da raiz | `COMPOSE_PROJECT_NAME` definido num env-file. Não o definas. |
| Grafana sem dados das aplicações | A telemetria está desligada por defeito — ver [Stack Monitoring](./stack-monitoring.mdx#ligar-a-telemetria-das-aplicações). |

<Callout type="info" title="HTTP">
  O workspace corre em **HTTP simples**. Passa `ACCESS_MANAGEMENT_SESSION_COOKIE_SECURE` a `true` assim que servires por HTTPS.
</Callout>


# Stack de monitorização

URL: /pt/docs/developer/workspace/stack-monitoring

monitoring/ — observabilidade do workspace local com OpenTelemetry Collector, Prometheus, Loki, Tempo, Grafana e cAdvisor; como ativar, ligar a telemetria e usar o Grafana.

A stack Monitoring dá observabilidade a todo o workspace: métricas, logs e traces dos serviços e dos teus projetos, num só Grafana. É opcional e arranca **depois** da stack principal.

| Compose | Ambiente |
|---|---|
| `monitoring/igrp-monitoring-compose.yaml` | `monitoring/.env_monitoring` |

## Serviços [#serviços]

| Serviço | Papel |
|---|---|
| `otel-collector` | Recebe a telemetria das aplicações (OTLP: gRPC `4317`, HTTP `4318`) e encaminha-a |
| `prometheus` | Métricas |
| `loki` | Logs |
| `tempo` | Traces — e métricas derivadas dos spans, incluindo o *service graph* |
| `cadvisor` | Métricas dos contentores Docker (CPU, memória, rede) |
| `grafana` | Painéis sobre os três — em `/grafana` |

```text
aplicações ──OTLP──▶ otel-collector ──▶ Prometheus (métricas)
                                    ├─▶ Loki       (logs)
                                    └─▶ Tempo      (traces)
cAdvisor ─────────────────────────────▶ Prometheus
                         Grafana ◀── consulta os três
```

## Ativar e arrancar [#ativar-e-arrancar]

<Tabs>
  <TabItem value="studio" label="No Studio">
    Escolhe **Monitoring** ao [criar o workspace](./criar-workspace.mdx), ou acrescenta-a depois em **Serviços → Docker → Add Stack → Monitoring Stack**. Arranca e para pelo grupo **Monitoring** em **Serviços → Visão Geral**.
  </TabItem>
  <TabItem value="cli" label="docker compose">
    Com a stack principal já a correr:

    ```bash
    cd monitoring
    docker compose --env-file ./.env_monitoring -f igrp-monitoring-compose.yaml up -d
    ```

    Para parar, o mesmo comando com `down`.
  </TabItem>
</Tabs>

## Ligar a telemetria das aplicações [#ligar-a-telemetria-das-aplicações]

<Callout type="warning" title="Desligada por defeito">
  Sem esta stack não há collector, por isso as aplicações vêm com a telemetria **desligada**. Com a stack a correr, só o cAdvisor envia dados até ligares cada serviço.
</Callout>

As aplicações Spring controlam a telemetria por `OTEL_DISABLED`; o endpoint do collector já vem configurado. Para ligar, passa a variável a `false` no env-file e reinicia o serviço:

| Serviço | Variável | Ficheiro |
|---|---|---|
| Access Management | `ACCESS_MANAGEMENT_OTEL_DISABLED` | `.env` |
| Process Management API | `PROCESS_MANAGEMENT_OTEL_DISABLED` | `process/.env_process` |
| Process Studio API | `PROCESS_STUDIO_OTEL_DISABLED` | `process/.env_process` |
| Os teus backends Spring Boot | `OTEL_DISABLED` (gerado em todos os projetos, com `OTEL_EXPORTER_OTLP_ENDPOINT` já apontado ao collector) | env-file do projeto em `projects/<projeto>/` |

O endpoint é sempre `http://<slug>-otel-collector:4318`.

## Grafana [#grafana]

Abre `http://<slug>:<porta>/grafana`. As credenciais de administração estão em `monitoring/.env_monitoring` (`GRAFANA_ADMIN_USER` / `GRAFANA_ADMIN_PASSWORD`). Os datasources — Prometheus, Loki e Tempo — são provisionados automaticamente; não há nada a configurar para começar a explorar.

A configuração de cada componente está em `monitoring/monitoring/` (`collector/`, `prometheus/`, `tempo/`, `grafana/provisioning/`).


# Stack principal

URL: /pt/docs/developer/workspace/stack-principal

igrp-compose.yaml — os serviços da plataforma iGRP, as tarefas de inicialização do Access Management, como arrancar e parar, URLs e verificação.

A stack principal é a camada partilhada da plataforma. Está sempre presente, arranca **primeiro** e dela dependem os projetos e as stacks Process e Monitoring.

| | Modo Keycloak (por defeito) | Modo Autentika |
|---|---|---|
| Compose | `igrp-compose.yaml` | `igrp-autentika-compose.yaml` |
| Ambiente | `.env` | `.igrp-autentika.env` |

## Serviços [#serviços]

Os contentores têm o prefixo do workspace (`<slug>-`).

| Serviço | Papel | URL |
|---|---|---|
| `nginx` | Reverse proxy — a única porta exposta (`NGINX_HTTP_PORT`, a partir de `2575`) | |
| `database-postgres` | PostgreSQL partilhado por todos os serviços | |
| `iam-keycloak` | Fornecedor de identidade (só no modo Keycloak) | `/auth` |
| `access-management` | Autorização: utilizadores, perfis, permissões, clientes OAuth | |
| `application-center` | Portal de administração e entrada nas aplicações | `/` |
| `gateway` | API Gateway para as APIs Spring | `/gateway-api/<serviço>` |
| `eureka` | Registo de serviços usado pelo gateway | `/eureka` |
| `minio` | Armazenamento de ficheiros (S3) | `/minio` |
| `redis` | Cache | |
| `pgadmin` | Administração do PostgreSQL | `/pgadmin` |

As credenciais de administração (Keycloak, pgAdmin, superadmin do Access Management) estão no `.env`.

## Tarefas de inicialização [#tarefas-de-inicialização]

Além dos serviços, a stack corre tarefas *one-shot* que preparam a plataforma. Tudo o que vem a seguir depende delas.

| Tarefa | O que faz |
|---|---|
| `*-db-prepare` | Cria as bases de dados que faltem antes de o serviço arrancar |
| `access-management-sas-schema` | Cria as tabelas do Spring Authorization Server numa base de dados nova |
| `access-management-ready` | Espera pelo Access Management e prova que o catálogo de permissões está semeado |
| `access-management-oauth-clients` | Regista os clientes OAuth e service accounts da plataforma **e dos teus projetos**, e escreve os segredos nos env-files. Idempotente |
| `access-management-oauth-client-uri-sync` | Reconcilia os callbacks de login das UIs da plataforma e dos teus frontends |

<Callout type="warning" title="O primeiro arranque reinicia o Access Management uma vez">
  Numa base de dados nova, o catálogo de permissões só fica completo ao segundo arranque. A tarefa `-ready` deteta esse estado e reinicia o Access Management sozinha — ver [o defeito do primeiro arranque](./autenticacao.mdx#o-defeito-do-primeiro-arranque).
</Callout>

O Access Management é *distroless* (sem shell nem `curl`), por isso não tem healthcheck próprio: a prontidão é dada pela tarefa `-ready`. Os detalhes do registo de clientes estão em [Registo de clientes OAuth](./registo-de-clientes.mdx).

## Arrancar e parar [#arrancar-e-parar]

<Tabs>
  <TabItem value="studio" label="No Studio">
    **Serviços → Visão Geral → IGRP Stack** → ▶ para arrancar, ■ para parar. Ao criar o workspace, o Studio já arranca esta stack se o Docker estiver a correr.
  </TabItem>
  <TabItem value="cli" label="docker compose">
    A partir da pasta do workspace:

    ```bash
    docker compose --env-file .env -f igrp-compose.yaml up -d
    ```

    Espera que o registo dos clientes OAuth termine antes de arrancar projetos ou outras stacks — eles leem os segredos que esta tarefa escreve:

    ```bash
    docker wait <slug>-access-management-oauth-clients
    docker logs <slug>-access-management-oauth-clients
    ```

    Para parar: o mesmo comando com `down` (`down -v` **destrói** os volumes, bases de dados incluídas). No modo Autentika, troca para `--env-file .igrp-autentika.env -f igrp-autentika-compose.yaml`.
  </TabItem>
</Tabs>

## Verificar [#verificar]

<Checklist>
  <Item title="Application Center responde">`http://<slug>:<porta>/` mostra a página de login.</Item>
  <Item title="Tarefas concluídas">`docker compose ps -a` mostra as tarefas `-sas-schema`, `-ready` e `-oauth-clients` em `Exited (0)`.</Item>
  <Item title="Login funciona">Entra com o superadmin definido no `.env`.</Item>
</Checklist>

Se algo falhar, ver [Resolução de problemas](./resolucao-de-problemas.mdx).


# Stack de processos

URL: /pt/docs/developer/workspace/stack-process

process/ — Process Management e Process Studio (APIs e UIs) no workspace local, como ativar e arrancar, clientes OAuth, rotas e verificação.

A stack Process acrescenta à plataforma os serviços de [processos BPMN](/docs/developer/process). Corre contra o mesmo Access Management que a stack principal e os projetos, e arranca **sempre depois** dela.

| | Modo Keycloak | Modo Autentika |
|---|---|---|
| Compose | `process/igrp-process-compose.yaml` | `process/igrp-process-autentika-compose.yaml` |
| Ambiente | `process/.env_process` | `process/.env_process_autentika` |

## Serviços [#serviços]

Todas as imagens em `0.2.0-beta` (`registry.nosi.cv/igrp/…`).

| Serviço | Rota | Verificação nos logs |
|---|---|---|
| `<slug>-process-management-api` | `/gateway-api/<slug>-process-management-api` | `Resource synchronization completed.` |
| `<slug>-process-studio-api` | `/gateway-api/<slug>-process-studio-api` | idem |
| `igrp-process-management` | `/apps/igrp-process-management` | `Application IGRP_PROCESS_MANAGEMENT metadata synced successfully.` |
| `igrp-process-studio` | `/apps/igrp-process-studio` | `Application IGRP_PROCESS_STUDIO metadata synced successfully.` |

Cada API tem a sua base de dados, criada por uma tarefa `*-db-prepare`. No catálogo do Access Management ficam as aplicações `IGRP_PROCESS_MANAGEMENT` e `IGRP_PROCESS_STUDIO`.

## Ativar e arrancar [#ativar-e-arrancar]

<Tabs>
  <TabItem value="studio" label="No Studio">
    Escolhe **Process** ao [criar o workspace](./criar-workspace.mdx), ou acrescenta-a depois em **Serviços → Docker → Add Stack → Process Stack**. A partir daí, arranca e para pelo grupo **Process** em **Serviços → Visão Geral**.
  </TabItem>
  <TabItem value="cli" label="docker compose">
    Com a stack principal já a correr:

    ```bash
    cd process
    docker compose --env-file ./.env_process -f igrp-process-compose.yaml up -d
    ```

    Modo Autentika: `--env-file ./.env_process_autentika -f igrp-process-autentika-compose.yaml`. Para parar, o mesmo comando com `down`.
  </TabItem>
</Tabs>

## Clientes OAuth [#clientes-oauth]

Quatro clientes `client_credentials`, sem `redirectUris` — as APIs por serem backends, as UIs porque o login OIDC usa o cliente semeado, a quem a tarefa `-oauth-client-uri-sync` já regista os callbacks.

| `clientId` | Usado como |
|---|---|
| `igrp-process-management-api` | `SPRING_APPLICATION_NAME` + M2M da API |
| `igrp-process-studio-api` | idem |
| `igrp-process-management-ui` | `IGRP_SERVICE_ID` + M2M da UI |
| `igrp-process-studio-ui` | idem |

O registo é automático, pela tarefa `-oauth-clients` da [stack principal](./stack-principal.mdx). Os segredos são escritos em `process/.env_process` e `process/.env_process_autentika`, como `PROCESS_{MANAGEMENT,STUDIO}_{API,UI}_M2M_CLIENT_SECRET`.

<Callout type="warning" title="O segredo do cliente semeado tem de coincidir">
  O `ACCESS_MANAGEMENT_OAUTH_CLIENT_SECRET` dos env-files da Process tem de ser igual ao do `.env` / `.igrp-autentika.env` — senão o login das UIs falha. O template já traz o mesmo valor em todos.
</Callout>

## Rota e client ID são coisas diferentes [#rota-e-client-id-são-coisas-diferentes]

São **três** nomes, e só dois estão obrigatoriamente acoplados:

| | O que é | Quem obriga |
|---|---|---|
| `clientId` | nome do *resource* criado ao registar o cliente | o Access Management |
| `spring.application.name` | nome com que a aplicação procura esse *resource* no arranque | a biblioteca — se divergir, 404 `IGRP_AUTH_RESOURCE_NOT_FOUND_BY_NAME` |
| nome no Eureka = `/gateway-api/<id>` | a **rota** | ninguém: por defeito igual a `spring.application.name` |

`clientId == spring.application.name` é obrigatório, mas a rota pode seguir a convenção de prefixo do workspace (`<slug>-process-management-api`), como o próprio Access Management faz. Desacoplar exige **três** variáveis — só `EUREKA_INSTANCE_APPNAME` não chega, porque o gateway resolve `lb://<APP>` pelo *virtual host*:

```yaml
- SPRING_APPLICATION_NAME=${PROCESS_MANAGEMENT_API_M2M_CLIENT_ID}   # = clientId
- EUREKA_INSTANCE_APPNAME=${PROCESS_MANAGEMENT_API_SERVICE_NAME}
- EUREKA_INSTANCE_VIRTUAL_HOST_NAME=${PROCESS_MANAGEMENT_API_SERVICE_NAME}
- EUREKA_INSTANCE_SECURE_VIRTUAL_HOST_NAME=${PROCESS_MANAGEMENT_API_SERVICE_NAME}
```

Todas as referências `/gateway-api/<...>` — o `API_GATEWAY` das UIs, `SPRINGDOC_SWAGGER_UI_*`, `OPENAPI_SERVER_API`, `IGRP_PROCESS_ENGINE_BASE_URL` — usam o nome da **rota**, não o do cliente.

## Verificar [#verificar]

```bash
curl http://<slug>:<porta>/apps/igrp-process-management
curl http://<slug>:<porta>/gateway-api/<slug>-process-studio-api/v3/api-docs
```

Nos logs das APIs tem de aparecer `Resource synchronization completed.` e o registo no Eureka (`registration status: 204`); nos das UIs, `metadata synced successfully` e `Access Management sync completed`.

<Callout type="info" title="Respostas que parecem erro mas não são">
  - O `process-studio-api` **não expõe `/actuator`**: um 404 aí vem da aplicação, não do gateway — testa o routing com `/v3/api-docs`.
  - O `process-management-api` devolve **401** em `/actuator/health`: o actuator está protegido. Um 401 prova que a rota existe; um 404 do gateway seria rota ausente.
  - Um toast *"An error occurred in the Server Components render"* em páginas como *Gestão de Tarefas*, com `HTTP Error: 404` nos logs da UI, indica rota desalinhada; a cache de rotas do gateway leva até ~2 minutos a acompanhar uma mudança.
</Callout>


# Application Center

URL: /pt/docs/utilizador/access-manager/application-center

O que aparece depois do login, os cartões de aplicações e a página global de Definições.

# Application Center — Home & Definições [#application-center--home--definições]

O **Application Center** é o ponto de entrada único para todas as aplicações do IGRP. Depois do login, aterra na página _Home_ — que apresenta as aplicações a que tem acesso — e a partir daí pode abrir cada uma delas ou aceder às áreas de administração através de _Definições_.

## O que aparece depois do login [#o-que-aparece-depois-do-login]

Ao autenticar‑se, é encaminhado directamente para a página _Home_, onde vê cartões com todas as aplicações que o seu perfil autoriza. Clicar num cartão abre a respectiva aplicação sem exigir novo login.

<StudioPreview imageName="acHome" alt="Página Home do Application Center" />

<Callout type="info" title="Porque é que só vejo estas aplicações">
A lista está filtrada pelo **perfil que tem activo** nesta sessão. Se um colega vir uma aplicação que a si não aparece, é porque o perfil dele lhe dá esse acesso — e o seu não.
</Callout>

### Barra superior [#barra-superior]

Do lado direito da barra superior encontra três controlos transversais:

- **Seletor de aplicação** — permite saltar entre aplicações sem voltar à Home.
- **Notificações** — caixa agregada com avisos e mensagens dirigidas à sua conta.
- **Perfil do utilizador** — permite trocar de _perfil activo_, aceder às preferências e terminar sessão.

<Callout type="warning" title="Trocar de perfil activo">
Se tem perfis em vários departamentos ou aplicações, apenas **um** está activo em cada sessão. Ao trocar, o que vê muda imediatamente: as aplicações disponíveis, os menus e as acções que pode executar passam a reflectir o novo perfil.
</Callout>

---

## Definições globais [#definições-globais]

Do lado direito da barra superior, o ícone **Definições (⚙)** abre a página global de _Definições_. Cada cartão dá acesso a uma área de administração:

<StudioPreview imageName="acSettingsGlobal" alt="Página de Definições Globais" />

| Cartão                     | Onde documentar                                   |
|----------------------------|---------------------------------------------------|
| **Gestão de Aplicações**   | [Aplicações & Menus](./application.mdx)           |
| **Gestão de Utilizadores** | [Utilizadores & Convites](./user.mdx)             |
| **Gestão de Acessos**      | [Departamentos & Perfis](./departamento.mdx)      |

<Callout type="info" title="Só aparecem os cartões a que tem acesso">
Os cartões visíveis dependem das permissões da sua conta. Se não tem permissão para gerir aplicações, o cartão _Gestão de Aplicações_ nem aparece. Se acha que devia ter acesso a alguma área que não vê, contacte um administrador com âmbito mais alargado.
</Callout>

---

## Login e convite de utilizadores [#login-e-convite-de-utilizadores]

O login é feito através do fornecedor de identidade configurado pela sua organização. Depois de autenticar‑se, é encaminhado para a Home.

<Callout type="info" title="Convite de novos utilizadores">
Quando um novo utilizador é convidado e clica no link que recebeu por email, entra num percurso diferente do login normal — com pedido de OTP e passo de aceitação. Ver [Fluxo de Convite](./invitation-flow.mdx).
</Callout>

### Páginas dedicadas ao convite [#páginas-dedicadas-ao-convite]

O Application Center tem três páginas próprias para o percurso do convidado, para garantir que este nunca aterra num erro genérico:

| Página                    | Quando aparece |
|---------------------------|----------------|
| **Aceitar convite**       | O convidado abriu o link e ainda está a tempo — pede o OTP e conclui a aceitação. |
| **Convite pendente**      | O convidado voltou a abrir o link depois de já ter aceite — informa que a conta ainda está a ser provisionada. |
| **Convite inválido**      | O link já não é válido — expirou, foi cancelado ou já foi respondido. |

---

## Cross‑refs [#crossrefs]

- [Identidade & Login](./identityAccess.mdx) — visão geral da autenticação e da interface geral.
- [Aplicações & Menus](./application.mdx) — para gerir as aplicações que aparecem na Home.
- [Departamentos & Perfis](./departamento.mdx) — para gerir os perfis que decidem que aplicações e menus são visíveis.
- [Fluxo de Convite (OTP)](./invitation-flow.mdx) — percurso completo do convidado.


# Gestão de Aplicações e Menus

URL: /pt/docs/utilizador/access-manager/application

Criar e editar aplicações, gerir a árvore de menus e atribuir perfis a cada entrada de menu.

# Gestão de Aplicações e Menus [#gestão-de-aplicações-e-menus]

Uma **Aplicação** no IGRP representa cada módulo/sistema registado no Application Center. Cada aplicação tem uma **árvore de menus** (pastas, grupos e páginas), e a **atribuição de perfis** a cada entrada de menu é o que decide o que fica visível para os utilizadores finais.

<StudioPreview imageName="app" alt="Lista de aplicações" />

## Como aceder [#como-aceder]

No Application Center, ícone **Definições (⚙)** → **Aplicações**. A lista apresenta as aplicações registadas com o respectivo estado (activa / inactiva), tipo e slug.

---

## Criar aplicação [#criar-aplicação]

Clicar em **+ Nova Aplicação**. Preencher os campos, escolher o tipo (_Interna_ ou _Externa_) e guardar.

<StudioPreview imageName="novoApp" alt="Modal Criar Aplicação" />

<Steps>
  <Step>
    ### Preencher os campos [#preencher-os-campos]

    <Grid cols={2}>
      <Feature eyebrow="Campo 1" title="Nome">Nome descritivo da aplicação. Ex.: `App de Facturação`</Feature>
      <Feature eyebrow="Campo 2" title="Código">Identificador único (imutável após criação). Ex.: `APP_FATURACAO`</Feature>
      <Feature eyebrow="Campo 3" title="Slug">Slug usado nos links do Application Center. Ex.: `app-facturacao`</Feature>
      <Feature eyebrow="Campo 4" title="Tipo">Interna (gerida pelo IGRP) ou Externa (URL de outro sistema). Ex.: `Interna`</Feature>
      <Feature eyebrow="Campo 5" title="Descrição">Descrição opcional. Ex.: `Módulo financeiro`</Feature>
    </Grid>
  </Step>
  <Step>
    ### Aplicação criada [#aplicação-criada]

    Clicar em **Guardar**. A aplicação passa a poder ser associada a departamentos (ver [Departamentos → Associar aplicações](./departamento.mdx#associar-aplicacoes-ao-departamento)).
  </Step>
</Steps>

<Callout type="info" title="Aplicações registadas automaticamente">
Muitas aplicações do IGRP registam‑se sozinhas quando arrancam pela primeira vez — o nome, slug, tipo e descrição chegam directamente do serviço. Nesse caso, o administrador só precisa de associá‑las aos departamentos correctos.
</Callout>

---

## Editar aplicação [#editar-aplicação]

No detalhe da aplicação, clicar em **Editar**. É possível alterar o **nome**, **slug**, **descrição**, **tipo** e **estado**. O **código** é imutável — é a referência que menus, permissões e associações utilizam.

<StudioPreview imageName="editApp" alt="Modal Editar Aplicação" />

---

## Detalhe da aplicação [#detalhe-da-aplicação]

A página de detalhe organiza a configuração em separadores. Os mais utilizados são **Menus** (a árvore de navegação apresentada aos utilizadores finais) e **Aplicações** (aplicações "filhas" — para casos em que uma aplicação agrupa outras).

<StudioPreview imageName="acApplicationDetail" alt="Página de detalhe da aplicação" />

---

## Gestão de menus [#gestão-de-menus]

Cada aplicação tem uma árvore de menus com os seguintes tipos disponíveis:

| Tipo                  | Utilização                                                                            |
|-----------------------|---------------------------------------------------------------------------------------|
| **Pasta**             | Nó organizador — agrupa outros nós, não é clicável.                                   |
| **Grupo**             | Cabeçalho dentro de uma pasta (categoria visual).                                     |
| **Página**            | Item clicável — abre uma página dentro da aplicação.                                  |
| **Página externa**    | Item clicável — abre uma página fora da aplicação (opção de abrir em novo separador). |
| **Página de sistema** | Reservado para páginas geridas pelo IGRP (ex.: convites, definições).                 |

### Criar menu [#criar-menu]

No separador **Menus**, clicar em **+ Novo Menu**. O primeiro passo é escolher o tipo:

<StudioPreview imageName="criarMenu" alt="Modal Criar Menu — escolher tipo" />

Consoante o tipo, o formulário adapta‑se:

| Campo                 | Pasta / Grupo | Página | Página externa |
|-----------------------|:---:|:---:|:---:|
| **Nome**              | ✔ | ✔ | ✔ |
| **Código**            | ✔ | ✔ | ✔ |
| **Ícone**             | ✔ | ✔ | ✔ |
| **Estado**            | ✔ | ✔ | ✔ |
| **Posição**           | ✔ | ✔ | ✔ |
| **URL interno**       | — | ✔ | — |
| **URL externo**       | — | — | ✔ |
| **Abrir em nova aba** | — | — | ✔ |

<StudioPreview imageName="menuGrupoPasta" alt="Formulário — Pasta ou Grupo" />
<StudioPreview imageName="menuPagina" alt="Formulário — Página" />

---

## Atribuir perfis a um menu [#atribuir-perfis-a-um-menu]

É a atribuição de **perfis** a cada entrada de menu que decide se ela é visível para um utilizador. Cada menu tem a acção **Atribuir Perfis**, que abre um modal com a lista dos perfis dos departamentos onde a aplicação está associada.

<StudioPreview imageName="acAddRolesToMenu" alt="Atribuir perfis a um menu" />

<Callout type="info" title="Efeito imediato para os utilizadores">
Depois de atribuir ou remover perfis a um menu, os utilizadores afectados vêem a navegação lateral actualizada logo à próxima interacção — não precisam de terminar sessão.
</Callout>

---

## Menus geridos pelo próprio projecto [#menus-geridos-pelo-próprio-projecto]

<Callout type="info" title="Alguns menus não são editáveis aqui">
Alguns projectos declaram a estrutura dos menus no próprio código; nesse caso a definição do código passa a ser a fonte da verdade e alterações feitas manualmente por este ecrã são substituídas na próxima sincronização. Se notar que uma edição sua é revertida, é este o motivo — nesse caso, o menu terá de ser alterado pela equipa técnica que mantém o projecto.
</Callout>

---

## Cross‑refs [#crossrefs]

- [Departamentos & Perfis](./departamento.mdx) — para associar a aplicação a departamentos e criar os perfis que vão gerir os menus.
- [Utilizadores & Convites](./user.mdx) — para atribuir esses perfis aos utilizadores.


# Gestão de Departamentos e Perfis

URL: /pt/docs/utilizador/access-manager/departamento

Hierarquia de departamentos, perfis por departamento e associação de aplicações, recursos e menus.

# Gestão de Departamentos e Perfis [#gestão-de-departamentos-e-perfis]

A área de **Gestão de Acessos** concentra três eixos: a **hierarquia de departamentos** (a estrutura organizacional), os **perfis** definidos por departamento (as _roles_ do RBAC) e a **associação de aplicações, recursos e menus** a cada departamento. Estes três eixos determinam, em conjunto, o que cada utilizador vê e pode fazer no Application Center.

<StudioPreview imageName="gestaoDepartamento" alt="Interface de Gestão de Departamentos" />

## Como aceder [#como-aceder]

Clicar no ícone de **Definições (⚙)** no canto superior direito do Application Center e selecionar o cartão **Gestão de Acessos**. A lista de departamentos é apresentada na barra lateral esquerda, com uma árvore que reflete a hierarquia pai → filho.

<StudioPreview imageName="departamento" alt="Lista de departamentos" />

<Callout type="info" title="Hierarquia de departamentos">
Um departamento pode ter um departamento‑pai. O modelo forma uma **árvore** — nunca um ciclo — e essa árvore é a base do **âmbito do gestor**: cada gestor só administra o seu próprio departamento e os descendentes, nunca ancestrais nem irmãos. Ver [Âmbito do Gestor (Hierarquia)](./manager-scope.mdx) para a regra completa.
</Callout>

---

## Criar departamento [#criar-departamento]

Clicar em **+ Novo Departamento** na barra lateral. Abre‑se um modal onde deverá preencher o nome, o código e, opcionalmente, uma descrição. Caso pretenda que o novo departamento seja filho de outro, selecione o **departamento‑pai** na lista.

<StudioPreview imageName="novoDepartamento" alt="Modal Criar Novo Departamento" />

<Steps>
  <Step>
    ### Preencher os campos [#preencher-os-campos]

    Preencher os seguintes campos para configurar o novo departamento:

    <Grid cols={2}>
      <Feature eyebrow="Campo 1" title="Nome">Nome descritivo do departamento. Ex.: `Recursos Humanos`</Feature>
      <Feature eyebrow="Campo 2" title="Código">Identificador único em maiúsculas (imutável após criação). Ex.: `DEPT_RH`</Feature>
      <Feature eyebrow="Campo 3" title="Descrição">Descrição opcional do departamento. Ex.: `Gestão de pessoas`</Feature>
      <Feature eyebrow="Campo 4" title="Departamento-pai">Opcional — coloca o novo departamento como filho na hierarquia. Ex.: `Direcção Geral`</Feature>
    </Grid>
  </Step>
  <Step>
    ### Departamento criado [#departamento-criado]

    Clicar em **Guardar** para guardar e sair, ou **Guardar e Novo** para continuar a criar outro departamento.
  </Step>
</Steps>

---

## Editar departamento [#editar-departamento]

No detalhe de um departamento, clicar em **Editar**, no canto superior direito. Abre‑se um modal com os dados atuais onde é possível actualizar o **nome**, a **descrição** e o **estado**. O **código** é imutável (é referenciado em roles/perfis por toda a plataforma).

<StudioPreview imageName="editDepartamento" alt="Modal Editar Departamento" />

<Callout type="warning" title="Eliminar departamento">
Eliminar um departamento remove‑o e a todos os seus **descendentes**. Os perfis, utilizadores e menus que dele dependam perdem imediatamente essas ligações — as sessões em curso ficam actualizadas na próxima interacção, sem necessidade de terminar sessão.
</Callout>

---

## Detalhe do departamento (separadores) [#detalhe-do-departamento-separadores]

O painel de detalhe de um departamento organiza a configuração em separadores. Cada separador é uma associação (_N para N_) entre o departamento e um outro recurso do sistema:

- **Perfis (Roles)** — os perfis definidos para este departamento (ver secção seguinte).
- **Aplicações** — as aplicações que os utilizadores deste departamento podem abrir.
- **Recursos** — os recursos (APIs) contra os quais este departamento pode ter permissões atribuídas.
- **Permissões** — as permissões efectivamente concedidas ao departamento (guia dedicado).
- **Menus** — os itens de menu, dentro de cada aplicação, que devem ficar visíveis para este departamento.

<Callout type="info" title="Regra do âmbito do gestor">
Em cada um destes separadores, a lista de _candidatos_ a associar (aplicações, recursos, menus, perfis, permissões) já vem pré‑filtrada pelo âmbito do gestor: um gestor do departamento X só pode associar itens que o próprio X ou os seus ancestrais autorizam. Ver [Âmbito do Gestor](./manager-scope.mdx).
</Callout>

### Associar aplicações ao departamento [#associar-aplicações-ao-departamento]

Na aba **Aplicações**, clicar em **+ Adicionar Aplicação**. Selecionar as aplicações pretendidas na tabela e clicar em **Guardar**. A operação é reversível — desmarcar remove a associação (a cascata desassocia também os menus e perfis daquela aplicação que dependessem da associação).

<StudioPreview imageName="acAddAppsToDept" alt="Adicionar aplicações ao departamento" />

### Associar recursos ao departamento [#associar-recursos-ao-departamento]

Na aba **Recursos**, é possível abrir o modal **Gerenciar Recursos** para escolher que serviços/APIs ficam disponíveis para o departamento. Um recurso é normalmente um serviço técnico (por exemplo, o serviço de facturação) que foi registado previamente na plataforma pela equipa técnica.

<StudioPreview imageName="acAddResourcesToDept" alt="Adicionar recursos ao departamento" />

### Associar menus ao departamento [#associar-menus-ao-departamento]

Na aba **Menus**, clicar em **+ Adicionar Menu**. A lista mostra os itens de menu de cada aplicação já associada. Basta marcar os que devem estar visíveis para os utilizadores deste departamento.

<StudioPreview imageName="acAddMenusToDept" alt="Adicionar menus ao departamento" />

<Callout type="warning" title="Remoções em cascata">
Ao remover um menu (ou uma permissão, ou uma aplicação) de um departamento, a plataforma remove **também** essas ligações em todos os perfis do departamento e nos seus descendentes. Assim, um perfil filho nunca fica com uma referência a algo que o pai já não lhe permite herdar.
</Callout>

### Permissões [#permissões]

A gestão de permissões (activar/desactivar por departamento, atribuir ou remover permissões a um perfil) é tratada num guia dedicado.

---

## Gestão de Perfis (Roles) [#gestão-de-perfis-roles]

Os **perfis** do IGRP implementam o núcleo do RBAC — são as _roles_ que agrupam permissões relacionadas. Cada perfil **pertence a um departamento** e pode ter um **perfil‑pai** dentro do mesmo departamento (sub‑perfis herdam automaticamente as permissões do pai).

### Criar perfil [#criar-perfil]

No detalhe do departamento, no separador **Perfis (Roles)**, clicar em **+ Novo Perfil**. Preencher o nome, o código e (opcionalmente) a descrição e o perfil‑pai.

<StudioPreview imageName="perfil" alt="Modal Criar Novo Perfil" />

<Steps>
  <Step>
    ### Preencher os campos [#preencher-os-campos-1]

    Preencher os campos que definem o perfil:

    <Grid cols={2}>
      <Feature eyebrow="Campo 1" title="Nome">Nome descritivo do perfil. Ex.: `Gestor de RH`</Feature>
      <Feature eyebrow="Campo 2" title="Código">Identificador único em maiúsculas (imutável). Ex.: `MANAGER`</Feature>
      <Feature eyebrow="Campo 3" title="Descrição">Descrição das responsabilidades (opcional). Ex.: `Gere pessoal RH`</Feature>
      <Feature eyebrow="Campo 4" title="Perfil-pai">Opcional — o novo perfil fica abaixo do pai: só pode receber permissões que o pai tem (não as herda) e o código passa a `PAI.CODIGO`. Ex.: `ADMIN`</Feature>
      <Feature eyebrow="Campo 5" title="Estado">Ativa ou desactiva o perfil. Ex.: `Ativo / Inativo`</Feature>
    </Grid>
  </Step>
  <Step>
    ### Perfil criado [#perfil-criado]

    Clicar em **Criar Perfil** para confirmar. O perfil passa a estar disponível para atribuição a utilizadores.
  </Step>
</Steps>

### Editar, criar sub‑perfil ou eliminar [#editar-criar-subperfil-ou-eliminar]

No separador **Perfis (Roles)**, cada linha tem um menu de acções **(⋯)** com quatro opções:

- **Editar** — actualizar nome, descrição e estado. O código do perfil é imutável.
- **Criar sub‑perfil** — abre o modal de criação já com o perfil actual pré‑selecionado como _perfil‑pai_.
- **Permissões** — gerir as permissões atribuídas ao perfil (ver o guia dedicado a Permissões).
- **Eliminar** — elimina o perfil. Todos os perfis filhos e todas as atribuições em utilizadores/menus deste perfil são removidos em cascata.

### Adicionar e Remover Perfil (a um utilizador) [#adicionar-e-remover-perfil-a-um-utilizador]

A associação de perfis a um utilizador é feita na página de detalhe do utilizador (ver [Utilizadores & Convites](./user.mdx)) — o modal **Adicionar ou Remover Perfis** permite filtrar por nome, por aplicação, definir data de expiração e marcar/desmarcar perfis.

<StudioPreview imageName="addRemovePerfil" alt="Modal Adicionar ou Remover Perfis" />

---

## Cross‑refs [#crossrefs]

- [Aplicações & Menus](./application.mdx) — para criar as aplicações e os menus que aparecem nas abas acima.
- [Utilizadores & Convites](./user.mdx) — para atribuir os perfis aqui criados a contas de utilizador.
- [Âmbito do Gestor (Hierarquia)](./manager-scope.mdx) — a regra que filtra o que aparece em cada modal de associação.
- Guia de _Permissões_ — para tudo o que envolve _atribuir/remover permissão_ a um perfil ou departamento.


# Gestão de Identidade e Acessos

URL: /pt/docs/utilizador/access-manager/identityAccess

Como a plataforma gere utilizadores, credenciais e autenticação, e as áreas de administração do Application Center.

# Gestão de Identidade e Acessos [#gestão-de-identidade-e-acessos]

Descreve como a plataforma gere os utilizadores e as suas identidades, como as credenciais de acesso são configuradas e como o processo de autenticação é realizado para garantir que apenas utilizadores autorizados acedem à plataforma.

## Home Page [#home-page]

Interface de autenticação — ponto de entrada para a plataforma IGRP.

<StudioPreview imageName="homePage" alt="Página de login do IGRP" />

---

## Login Page [#login-page]

A página de login é o ponto de entrada onde o utilizador introduz as suas credenciais para aceder à plataforma. Após a submissão, o sistema valida a identidade do utilizador e, em caso de sucesso, redireciona-o para a aplicação.

<StudioPreview imageName="loginPage" alt="Formulário de login do IGRP" />

---

## App Shell [#app-shell]

Após a autenticação, o utilizador é redirecionado para o Centro de Aplicações. Aqui são apresentadas todas as aplicações disponíveis, organizadas em internas e externas, com suporte a pesquisa rápida, acesso às aplicações recentes e fixação de favoritos para acesso direto.

<StudioPreview imageName="appShell" alt="App Shell — Centro de Aplicações do IGRP" />

---

## Principais Funcionalidades [#principais-funcionalidades]

<Grid cols={3}>
  <Feature icon="Users" color="cyan" title="Gestão de Utilizadores">
    Interface completa para criação, edição e gestão de utilizadores com perfis e permissões.

    <Badge color="cyan">Criar, Editar, Gerir</Badge>
  </Feature>
  <Feature icon="Building2" color="green" title="Gestão de Departamentos">
    Estrutura organizacional hierárquica com pesquisa, filtros e ações de gestão.

    <Badge color="green">Pesquisar e Filtrar</Badge>
  </Feature>
  <Feature icon="AppWindow" color="purple" title="Gestão de Aplicações">
    Dashboard centralizado para gerir aplicações com status, ações e configurações.

    <Badge color="purple">Ver e Editar</Badge>
  </Feature>
  <Feature icon="AlignJustify" color="orange" title="Gestão de Menus">
    Configuração hierárquica de menus com tipos, estados e posicionamento.

    <Badge color="orange">Criar e Configurar</Badge>
  </Feature>
  <Feature icon="UserPlus" color="teal" title="Sistema de Convites">
    Convites automáticos por email para novos utilizadores com configuração de permissões.

    <Badge color="teal">Convidar Utilizadores</Badge>
  </Feature>
  <Feature icon="Settings" color="red" title="Gestão de Perfis">
    Criação e gestão de perfis de utilizador com permissões granulares e hierárquicas.

    <Badge color="red">Permissões RBAC</Badge>
  </Feature>
</Grid>

---

## Explorar as áreas do Application Center [#explorar-as-áreas-do-application-center]

Cada bloco acima corresponde a uma área específica da administração. As páginas seguintes desta secção documentam cada uma delas com base na versão **0.2.0-beta**:

- [**Application Center — Home & Definições**](./application-center.mdx) — o que aparece depois do login, os cartões de aplicações fixadas e a página global de definições.
- [**Aplicações & Menus**](./application.mdx) — criar aplicações, gerir menus (pasta, grupo, página) e atribuir perfis a cada entrada de menu.
- [**Departamentos & Perfis**](./departamento.mdx) — hierarquia organizacional, gestão de perfis por departamento e associação de aplicações, recursos e menus.
- [**Utilizadores & Convites**](./user.mdx) — lista de utilizadores, convite (com envio por email + OTP) e atribuição de perfis.
- [**Fluxo de Convite (OTP)**](./invitation-flow.mdx) — o percurso completo do convidado, do email inicial à aceitação/rejeição.
- [**Âmbito do Gestor (Hierarquia)**](./manager-scope.mdx) — como um gestor só pode administrar o seu departamento e descendentes.

<Callout type="info" title="Documentação técnica">
Se está à procura de referência técnica (endpoints, configuração de integração, SDK Java, envio de notificações), a secção equivalente para _developers_ vive em **Access Manager (Integração)** no menu de _Desenvolvedores_.
</Callout>

<Callout type="info">
A gestão de **permissões** (atribuir/remover permissões a perfis e a departamentos) é tratada num guia dedicado.
</Callout>


# Fluxo de Convite (OTP)

URL: /pt/docs/utilizador/access-manager/invitation-flow

O percurso completo do convidado — email de convite, pedido de OTP, código por email e aceitação ou rejeição.

# Fluxo de Convite (OTP) [#fluxo-de-convite-otp]

Descreve o percurso completo de um convite — desde o momento em que o administrador o envia (ver [Utilizadores → Convidar](./user.mdx#convidar-utilizador)) até à activação da conta pelo convidado. Do lado do convidado, o percurso passa por quatro passos: **email de convite**, **pedido de código OTP**, **email com o código OTP**, e finalmente **aceitar ou rejeitar**.

## Como funciona, em resumo [#como-funciona-em-resumo]

```text
Administrador           Application Center            Convidado
   │                            │                            │
   │  envia convite             │                            │
   │──────────────────────────▶│                            │
   │                            │──── email de convite ─────▶│  ①
   │                            │                            │
   │                            │◀──── clique no link ───────│
   │                            │───── pede código OTP ─────▶│  ②
   │                            │───── envia email OTP ─────▶│  ③
   │                            │                            │
   │                            │◀──── OTP + Aceitar ────────│  ④
   │                            │  conta activada            │
   │                            │───── redirige p/ login ───▶│
```

---

## ① Email de convite [#-email-de-convite]

O convidado recebe um email com o nome do departamento, dos perfis que lhe foram atribuídos e um botão _Aceitar Convite_. Basta clicar para começar o processo.

<StudioPreview imageName="acInviteEmail" alt="Email de convite recebido" />

<Callout type="info" title="Reenviar ou cancelar um convite">
Um convite ainda por aceitar pode ser **reenviado** ou **cancelado** pelo administrador. Na lista de utilizadores, filtrar por _Estado = Convidado_, abrir o menu de acções **(⋯)** na linha do convite e escolher a acção pretendida. O convidado é notificado por email quando o convite é cancelado.
</Callout>

---

## ② Pedido de código OTP [#-pedido-de-código-otp]

Depois de clicar no link, o convidado é levado para a página _Aceitar convite_. Antes de conceder acesso, o Application Center pede um **código OTP** — uma sequência numérica de uso único enviada para o mesmo email.

<StudioPreview imageName="acInviteOtpPrompt" alt="Prompt de OTP" />

<Callout type="warning" title="Porque é que é preciso o OTP">
O OTP existe para proteger contra links de convite que possam ser reencaminhados por engano para outra pessoa. Sem o código enviado para o email registado, o convite não pode ser aceite mesmo que alguém tenha o link.
</Callout>

---

## ③ Email com o código OTP [#-email-com-o-código-otp]

Enquanto está no ecrã de OTP, o convidado recebe um segundo email — com o código a inserir. O código tem uma validade curta (normalmente cerca de 5 minutos) e, se expirar, pode ser gerado um novo através do botão _Reenviar OTP_.

<StudioPreview imageName="acInviteOtpEmail" alt="Email com código OTP" />

---

## ④ Aceitar ou Rejeitar [#-aceitar-ou-rejeitar]

Introduzido o código, o convidado escolhe entre **Aceitar** ou **Rejeitar** o convite. Ao aceitar, a conta é activada com o departamento e os perfis definidos pelo administrador, e o convidado é encaminhado para o ecrã de login para entrar pela primeira vez. Ao rejeitar, o convite fica marcado como _rejeitado_ e o administrador é notificado.

<StudioPreview imageName="acInviteAcceptReject" alt="Aceitar ou Rejeitar convite" />

---

## Estados possíveis do convite [#estados-possíveis-do-convite]

| Estado                    | Significado |
|---------------------------|-------------|
| **Aceitar convite**       | O link está válido — o convidado pode inserir o OTP e concluir a aceitação. |
| **Convite pendente**      | O convidado já aceitou; a conta ainda está a ser provisionada. |
| **Convite inválido**      | O link já não é válido — pode ter expirado, sido cancelado ou já respondido. |

---

## Cross‑refs [#crossrefs]

- [Utilizadores & Convites](./user.mdx) — para enviar, reenviar ou cancelar convites.
- [Application Center — Home & Definições](./application-center.mdx) — para o percurso geral do utilizador depois de aceitar.


# Âmbito do Gestor

URL: /pt/docs/utilizador/access-manager/manager-scope

A regra que determina o que um gestor consegue ver e alterar, em função da sua posição na árvore de departamentos.

# Âmbito do Gestor — Hierarquia de Departamentos [#âmbito-do-gestor--hierarquia-de-departamentos]

Explica a regra que determina **o que um gestor consegue ver e alterar** na Gestão de Acessos. Não é uma questão de ser gestor ou não — o âmbito depende da posição do gestor na **árvore de departamentos**.

## A regra [#a-regra]

<Callout type="warning" title="Regra do âmbito do gestor">
Um gestor administra **o seu próprio departamento e todos os departamentos que dele descendem**. Nunca vê nem altera:

- Departamentos **acima** do seu (pai, avô, …).
- Departamentos **irmãos** — ou os departamentos que dependem deles.
- Perfis, permissões, menus, aplicações ou utilizadores atribuídos a esses departamentos fora do seu âmbito.
</Callout>

A regra é aplicada de forma **uniforme**: o que aparece nos ecrãs, o que aparece nos modais para associar (aplicações, recursos, menus, perfis, permissões) e o que a plataforma aceita quando o gestor submete uma alteração — tudo passa pelo mesmo filtro. Não há forma de ir além do seu âmbito, mesmo tentando aceder directamente pelo URL: o Application Center responde com uma página de _acesso negado_.

### Exemplo: árvore de departamentos [#exemplo-árvore-de-departamentos]

Um gestor de **RH** administra o seu departamento e os descendentes; nunca vê a Direcção Geral, o Financeiro ou os departamentos que dependem do Financeiro.

- Direcção Geral (ancestral) <Badge color="neutral" outline>Fora do âmbito</Badge>
  - **RH** — o gestor está aqui <Badge color="orange">O seu departamento</Badge>
    - RH — Cabo Verde <Badge color="blue">Dentro do âmbito</Badge>
    - RH — Angola <Badge color="blue">Dentro do âmbito</Badge>
  - Financeiro (irmão) <Badge color="neutral" outline>Fora do âmbito</Badge>
    - Contabilidade <Badge color="neutral" outline>Fora do âmbito</Badge>
    - Tesouraria <Badge color="neutral" outline>Fora do âmbito</Badge>

---

## Como se vê no dia‑a‑dia [#como-se-vê-no-diaadia]

Todos os modais de "adicionar … ao departamento" no [detalhe do departamento](./departamento.mdx#detalhe-do-departamento-separadores) apresentam já uma lista reduzida — só aparecem os itens que o gestor pode legitimamente atribuir.

### Exemplos [#exemplos]

<StudioPreview imageName="acAddAppsToDept" alt="Adicionar aplicações — lista pré-filtrada" />
<StudioPreview imageName="acAddResourcesToDept" alt="Adicionar recursos — lista pré-filtrada" />
<StudioPreview imageName="acAddMenusToDept" alt="Adicionar menus — lista pré-filtrada" />

<Callout type="info" title="A lista pode aparecer vazia">
Se abrir um modal de associação e a lista aparecer vazia, isso **não** quer dizer que não existem candidatos — quer dizer que os que existem estão fora do seu âmbito. Nesse caso, um administrador de um departamento acima do seu terá de fazer a associação por si.
</Callout>

---

## O que abrange a regra [#o-que-abrange-a-regra]

A regra aplica‑se a todas as operações de escrita sobre recursos com âmbito departamental. Em termos práticos, isto cobre:

| Área              | Operações abrangidas                                                                              |
|-------------------|---------------------------------------------------------------------------------------------------|
| **Departamentos** | Criar, editar, eliminar. Associar ou remover perfis do departamento.                              |
| **Perfis**        | Criar, editar, eliminar. Adicionar ou remover perfis a contas de utilizador.                      |
| **Aplicações**    | Associar ou remover aplicações ao departamento. Eliminar aplicações.                              |
| **Recursos**      | Associar ou remover recursos ao departamento.                                                     |
| **Menus**         | Associar ou remover menus ao departamento. Atribuir ou remover perfis a um menu. Eliminar menus.  |
| **Permissões**    | Adicionar ou remover permissões ao departamento. Ver o guia de _Permissões_.                      |

<Callout type="info" title="Remoções em cascata">
Quando um recurso (aplicação, menu, permissão) é **removido** de um departamento, a associação é também removida em todos os departamentos que dele descendem. É assim que a plataforma garante que um departamento filho nunca fica com uma referência a algo que o pai já não lhe permite.
</Callout>

---

## Quanto tempo demora a produzir efeito [#quanto-tempo-demora-a-produzir-efeito]

As alterações à hierarquia, aos perfis ou às atribuições de perfis a utilizadores produzem efeito **imediato** nos ecrãs e nas próximas acções desses utilizadores. Sessões em curso são reavaliadas na próxima interacção com a plataforma — na prática, sem necessidade de o utilizador afectado voltar a fazer login.

---

## Situações comuns [#situações-comuns]

| Situação                                                              | O que acontece |
|-----------------------------------------------------------------------|----------------|
| Gestor tenta abrir um departamento fora do seu âmbito.                | Recebe uma página de _acesso negado_. |
| Gestor tenta atribuir um perfil que não existe no seu âmbito.         | A opção nem sequer aparece na lista de perfis a atribuir. |
| Gestor tenta associar uma aplicação que não lhe está atribuída.        | A aplicação nem sequer aparece na lista para associar. |
| Gestor consulta a barra lateral e não vê nenhum sub‑departamento.      | Não é um erro — o gestor está numa "folha" da árvore, sem departamentos abaixo. |

---

## Cross‑refs [#crossrefs]

- [Departamentos & Perfis](./departamento.mdx) — hierarquia e associações a que a regra se aplica.
- [Utilizadores & Convites](./user.mdx) — atribuição de perfis (também restringida pelo âmbito).


# Gestão de Utilizadores

URL: /pt/docs/utilizador/access-manager/user

Convidar utilizadores, consultar o detalhe de uma conta e atribuir ou remover perfis.

# Gestão de Utilizadores [#gestão-de-utilizadores]

A gestão de utilizadores no IGRP cobre três operações fundamentais: **convidar** um novo utilizador (por email, com fluxo de aceitação e OTP); **consultar** os dados de uma conta existente através da página de detalhe; e **atribuir/remover perfis** — que é o que efectivamente concede acesso a aplicações e permissões.

<StudioPreview imageName="user" alt="Lista de Utilizadores" />

## Como aceder [#como-aceder]

No Application Center, clicar no ícone de **Definições (⚙)** e selecionar **Utilizadores**. A lista suporta pesquisa por nome, email ou departamento e permite filtrar por estado (activo / bloqueado / convidado).

<StudioPreview imageName="interfaceUser" alt="Interface de Utilizadores" />

---

## Convidar utilizador [#convidar-utilizador]

Para adicionar um novo utilizador clicar em **+ Convidar Utilizador**. Abre‑se o modal onde deverão ser preenchidos o email, o departamento inicial, os perfis a atribuir e — opcionalmente — uma data de expiração para o convite.

<StudioPreview imageName="inviteUser" alt="Modal Convidar Utilizador" />

<Steps>
  <Step>
    ### Preencher os campos do convite [#preencher-os-campos-do-convite]

    <Grid cols={2}>
      <Feature eyebrow="Campo 1" title="Email">Endereço para onde o convite é enviado. Ex.: `ana@example.cv`</Feature>
      <Feature eyebrow="Campo 2" title="Departamento">Departamento a que a conta ficará associada. Ex.: `Recursos Humanos`</Feature>
      <Feature eyebrow="Campo 3" title="Perfis">Perfis atribuídos à conta após a aceitação. Ex.: `Gestor de RH`</Feature>
      <Feature eyebrow="Campo 4" title="Expiração">Opcional — o convite deixa de ser aceite após esta data. Ex.: `dd/mm/aaaa`</Feature>
    </Grid>
  </Step>
  <Step>
    ### Enviar convite [#enviar-convite]

    Clicar em **Enviar Convite**. O convidado recebe um email — o percurso completo (email → OTP → aceitação) está descrito em [Fluxo de Convite (OTP)](./invitation-flow.mdx).
  </Step>
</Steps>

<Callout type="info" title="Como os emails são entregues">
Consoante a configuração feita pela equipa técnica da sua organização, o email pode ser entregue directamente pelo servidor de correio da plataforma ou através de um serviço central de notificações — com _templates_ personalizáveis. Do ponto de vista do administrador não muda nada; o convite é sempre enviado ao clicar em _Enviar Convite_.
</Callout>

### Gerir convites pendentes [#gerir-convites-pendentes]

Na página de utilizadores, o filtro **Estado = Convidado** mostra os convites por aceitar. Cada linha tem um menu de acções com **Reenviar**, **Cancelar** e **Ver detalhes**. Convites expirados aparecem marcados como tal e não permitem reenvio; será necessário criar um novo.

---

## Detalhe do utilizador [#detalhe-do-utilizador]

Ao abrir uma conta, a página de detalhe agrupa a informação em separadores:

| Separador          | Conteúdo |
|--------------------|----------|
| **Perfis**         | Perfis atribuídos, o departamento de origem de cada perfil e a respectiva data de expiração. |
| **Departamentos**  | Todos os departamentos onde a conta foi colocada (o utilizador pode pertencer a mais que um). |
| **Aplicações**     | Aplicações a que o utilizador tem acesso, resultado da união dos perfis. |
| **Assinatura**     | Fichas de assinatura digital do utilizador (quando aplicável). |
| **Sessões**        | Sessões activas — cada uma com dispositivo, hora de login e possibilidade de revogar. |
| **Auditoria**      | Registo cronológico das operações relevantes na conta (logins, alterações, revogações). |
| **Metadados**      | Informação adicional da conta (identificador único, nome de utilizador preferido, se o email foi verificado, sistema de autenticação de origem, etc.). |

<StudioPreview imageName="perfilAtribuido" alt="Detalhe do utilizador — perfis atribuídos" />

---

## Adicionar/Remover perfis a um utilizador [#adicionarremover-perfis-a-um-utilizador]

No separador **Perfis**, clicar em **+ Adicionar** abre o modal **Adicionar ou Remover Perfis**. Aqui é possível pesquisar por nome, filtrar por aplicação, definir uma data de expiração para a atribuição e marcar/desmarcar perfis na tabela.

<StudioPreview imageName="addRemovePerfil" alt="Modal Adicionar ou Remover Perfis" />

<Callout type="info" title="Regra do âmbito do gestor">
A lista de perfis oferecida a um gestor está restrita aos perfis do seu departamento e descendentes. Um gestor não vê perfis de departamentos irmãos ou de ancestrais — mesmo que a conta a editar já os tenha. Ver [Âmbito do Gestor](./manager-scope.mdx).
</Callout>

<Callout type="warning" title="Efeito imediato">
Ao adicionar ou remover um perfil, as sessões activas do utilizador ganham ou perdem as permissões correspondentes de imediato — sem necessidade de terminar sessão e voltar a entrar.
</Callout>

---

## Cross‑refs [#crossrefs]

- [Fluxo de Convite (OTP)](./invitation-flow.mdx) — o percurso completo do lado do convidado.
- [Departamentos & Perfis](./departamento.mdx) — para criar os perfis que aqui são atribuídos.
- [Aplicações & Menus](./application.mdx) — para saber que aplicações e menus são visíveis por consequência.


# Contactar suporte

URL: /pt/docs/utilizador/ajuda/contactar-suporte

Como pedir ajuda quando não consegues resolver um problema no IGRP Studio.

# Contactar suporte [#contactar-suporte]

## Antes de contactar [#antes-de-contactar]

<Checklist>
  <Item title={<a href="./perguntas-frequentes.mdx">Perguntas frequentes</a>}>Para dúvidas comuns</Item>
  <Item title={<a href="./erros-comuns.mdx">Erros comuns</a>}>Para mensagens de erro específicas</Item>
</Checklist>

## Quem contactar [#quem-contactar]

O primeiro passo é falar com o **administrador da tua organização** — a pessoa responsável pelo IGRP Studio na tua empresa.

<Grid cols={2}>
  <Feature icon="KeyRound" color="cyan" title="Repor palavras-passe">
    O administrador pode repor o acesso à tua conta.
  </Feature>
  <Feature icon="UserCog" color="purple" title="Alterar perfis e permissões">
    Ajusta o que cada utilizador pode ver e fazer.
  </Feature>
  <Feature icon="Mail" color="green" title="Reenviar convites">
    Se o convite expirou ou não chegou, o administrador reenvia.
  </Feature>
  <Feature icon="ShieldCheck" color="amber" title="Resolver problemas de acesso">
    Se nada resolver, o administrador escala para o suporte técnico do IGRP.
  </Feature>
</Grid>

## O que incluir quando pedires ajuda [#o-que-incluir-quando-pedires-ajuda]

| O que indicar | Exemplo |
|---|---|
| O que estavas a fazer | Tentei criar um novo projecto |
| O que aconteceu | Apareceu um erro a vermelho |
| A mensagem de erro exacta | Captura de ecrã |
| Quando aconteceu | Hoje às 14:30 |
| O teu browser | Chrome versão 123 |

Uma captura de ecrã do erro é sempre muito útil.

## Canal de comunidade [#canal-de-comunidade]

Para questões gerais e sugestões, podes usar o canal de comunidade Discord do IGRP.

<Callout type="info" title="Nota">
  O Discord é para a comunidade de developers e utilizadores avançados. Para problemas urgentes de acesso, contacta directamente o teu administrador.
</Callout>


# Erros comuns

URL: /pt/docs/utilizador/ajuda/erros-comuns

O que fazer quando algo não funciona no IGRP Studio.

# Erros comuns [#erros-comuns]

## Erros de acesso [#erros-de-acesso]

<Checklist>
  <Item status="warn" title='"Utilizador ou palavra-passe incorrectos"'>Verifica se o email está correcto e se as maiúsculas estão certas. Se persistir, usa **Esqueci a palavra-passe**.</Item>
  <Item status="warn" title='"A tua conta foi suspensa"'>O teu acesso foi suspenso pelo administrador. Contacta a tua organização para perceber o motivo.</Item>
  <Item status="warn" title='"Não tens permissão para aceder a esta página"'>O teu perfil não permite este acesso. Fala com o administrador se precisares.</Item>
</Checklist>

## Erros de carregamento [#erros-de-carregamento]

<Accordions>
  <Accordion title="A página fica em branco ou a carregar indefinidamente">
    <Steps>
      <Step>Actualiza a página com `F5`</Step>
      <Step>Limpa a cache do browser: `Ctrl + Shift + Delete`</Step>
      <Step>Tenta noutro browser (Chrome, Firefox, Edge)</Step>
      <Step>Se persistir, reporta ao administrador com o nome da página</Step>
    </Steps>
  </Accordion>
</Accordions>

<Checklist>
  <Item status="warn" title='"Erro de ligação" ou a página não abre'>Verifica a tua ligação à internet. Se funciona mas o Studio não abre, pode ser manutenção — aguarda e tenta novamente.</Item>
</Checklist>

## Erros em projectos [#erros-em-projectos]

<Checklist>
  <Item status="warn" title='"Não foi possível criar o projecto"'>Problema temporário do servidor. Tenta novamente. Se persistir, reporta ao administrador.</Item>
  <Item status="warn" title="Os serviços de um projecto aparecem a vermelho">Requer intervenção técnica. Reporta ao developer ou administrador responsável.</Item>
  <Item status="warn" title='"Acesso negado ao projecto"'>Não tens permissão. Pede ao administrador que te adicione ao projecto.</Item>
</Checklist>

## Como reportar um problema [#como-reportar-um-problema]

Quando reportares um erro inclui sempre:

| O que indicar | Exemplo |
|---|---|
| O que estavas a fazer | Tentei criar um novo projecto |
| O que aconteceu | Apareceu um erro a vermelho |
| A mensagem de erro exacta | Captura de ecrã |
| Quando aconteceu | Hoje às 14:30 |
| O teu browser | Chrome versão 123 |


# Perguntas frequentes

URL: /pt/docs/utilizador/ajuda/perguntas-frequentes

Respostas às perguntas mais comuns sobre o IGRP Studio.

# Perguntas frequentes [#perguntas-frequentes]

## Acesso e login [#acesso-e-login]

<Accordions>
  <Accordion title="Esqueci a palavra-passe. O que faço?">
    Na página de login, clica em **Esqueci a palavra-passe**, introduz o teu email e segue as instruções. O link é válido por 24 horas.
  </Accordion>
  <Accordion title="Não consigo fazer login — a conta diz que não tem acesso.">
    O teu acesso pode ter sido suspenso. Fala com o administrador da tua organização.
  </Accordion>
  <Accordion title="O link do convite não funciona.">
    Os convites expiram ao fim de 72 horas. Pede ao administrador que reenvie o convite.
  </Accordion>
  <Accordion title="Posso usar o Studio em dispositivos móveis?">
    Sim. O IGRP Studio funciona no browser de qualquer dispositivo. Para melhor experiência, usa tablet ou computador.
  </Accordion>
</Accordions>

## Projectos [#projectos]

<Accordions>
  <Accordion title="Não vejo um projecto que devia estar aqui.">
    O projecto pode estar arquivado ou não tens permissão. Filtra por **Arquivado** ou fala com o administrador.
  </Accordion>
  <Accordion title="Não consigo criar projectos — o botão não aparece.">
    O teu perfil é provavelmente **Visualizador**. Pede ao administrador que mude para Editor.
  </Accordion>
  <Accordion title="Criei um projecto por engano. Como apago?">
    Primeiro arquiva (`⋮` → **Arquivar**). Depois, na lista de arquivados, podes apagar permanentemente. Esta acção é irreversível.
  </Accordion>
</Accordions>

## Utilizadores e acessos [#utilizadores-e-acessos]

<Accordions>
  <Accordion title="Como sei qual é o meu perfil?">
    Clica no teu nome no canto superior direito. O teu perfil aparece abaixo do nome.
  </Accordion>
  <Accordion title="Posso ter mais do que um perfil?">
    Não — cada utilizador tem um único perfil activo. Fala com o administrador se precisares de permissões diferentes.
  </Accordion>
  <Accordion title="Convidei um colega mas ele diz que não recebeu o email.">
    Pede que verifique a pasta de spam. Se não estiver lá, reenvia o convite pelo Access Manager.
  </Accordion>
</Accordions>

## Interface [#interface]

<Accordions>
  <Accordion title="A plataforma está a carregar muito devagar.">
    Tenta actualizar a página (`F5`). Se persistir, limpa a cache do browser ou tenta noutro browser.
  </Accordion>
  <Accordion title="Uma funcionalidade que usava deixou de aparecer.">
    O teu perfil pode ter sido alterado. Verifica o perfil actual e, se necessário, contacta o administrador.
  </Accordion>
</Accordions>


# Bem-vindo

URL: /pt/docs/utilizador/bem-vindo

Guia do utilizador — o que é o IGRP Studio, o que podes fazer e como aceder.

# Bem-vindo ao IGRP Studio [#bem-vindo-ao-igrp-studio]

O IGRP Studio é a plataforma onde a tua equipa cria e gere aplicações digitais. Pensa nele como o painel de controlo do trabalho da tua organização — tudo num só lugar, acessível pelo browser, sem precisar de instalar nada.

## O que é o IGRP Studio [#o-que-é-o-igrp-studio]

Não precisas de perceber como o Studio funciona por baixo. Termos como Docker, microserviços, APIs ou bases de dados não são relevantes para o teu dia a dia. O Studio trata disso por ti.

<Grid cols={2}>
  <Feature icon="User" color="cyan" title="Utilizador">
    Gere projetos, convida pessoas e acompanha o trabalho — é o teu caso se estás a ler esta documentação.
  </Feature>
  <Feature icon="Code2" color="purple" title="Developer">
    Instala, configura e constrói as aplicações por baixo. Tem a sua própria secção de documentação.
  </Feature>
</Grid>

## O que podes fazer [#o-que-podes-fazer]

<Grid cols={2}>
  <Feature icon="FolderKanban" color="cyan" title="Gerir projetos">
    Cria, abre e acompanha os projetos da tua organização. Arquiva os que já não estão activos.
  </Feature>
  <Feature icon="Users" color="green" title="Gerir pessoas e acessos">
    Convida colegas por email, atribui perfis e gere permissões a qualquer momento.
  </Feature>
  <Feature icon="ShieldCheck" color="purple" title="Controlar acessos">
    Define quem pode aceder a cada projeto e com que nível de permissão.
  </Feature>
  <Feature icon="ClipboardList" color="amber" title="Acompanhar o trabalho">
    O dashboard dá-te uma visão geral do estado da plataforma e das actividades recentes.
  </Feature>
</Grid>

### O que depende do teu perfil [#o-que-depende-do-teu-perfil]

| Permissão | <Badge color="neutral" icon="Eye">Visualizador</Badge> | <Badge color="blue" icon="Pencil">Editor</Badge> | <Badge color="amber" icon="Crown">Administrador</Badge> |
|---|:---:|:---:|:---:|
| Ver projetos | ✅ | ✅ | ✅ |
| Criar projetos | ❌ | ✅ | ✅ |
| Convidar utilizadores | ❌ | ❌ | ✅ |
| Gerir acessos | ❌ | ❌ | ✅ |
| Arquivar projetos | ❌ | ✅ | ✅ |

<Callout type="info" title="Nota">
  Não vês uma funcionalidade? Se não consegues aceder a algo que precisas, fala com o administrador da tua organização. Ele pode ajustar o teu perfil.
</Callout>

## Como aceder [#como-aceder]

<Accordions>
  <Accordion title="Primeiro acesso">
    <Checklist>
      <Item title="Verifica o teu email">Deves ter recebido um convite com o link de acesso</Item>
      <Item title="Clica no link do convite">Serás redirecionado para definir a tua palavra-passe</Item>
      <Item title="Confirma a palavra-passe">Após confirmar, és redirecionado automaticamente para o Studio</Item>
    </Checklist>

    <Callout type="info" title="Nota">
      Não recebeste o convite? Verifica a pasta de spam. Se não estiver lá, pede ao administrador da tua organização que reenvie o convite.
    </Callout>
  </Accordion>

  <Accordion title="Acessos seguintes">
    <Checklist>
      <Item title="Abre o browser">Chrome, Firefox, Edge ou Safari</Item>
      <Item title="Vai ao endereço da plataforma">Fornecido pelo teu administrador</Item>
      <Item title={<>Introduz o teu <strong>utilizador</strong> e <strong>palavra-passe</strong> e clica em <strong>Entrar</strong></>}>O dashboard abre automaticamente após o login</Item>
    </Checklist>
  </Accordion>

  <Accordion title="Esqueci a palavra-passe">
    <Checklist>
      <Item title={<>Na página de login, clica em <strong>Esqueci a palavra-passe</strong></>} />
      <Item title="Introduz o teu endereço de email" />
      <Item title="Verifica o email e clica no link para redefinir a palavra-passe">O link é válido por **24 horas**</Item>
    </Checklist>
  </Accordion>

  <Accordion title="Problemas de acesso">
    <Checklist>
      <Item status="warn" title='"Utilizador ou palavra-passe incorrectos"'>Verifica o email e a palavra-passe. As maiúsculas contam.</Item>
      <Item status="warn" title='"Conta sem acesso"'>Fala com o administrador — o teu acesso pode ter expirado.</Item>
      <Item status="warn" title="A página não carrega">Verifica a ligação à internet e tenta noutro browser.</Item>
      <Item status="warn" title="O link do convite expirou">Pede ao administrador que reenvie o convite.</Item>
    </Checklist>

    <Callout type="warning" title="Sessão expirada">
      Por segurança, a sessão expira após um período de inactividade. Basta fazer login novamente — os teus dados não se perdem.
    </Callout>
  </Accordion>
</Accordions>


# Dashboard principal

URL: /pt/docs/utilizador/interface/dashboard-principal

O que encontras no dashboard do IGRP Studio e como navegar.

# Dashboard principal [#dashboard-principal]

O dashboard é a primeira página que vês após o login. É o ponto de partida para tudo o que fazes no IGRP Studio.

## O que encontras no dashboard [#o-que-encontras-no-dashboard]

<Grid cols={2}>
  <Feature icon="PanelLeft" color="cyan" title="Barra lateral esquerda">
    Navegação principal. Acedes a Espaços de Trabalho, Projectos e Base de Dados.
  </Feature>
  <Feature icon="LayoutGrid" color="purple" title="Área central">
    Mostra os projectos do workspace activo. Se não existirem, aparece a opção de criar o primeiro.
  </Feature>
  <Feature icon="MonitorDot" color="green" title="Barra superior">
    Acesso a Executar, Pesquisa, Configurações e Notificações do Studio.
  </Feature>
  <Feature icon="Activity" color="amber" title="Secção de Serviços">
    No fundo do ecrã. Verde significa activo, vermelho significa problema.
  </Feature>
</Grid>

## Ordenar e filtrar projectos [#ordenar-e-filtrar-projectos]

No topo da lista de projectos podes alternar a vista e a ordenação:

<Grid cols={3}>
  <Feature icon="LayoutGrid" title="Grelha">
    Vista em cartões — boa para ver muitos projectos de uma vez.
  </Feature>
  <Feature icon="List" title="Lista">
    Vista em linha — boa para comparar detalhes entre projectos.
  </Feature>
  <Feature icon="ArrowUpDown" title="Ordenar por">
    Última modificação, nome ou data de criação.
  </Feature>
</Grid>

## Criar novo projecto [#criar-novo-projecto]

Clica em **Criar Novo Projecto** no canto superior direito para iniciar o assistente de criação.


# Menus e navegação

URL: /pt/docs/utilizador/interface/menus-e-navegacao

Como navegar pelo IGRP Studio — menus, atalhos e barra lateral.

# Menus e navegação [#menus-e-navegação]

## Barra lateral [#barra-lateral]

A barra lateral esquerda é a tua referência principal:

<Checklist>
  <Item title="Topo">Logótipo e nome do workspace activo</Item>
  <Item title="Espaços de Trabalho">Lista de workspaces com pesquisa integrada</Item>
  <Item title="Projectos">Projectos do workspace activo</Item>
  <Item title="Base de Dados">Gestão de dados (se o teu perfil permitir)</Item>
  <Item title="Fundo">Botão de suporte e versão do Studio</Item>
</Checklist>

## Barra superior [#barra-superior]

<Grid cols={2}>
  <Feature icon="Play" color="green" title="Executar">Inicia ou para o workspace activo</Feature>
  <Feature icon="Settings" color="neutral" title="Configurações">Abre as definições do Studio</Feature>
  <Feature icon="Bell" color="amber" title="Notificações">Alertas de sistema e actualizações</Feature>
  <Feature icon="UserCircle" color="blue" title="Perfil">O teu nome, perfil activo e logout</Feature>
</Grid>

## Navegar entre workspaces [#navegar-entre-workspaces]

<Steps>
  <Step>Clica no nome do workspace no topo da barra lateral</Step>
  <Step>Aparece a lista de workspaces disponíveis</Step>
  <Step>Clica no workspace pretendido</Step>
</Steps>

## Atalhos de teclado úteis [#atalhos-de-teclado-úteis]

| Atalho | Acção |
|---|---|
| <kbd>Ctrl + B</kbd> | Colapsar barra lateral |
| <kbd>Ctrl + N</kbd> | Novo projecto |
| <kbd>Ctrl + S</kbd> | Guardar |
| <kbd>Ctrl + ,</kbd> | Configurações |
| <kbd>F1</kbd> | Ajuda |

<Callout type="tip">
  Se ficares perdido em qualquer página, clica no logótipo do IGRP no topo esquerdo para voltares ao dashboard.
</Callout>


# Pesquisa e filtros

URL: /pt/docs/utilizador/interface/pesquisa-e-filtros

Como encontrar projectos, utilizadores e conteúdo no IGRP Studio.

# Pesquisa e filtros [#pesquisa-e-filtros]

## Pesquisa global [#pesquisa-global]

A barra de pesquisa está na barra superior. Para activar:

<Checklist>
  <Item title="Clica no ícone de lupa na barra superior">Os resultados aparecem em tempo real</Item>
  <Item title={<>Ou usa o atalho <kbd>Ctrl + K</kbd></>}>Em páginas com listas, a pesquisa filtra automaticamente conforme escreves</Item>
</Checklist>

## Filtrar projectos [#filtrar-projectos]

<Grid cols={2}>
  <Feature icon="Filter" title="Estado">activo, em pausa, arquivado</Feature>
  <Feature icon="CalendarPlus" title="Data de criação">do mais recente ao mais antigo</Feature>
  <Feature icon="Clock" title="Última modificação" />
  <Feature icon="ArrowDownAZ" title="Nome">ordenação alfabética</Feature>
</Grid>

## Não encontras o que procuras? [#não-encontras-o-que-procuras]

<Checklist>
  <Item status="warn" title="O projecto não aparece">Pode estar arquivado ou não tens acesso</Item>
  <Item status="warn" title="O utilizador não aparece">O convite pode ainda não ter sido aceite</Item>
  <Item status="warn" title="A lista aparece vazia">Tens um filtro activo — limpa os filtros</Item>
</Checklist>

<Callout type="tip">
  Para ver projectos arquivados, vai a **Projectos** → muda o filtro de estado para **Arquivado**.
</Callout>


# Arquivar e gerir

URL: /pt/docs/utilizador/projetos/arquivar-e-gerir

Como arquivar, restaurar e gerir o ciclo de vida dos projectos no IGRP Studio.

# Arquivar e gerir projectos [#arquivar-e-gerir-projectos]

Arquivar remove o projecto da lista principal sem o apagar. Os dados ficam guardados e pode ser restaurado a qualquer momento.

## Arquivar um projecto [#arquivar-um-projecto]

<Steps>
  <Step>Na lista de projectos, clica no ícone `⋮` no cartão do projecto</Step>
  <Step>Selecciona **Arquivar**</Step>
  <Step>Confirma a acção</Step>
</Steps>

## Ver projectos arquivados [#ver-projectos-arquivados]

<Steps>
  <Step>Na lista de projectos, clica em **Ordenar por**</Step>
  <Step>Muda o filtro de estado para **Arquivado**</Step>
</Steps>

## Restaurar um projecto arquivado [#restaurar-um-projecto-arquivado]

<Steps>
  <Step>Encontra o projecto arquivado</Step>
  <Step>Clica no ícone `⋮` do projecto</Step>
  <Step>Selecciona **Restaurar**</Step>
</Steps>

## Apagar um projecto [#apagar-um-projecto]

<Callout type="error" title="Acção irreversível">
  Apagar remove permanentemente todos os dados. Usa apenas quando tens a certeza absoluta.
</Callout>

<Steps>
  <Step>O projecto tem de estar arquivado primeiro</Step>
  <Step>Clica em `⋮` → **Apagar permanentemente**</Step>
  <Step>Escreve o nome do projecto para confirmar</Step>
</Steps>

## Resumo de permissões [#resumo-de-permissões]

| Acção | Reversível | Editor | Administrador |
|---|---|---|---|
| **Arquivar** | <Badge color="green" icon="Check">Sim</Badge> | <Badge color="green" icon="Check">Sim</Badge> | <Badge color="green" icon="Check">Sim</Badge> |
| **Restaurar** | <Badge>N/A</Badge> | <Badge color="green" icon="Check">Sim</Badge> | <Badge color="green" icon="Check">Sim</Badge> |
| **Apagar** | <Badge color="red" icon="X">Irreversível</Badge> | <Badge color="red" icon="X">Não</Badge> | <Badge color="green" icon="Check">Sim</Badge> |


# Criar projeto (sem código)

URL: /pt/docs/utilizador/projetos/criar-projeto

Como criar um novo projecto no IGRP Studio sem precisar de escrever código.

# Criar projeto (sem código) [#criar-projeto-sem-código]

## Antes de começar [#antes-de-começar]

<Checklist>
  <Item title={<>Perfil de <strong>Editor</strong> ou <strong>Administrador</strong></>}>Se o botão "Criar Novo Projecto" não aparece, fala com o teu administrador</Item>
</Checklist>

## Passo a passo [#passo-a-passo]

<Steps>
  <Step>
    ### Abrir o assistente [#abrir-o-assistente]

    No dashboard, clica em **Criar Novo Projecto** no canto superior direito.
  </Step>
  <Step>
    ### Dar um nome ao projecto [#dar-um-nome-ao-projecto]

    Escolhe um nome claro que identifique o propósito do projecto.

    <Grid cols={2}>
      <Feature icon="Check" color="green" title="Bom exemplo">`Portal de Recursos Humanos`</Feature>
      <Feature icon="X" color="red" title="Mau exemplo">`Projecto1` ou `Teste`</Feature>
    </Grid>
  </Step>
  <Step>
    ### Adicionar uma descrição (opcional) [#adicionar-uma-descrição-opcional]

    Descreve brevemente o que este projecto vai fazer. Ajuda os colegas a perceber o contexto.
  </Step>
  <Step>
    ### Confirmar a criação [#confirmar-a-criação]

    Clica em **Criar**. O Studio cria o projecto automaticamente e redireccionas-te para ele.
  </Step>
</Steps>

## O que acontece automaticamente [#o-que-acontece-automaticamente]

<Steps>
  <Step>A estrutura base do projecto é gerada</Step>
  <Step>As configurações iniciais são aplicadas</Step>
  <Step>Os acessos para a tua conta são criados</Step>
</Steps>

<Callout type="info" title="Nota">
  A criação de um projecto regista-se automaticamente no histórico do workspace com o teu nome e data.
</Callout>


# Ver e abrir projectos

URL: /pt/docs/utilizador/projetos/ver-e-abrir-projetos

Como ver a lista de projectos disponíveis e abrir um projecto no IGRP Studio.

# Ver e abrir projectos [#ver-e-abrir-projectos]

Ao abrir o IGRP Studio, a área central mostra os projectos disponíveis. Cada cartão de projecto contém:

<Grid cols={2}>
  <Feature icon="FileText" title="Nome">do projecto</Feature>
  <Feature icon="Activity" title="Estado">activo, em pausa ou arquivado</Feature>
  <Feature icon="Clock" title="Última modificação" />
  <Feature icon="MoreVertical" title="Menu">`⋮` para acções rápidas</Feature>
</Grid>

Clica no nome do projecto ou no cartão para o abrir. O projecto abre na sua página principal.

## Estados de um projecto [#estados-de-um-projecto]

<Grid cols={2}>
  <Feature icon="CheckCircle" color="green" title="Activo">O projecto está a correr normalmente — podes usar e editar.</Feature>
  <Feature icon="PauseCircle" color="amber" title="Em pausa">Pausado temporariamente — contacta o administrador para retomar.</Feature>
  <Feature icon="Archive" color="neutral" title="Arquivado">Desactivado — pode ser restaurado pelo administrador.</Feature>
  <Feature icon="AlertTriangle" color="red" title="Erro">Algo não está a funcionar — reporta ao administrador.</Feature>
</Grid>

<Callout type="info" title="Nota">
  Não vês um projecto que devia estar aqui? Pode estar arquivado ou não tens permissão de acesso. Fala com o administrador.
</Callout>
