Migração frontend — APIs de utilizador (auth)
Rotas actuais: apis/02-auth.md, legado, catálogo.
Guia para o time de frontend corrigir integrações após a mudança de user_profiles (tabela única, removida) para o modelo atual em G_DB_POSTGRESS:
- Tabela principal:
public."user" - Dados relacionais:
user_addresses,user_branding,user_documents,user_permissoes,user_personal_data
Base URL: {BASE_URL}/api (ex.: http://localhost:3000/api)
Swagger: GET {BASE_URL}/api/docs
Documentos complementares: FRONTEND_LOGIN.md, FRONTEND_REGISTER_BASE.md, GUIA_FRONTEND_APIS.md.
1. Resumo do que mudou
| Antes (legado) | Agora (atual) |
|---|---|
Tabela user_profiles numa linha só | public."user" + tabelas relacionais |
Login devolvia profile (resumo) | Login devolve user (objeto completo) |
Registo devolvia { id, nome, username, emailVerificado, … } | Registo devolve UserFullResponseDto (igual ao login) |
Campo nome na resposta | Campo nomeCompleto (+ opcional displayName) |
username, cargo, emailVerificado, telefone na raiz | Removidos da raiz — ver personalData, metadata, role |
password_hash na BD | Coluna password (o front nunca recebe) |
sessoes_ativas / ultimo_acesso como colunas | metadata.sessoesAtivas e metadata.ultimoAcesso (JSONB) |
| Conflito 409: "Email ou nome de utilizador já registado" | Conflito 409: Email já registado. |
| 403 login: "…estado do perfil" | 403 login: Conta não disponível para login. Verifique o estado do utilizador. |
As rotas HTTP não mudaram — o que mudou foi o formato dos bodies e das respostas.
2. Rotas (inalteradas)
| Método | Rota | Auth | Descrição |
|---|---|---|---|
POST | /api/v1/auth/login | Pública | Login |
POST | /api/v1/auth/logout | Bearer | Logout |
POST | /api/v1/auth/register | Pública | Registo |
GET | /api/v1/auth/users/:userId | Bearer | Perfil completo |
PATCH | /api/v1/auth/users/:userId | Bearer | Atualização parcial |
PATCH | /api/v1/auth/users/:userId/permissions | Bearer | Grant/revoke permissões |
GET | /api/v1/crm-admin/users | Bearer (admin) | Listagem admin |
3. Mapeamento de campos (legado → novo)
Use esta tabela para atualizar models, formulários e stores.
Campo antigo (user_profiles / profile) | Onde ler agora |
|---|---|
id | user.id |
nome | user.nomeCompleto |
username | Não existe — usar displayName se precisar de apelido |
cargo | user.role ou user.jwtRole |
email | user.email |
emailVerificado | Não exposto — ignorar ou pedir feature nova |
telefone | user.personalData?.fone |
localizacao | user.addresses[0] ou addresses[].metadata.localizacaoOriginal |
fusoHorario / idioma | user.metadata (se existir) |
fotoPerfil / fotoCapa | user.branding?.metadata ou imagemLogotipo |
rg, cpf | user.personalData?.rg, user.personalData?.cpf |
cep, endereco, cidade, estado | user.addresses[] |
sessoesAtivas | user.metadata.sessoesAtivas |
ultimoAcesso | user.metadata.ultimoAcesso |
Permissões no metadata | user.permissoes[] (chave, nome, permissaoId) |
role_number / papéis N0, N2… | user.roleNumber, user.roleId, user.personalData?.metadata.roles |
4. Alterações por endpoint
4.1 POST /api/v1/auth/login
Request — sem mudança obrigatória
{
"email": "demo@centralcrm.local",
"password": "suaSenha",
"tenantId": "uuid-opcional-para-jwt"
}
tenantId e tid continuam opcionais (incluídos no JWT se enviados).
Response — MUDOU
Antes:
{
"accessToken": "eyJ…",
"tokenType": "Bearer",
"expiresIn": 10800,
"profile": {
"id": "uuid",
"nome": "Maria",
"email": "maria@exemplo.pt",
"username": null,
"cargo": null,
"status": "ativo",
"emailVerificado": false
}
}
Agora:
{
"accessToken": "eyJ…",
"tokenType": "Bearer",
"expiresIn": 10800,
"user": {
"id": "413c0c2c-501d-4bd9-bd8c-bf3f488c7bf7",
"nomeCompleto": "Utilizador Demo",
"displayName": "Utilizador Demo",
"email": "demo@centralcrm.local",
"role": "admin",
"roleNumber": null,
"roleId": null,
"createdByUuid": null,
"status": "ativo",
"dataCadastro": "2026-03-21T07:51:51.540Z",
"createdAt": "2026-03-21T07:51:51.540Z",
"updatedAt": "2026-03-21T07:51:51.540Z",
"metadata": {
"sessoesAtivas": 1,
"ultimoAcesso": "2026-06-19T12:00:00.000Z"
},
"jwtRole": "admin",
"addresses": [],
"branding": {
"uuid": "…",
"imagemLogotipo": null,
"corPrimaria": "#191B1F",
"metadata": {}
},
"documents": [],
"permissoes": [],
"personalData": null
}
}
Correções no front
// ❌ Antigo
const { accessToken, profile } = await login(credentials);
setUser(profile);
// ✅ Novo
const { accessToken, user } = await login(credentials);
setUser(user);
localStorage.setItem('userId', user.id);
4.2 POST /api/v1/auth/logout
Response (inalterada na forma)
{
"success": true,
"sessoesAtivas": 0
}
sessoesAtivas reflete o valor em user.metadata após decremento — não é coluna separada na BD.
4.3 POST /api/v1/auth/register
Request — MUDOU
| ❌ Não enviar | ✅ Enviar |
|---|---|
fullName | nome |
username | (removido — não há unicidade de username) |
Mínimo:
{
"nome": "Maria Silva",
"email": "maria@exemplo.pt",
"password": "senhaSegura8"
}
Completo (com dados relacionais):
{
"nome": "maria",
"email": "as@g.com",
"password": "senhaSegura8",
"displayName": "maria",
"roleNumber": "N2",
"roleId": "f3a7b2d8-6b91-4c2e-8f4d-91a3d6b5e221",
"cep": "29030100",
"endereco": "Rua da FAESA",
"numero": "S/N",
"cidade": "Vitória",
"estado": "ES",
"telefone": "11959500103",
"rg": "59.500.103-0",
"cpf": "59500103028",
"dataAniversario": "2000-11-11",
"tipoPessoa": 1,
"addresses": [{ "cep": "29030100", "cidade": "Vitória", "estado": "ES", "principal": true }],
"branding": { "corPrimaria": "#191B1F" },
"personalData": { "fone": "11959500103", "rg": "59.500.103-0", "cpf": "59500103028" }
}
Campos planos (cep, rg, telefone, …) ainda são aceites e são mapeados para as tabelas corretas pelo backend.
Response — MUDOU
Antes: objeto plano { id, nome, email, username, status, emailVerificado, createdAt }.
Agora: mesmo formato de user no login (UserFullResponseDto) — HTTP 201.
// ❌ Antigo
const created = await register(data);
navigate(`/users/${created.id}`);
// ✅ Novo — mesma shape do login
const user = await register(data);
navigate(`/users/${user.id}`);
// Opcional: login imediato ou redirecionar para tela de login
Erro 409
- Antes:
Email ou nome de utilizador já registado. - Agora:
Email já registado.
4.4 GET /api/v1/auth/users/:userId
Novo contrato estável — devolve UserFullResponseDto.
- O utilizador autenticado pode ver o próprio perfil.
- Admin (
role: adminno JWT) pode ver qualqueruserId.
const user = await api.get(`/v1/auth/users/${userId}`);
// user.addresses, user.personalData, user.permissoes, etc.
4.5 PATCH /api/v1/auth/users/:userId
Patch parcial. Só envie campos a alterar.
{
"nome": "Maria Atualizada",
"displayName": "Maria",
"status": "ativo",
"personalData": { "fone": "912345678" }
}
Atenção — substituição de arrays:
Se enviar addresses, documents ou permissoes com itens, o backend substitui todos os registos existentes dessa coleção.
Response: UserFullResponseDto (HTTP 200).
4.6 PATCH /api/v1/auth/users/:userId/permissions
{
"grant": ["perm-user-edit", "3"],
"revoke": ["perm-user-delete"],
"updatedBy": "uuid-opcional-auditoria"
}
grant/revoke: aceita chave da tabelapermissoes(ex.:perm-user-edit) ou ID numérico como string (ex.:"3").- Pelo menos um item em
grantourevoke.
Response:
{
"userId": "uuid",
"granted": ["perm-user-edit"],
"revoked": [],
"active": ["perm-user-edit", "perm-user-view"],
"updatedAt": "2026-06-19T12:00:00.000Z",
"updatedBy": "uuid-do-ator"
}
4.7 GET /api/v1/crm-admin/users
Lista resumida (sem addresses, documents, etc.) — array de:
{
"id": "uuid",
"nomeCompleto": "…",
"displayName": "…",
"email": "…",
"role": "admin",
"roleNumber": "N2",
"roleId": "uuid",
"status": "ativo",
"dataCadastro": "…",
"createdAt": "…",
"updatedAt": "…",
"metadata": {},
"jwtRole": "admin"
}
Requer JWT com role admin. Caso contrário: HTTP 403.
5. Tipos TypeScript sugeridos
export interface UserFullResponse {
id: string;
nomeCompleto: string | null;
displayName: string | null;
email: string;
role: string | null;
roleNumber: string | null;
roleId: string | null;
createdByUuid: string | null;
status: string | null;
dataCadastro: string | null;
createdAt: string;
updatedAt: string;
metadata: Record<string, unknown>;
jwtRole: string | null;
addresses: UserAddress[];
branding: UserBranding | null;
documents: UserDocument[];
permissoes: UserPermissao[];
personalData: UserPersonalData | null;
}
export interface LoginResponse {
accessToken: string;
tokenType: 'Bearer';
expiresIn: number;
user: UserFullResponse; // ⚠️ era `profile`
}
export interface UserAddress {
uuid: string;
cep: string | null;
endereco: string | null;
numero: string | null;
complemento: string | null;
bairro: string | null;
cidade: string | null;
estado: string | null;
principal: boolean;
metadata: Record<string, unknown>;
}
export interface UserPersonalData {
uuid: string;
rg: string | null;
cpf: string | null;
cnpj: string | null;
cnh: string | null;
estadoCivil: string | null;
fone: string | null;
metadata: Record<string, unknown>;
}
export interface UserPermissao {
permissaoId: number;
chave: string | null;
nome: string | null;
metadata: Record<string, unknown>;
}
6. JWT após login
Claims emitidos pelo backend:
| Claim | Descrição |
|---|---|
sub | UUID do utilizador (user.id) |
tenantId / tid | Se enviados no body do login |
role / role_code | De user.role ou user.metadata.role |
jti | ID único da sessão do token |
// Guardar após login
sessionStorage.setItem('accessToken', data.accessToken);
sessionStorage.setItem('userId', data.user.id);
Para rotas companies / multi-tenant, inclua tenantId no body do login ou use outro emissor de token.
7. Checklist de migração no frontend
- Renomear
profile→userem tipos, stores e componentes de login - Renomear
nome→nomeCompletonas respostas (manternomesó no request de register/patch) - Remover
usernameeemailVerificadodos forms e validações - Trocar
fullNamepornomeno registo - Ler telefone de
user.personalData?.foneem vez da raiz - Ler morada de
user.addresses[]em vez de campos planos na raiz - Ler sessões ativas de
user.metadata.sessoesAtivas - Atualizar mensagem de erro 409 do registo
- Atualizar mensagem de erro 403 do login
- Tratar registo como retorno
UserFullResponse(não objeto plano) - Implementar/consumir
GETePATCHusers se ainda usavam dados só do login - Painel admin: consumir
GET /api/v1/crm-admin/userscom token admin - Atualizar testes e mocks com a nova shape
user
8. Erros comuns de integração
| Sintoma | Causa provável | Correção |
|---|---|---|
property fullName should not exist | Body de registo antigo | Usar nome |
property username should not exist | Campo removido | Não enviar username |
UI mostra undefined no nome | Lê user.nome | Usar user.nomeCompleto ou user.displayName |
| Login OK mas lista vazia de contactos | Esperava tudo na raiz | Usar user.personalData, user.addresses |
| Permissões não aparecem | Estavam em metadata legado | Usar user.permissoes[].chave |
| 403 no CRM admin | Token sem role admin | Login com utilizador role: admin |
| 403 ao ver outro utilizador | Não é admin nem o próprio | Só admin ou userId === sub do JWT |
9. Exemplo de adapter (compatibilidade temporária)
Se precisarem de migrar aos poucos, um adapter pode normalizar a resposta nova para código legado:
/** @deprecated Remover quando todo o front usar UserFullResponse */
export function toLegacyProfile(user: UserFullResponse) {
return {
id: user.id,
nome: user.nomeCompleto ?? user.displayName ?? '',
email: user.email,
cargo: user.role,
status: user.status,
telefone: user.personalData?.fone ?? null,
localizacao: user.addresses[0]
? [user.addresses[0].cidade, user.addresses[0].estado].filter(Boolean).join(', ')
: null,
sessoesAtivas: (user.metadata.sessoesAtivas as number) ?? 0,
};
}
export function normalizeLoginResponse(res: LoginResponse) {
return { ...res, profile: toLegacyProfile(res.user) }; // só durante transição
}
Recomendação: usar o adapter só em branch de migração e remover quando os ecrãs estiverem atualizados.
Documento alinhado ao código em src/modules/auth-login/ (março 2026). Em dúvida, consultar Swagger em /api/docs.