Pular para o conteúdo principal

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 respostaCampo nomeCompleto (+ opcional displayName)
username, cargo, emailVerificado, telefone na raizRemovidos da raiz — ver personalData, metadata, role
password_hash na BDColuna password (o front nunca recebe)
sessoes_ativas / ultimo_acesso como colunasmetadata.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étodoRotaAuthDescrição
POST/api/v1/auth/loginPúblicaLogin
POST/api/v1/auth/logoutBearerLogout
POST/api/v1/auth/registerPúblicaRegisto
GET/api/v1/auth/users/:userIdBearerPerfil completo
PATCH/api/v1/auth/users/:userIdBearerAtualização parcial
PATCH/api/v1/auth/users/:userId/permissionsBearerGrant/revoke permissões
GET/api/v1/crm-admin/usersBearer (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
iduser.id
nomeuser.nomeCompleto
usernameNão existe — usar displayName se precisar de apelido
cargouser.role ou user.jwtRole
emailuser.email
emailVerificadoNão exposto — ignorar ou pedir feature nova
telefoneuser.personalData?.fone
localizacaouser.addresses[0] ou addresses[].metadata.localizacaoOriginal
fusoHorario / idiomauser.metadata (se existir)
fotoPerfil / fotoCapauser.branding?.metadata ou imagemLogotipo
rg, cpfuser.personalData?.rg, user.personalData?.cpf
cep, endereco, cidade, estadouser.addresses[]
sessoesAtivasuser.metadata.sessoesAtivas
ultimoAcessouser.metadata.ultimoAcesso
Permissões no metadatauser.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
fullNamenome
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: admin no JWT) pode ver qualquer userId.
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 tabela permissoes (ex.: perm-user-edit) ou ID numérico como string (ex.: "3").
  • Pelo menos um item em grant ou revoke.

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:

ClaimDescrição
subUUID do utilizador (user.id)
tenantId / tidSe enviados no body do login
role / role_codeDe user.role ou user.metadata.role
jtiID ú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 profileuser em tipos, stores e componentes de login
  • Renomear nomenomeCompleto nas respostas (manter nome só no request de register/patch)
  • Remover username e emailVerificado dos forms e validações
  • Trocar fullName por nome no registo
  • Ler telefone de user.personalData?.fone em 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 GET e PATCH users se ainda usavam dados só do login
  • Painel admin: consumir GET /api/v1/crm-admin/users com token admin
  • Atualizar testes e mocks com a nova shape user

8. Erros comuns de integração

SintomaCausa provávelCorreção
property fullName should not existBody de registo antigoUsar nome
property username should not existCampo removidoNão enviar username
UI mostra undefined no nomeuser.nomeUsar user.nomeCompleto ou user.displayName
Login OK mas lista vazia de contactosEsperava tudo na raizUsar user.personalData, user.addresses
Permissões não aparecemEstavam em metadata legadoUsar user.permissoes[].chave
403 no CRM adminToken sem role adminLogin com utilizador role: admin
403 ao ver outro utilizadorNão é admin nem o próprioSó 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.