Expand action plan and support httpOnly auth

This commit is contained in:
Tiago Yamamoto 2026-02-07 09:23:27 -03:00
parent 49cfbaae16
commit d30e4a2ba7
9 changed files with 223 additions and 102 deletions

View file

@ -124,51 +124,68 @@ Como gerente de projetos, o objetivo é fechar o backend do marketplace B2B gara
---
### 📌 Passo a Passo (Plano Executivo)
### 📌 Passo a Passo (Plano Executivo Detalhado)
1. **Alinhamento Inicial (Semana 1)**
- Revisar lacunas no documento [GAPS_ANALISE_B2B](./docs/GAPS_ANALISE_B2B.md).
- Definir o **MVP do backend** e critérios de aceite por módulo.
- Atualizar backlog e priorizar dependências críticas.
- Revisar lacunas no documento [GAPS_ANALISE_B2B](./docs/GAPS_ANALISE_B2B.md) e mapear riscos.
- Definir o **MVP do backend** com critérios de aceite por módulo (dados, performance, segurança).
- Atualizar backlog com dependências críticas, owners e estimativas (XS/S/M/L).
- Definir calendário de ritos: planning, review, demo, retro e checkpoint executivo.
2. **Base Técnica & Observabilidade (Semana 1-2)**
- Garantir estrutura de projeto, padrões de logging e métricas.
- Definir versionamento de APIs (OpenAPI/Swagger).
- Configurar ambientes (dev/stage) e pipeline CI básico.
2. **Arquitetura, Base Técnica & Observabilidade (Semana 1-2)**
- Validar o diagrama de contexto e fluxos (auth, pedidos, pagamentos, logística).
- Padronizar logging estruturado, tracing e correlação de request.
- Definir versionamento de APIs (OpenAPI/Swagger + changelog).
- Configurar ambientes (dev/stage) com CI básico, seeds e migrations.
- Criar checklist de SLOs (latência, erros, disponibilidade).
3. **Modelagem de Dados & Core Domínio (Semana 2-3)**
- Validar e ajustar schema do banco ([docs/database-schema.md](./docs/database-schema.md)).
- Finalizar entidades core: empresas, usuários, produtos, pedidos.
- Implementar validações de domínio e regras de negócio.
- Definir tabelas de auditoria e eventos críticos.
4. **Catálogo & Estoque (Semana 3-4)**
4. **Autenticação, Autorização & RBAC (Semana 3)**
- Consolidar fluxo de login, refresh e logout com cookies httpOnly.
- Validar RBAC por nível de acesso (admin, seller, buyer).
- Revisar políticas de senha, rate-limit e bloqueio por tentativas.
- Garantir rota /auth/me para identificar o usuário em todas as apps.
5. **Catálogo & Estoque (Semana 3-4)**
- CRUD de produtos, lotes, validades e preços.
- Regras de disponibilidade e visibilidade por empresa.
- Importação de catálogo e validação de compliance.
5. **Carrinho & Pedido (Semana 4-5)**
6. **Carrinho & Pedido (Semana 4-5)**
- Fluxo completo: carrinho → pedido → status.
- Regras de frete, endereços e múltiplos fornecedores.
- Cálculo de impostos e condições comerciais.
6. **Pagamentos & Comissionamento (Semana 5-6)**
7. **Pagamentos & Comissionamento (Semana 5-6)**
- Integração Mercado Pago com split.
- Webhooks, antifraude básico e reconciliação.
- Simulações de chargeback e cancelamento.
7. **Logística & Notificações (Semana 6-7)**
8. **Logística & Notificações (Semana 6-7)**
- Integração de tracking, status de entrega.
- Notificações por email e eventos internos.
- Templates de comunicação e fallback de envio.
8. **Segurança & Compliance (Semana 7-8)**
- Revisão de autenticação/autorização.
- Auditoria de ações e políticas LGPD.
9. **Segurança & Compliance (Semana 7-8)**
- Revisão de autenticação/autorização e hardening de endpoints.
- Auditoria de ações, LGPD e retenção de dados.
- Checklist de segurança (OWASP Top 10).
9. **Testes & Hardening (Semana 8-9)**
10. **Testes & Hardening (Semana 8-9)**
- Cobertura mínima: unit/integration tests.
- Testes de carga nos endpoints críticos.
- Correções finais e documentação.
- Correções finais e documentação técnica.
- Validação com QA e testes de regressão.
10. **Go-live Preparação (Semana 9-10)**
11. **Operação Assistida & Go-live (Semana 9-10)**
- Checklist de release e rollback.
- Monitoramento e plano de suporte pós-lançamento.
- Treinamento de operação e documentação para suporte.
---

View file

@ -36,11 +36,12 @@ Este documento detalha, em ordem de prioridade, o que deve ser implementado no *
1. `POST /auth/login`
- Entrada: `email`, `password`.
- Saída: `accessToken`, `refreshToken`, `expiresIn`, `user`.
- **Opcional recomendado**: setar cookie httpOnly com refresh token.
2. `POST /auth/refresh`
- Entrada: `refreshToken`.
- Entrada: `refreshToken` (body) **ou** cookie httpOnly.
- Saída: `accessToken`, `refreshToken` (rotacionado).
3. `POST /auth/logout`
- Entrada: `refreshToken`.
- Entrada: `refreshToken` (body) **ou** cookie httpOnly.
- Saída: 204.
4. `GET /auth/me`
- Requer bearer token.
@ -52,6 +53,7 @@ Este documento detalha, em ordem de prioridade, o que deve ser implementado no *
- **JWT**: access token curto (1530min), refresh token longo (730 dias).
- **Rotação**: refresh token rotacionado a cada uso (revoga o anterior).
- **Bloqueio**: usuário com `status=blocked` não autentica.
- **Cookie**: refresh token em cookie httpOnly, `sameSite=lax` e `secure` em produção.
### 1.4. Infra/Configuração
@ -59,6 +61,7 @@ Este documento detalha, em ordem de prioridade, o que deve ser implementado no *
- `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`.
- `JWT_ACCESS_TTL`, `JWT_REFRESH_TTL`.
- `BCRYPT_ROUNDS`.
- `COOKIE_DOMAIN`, `COOKIE_SECURE` (quando aplicável).
---
@ -170,9 +173,10 @@ Baseado no plano de autenticação do backend, o próximo passo é solidificar o
### Backend: core de autenticação
- [ ] Criar/validar schema de `users` e `refresh_tokens` (incluindo UUIDs e hash do token).
- [ ] Implementar/validar `POST /auth/login` com retorno de `accessToken` + `refreshToken`.
- [ ] Implementar/validar `POST /auth/refresh` com rotação de refresh token.
- [ ] Implementar/validar `POST /auth/refresh` com rotação de refresh token (body e cookie httpOnly).
- [ ] Implementar/validar `GET /auth/me` protegido por bearer token.
- [ ] Aplicar regras mínimas de segurança (bcrypt, TTLs, bloqueio de usuário).
- [ ] Definir política de cookies (httpOnly, sameSite, secure) e logout com limpeza do cookie.
### Backend: base de autorização (RBAC)
- [ ] Criar schema de `roles`, `permissions`, `user_roles`, `role_permissions`.

View file

@ -46,10 +46,12 @@ const LoginPageContent = () => {
try {
// Verificar se há token armazenado
const storedToken = localStorage.getItem('access_token');
const headers: HeadersInit = {
accept: "application/json",
};
if (!storedToken) {
setCheckingAuth(false);
return;
if (storedToken) {
headers.Authorization = `Bearer ${storedToken}`; // Usar Authorization ao invés de Cookie
}
// Verificar autenticação usando BFF com o token no header Authorization
@ -57,10 +59,8 @@ const LoginPageContent = () => {
`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`,
{
method: "GET",
headers: {
"accept": "application/json",
"Authorization": `Bearer ${storedToken}`, // Usar Authorization ao invés de Cookie
},
headers,
credentials: "include",
}
);
@ -73,7 +73,7 @@ const LoginPageContent = () => {
console.log("❌ Falha no /me:", errorText);
// Only remove token if explicitly unauthorized (401)
if (response.status === 401) {
if (response.status === 401 && storedToken) {
localStorage.removeItem('access_token');
}
}
@ -148,25 +148,27 @@ const LoginPageContent = () => {
const token = loginData.access_token;
localStorage.setItem('access_token', token);
setAccessToken(token);
}
// 3. Verificar imediatamente se o /me funciona com o token
const meResponse = await fetch(`${baseUrl}/auth/me`, {
method: "GET",
headers: {
"accept": "application/json",
"Authorization": `Bearer ${token}`, // Tentar com Authorization header
},
});
// 3. Verificar imediatamente se o /me funciona (cookie httpOnly ou Authorization)
const meHeaders: HeadersInit = {
accept: "application/json",
};
if (loginData.access_token) {
meHeaders.Authorization = `Bearer ${loginData.access_token}`;
}
const meResponse = await fetch(`${baseUrl}/auth/me`, {
method: "GET",
headers: meHeaders,
credentials: "include",
});
if (meResponse.ok) {
const userData = await meResponse.json();
} else {
console.log("❌ Falha no /me:", await meResponse.text());
}
if (meResponse.ok) {
const userData = await meResponse.json();
} else {
throw new Error("Token não recebido do servidor");
console.log("❌ Falha no /me:", await meResponse.text());
}
// 4. Armazenar informações do usuário no localStorage

View file

@ -86,18 +86,20 @@ const Header = ({
// 1. Fazer logout no BFF
const token = localStorage.getItem('access_token');
const headers: HeadersInit = {
accept: '*/*',
};
if (token) {
const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/logout`, {
method: 'POST',
headers: {
'accept': '*/*',
'Authorization': `Bearer ${token}`,
},
});
headers.Authorization = `Bearer ${token}`;
}
await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/logout`, {
method: 'POST',
headers,
credentials: 'include',
});
// 2. Limpar todos os dados locais
localStorage.removeItem("access_token");
localStorage.removeItem("user");

View file

@ -59,23 +59,46 @@ export const carrinhoApiService = {
fetchUserData: async (): Promise<any | null> => {
try {
const token = carrinhoApiService.getAuthToken();
if (!token) return null;
const headers: HeadersInit = {
accept: 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${BFF_BASE_URL}/auth/me`, {
method: 'GET',
headers: {
'accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
headers,
credentials: 'include',
});
if (response.ok) {
const userData = await response.json();
if (!response.ok && response.status === 401 && token) {
const retryResponse = await fetch(`${BFF_BASE_URL}/auth/me`, {
method: 'GET',
headers: {
accept: 'application/json',
},
credentials: 'include',
});
if (!retryResponse.ok) {
return null;
}
const userData = await retryResponse.json();
localStorage.setItem('user', JSON.stringify(userData));
return userData;
}
return null;
if (!response.ok) {
return null;
}
const userData = await response.json();
localStorage.setItem('user', JSON.stringify(userData));
return userData;
} catch (error) {
console.error('Erro ao buscar dados do usuário:', error);
return null;
@ -345,4 +368,4 @@ export const carrinhoApiService = {
return { success: false, error: 'Erro de conexão ao listar carrinhos' };
}
},
};
};

View file

@ -118,19 +118,35 @@ export const pedidoApiService = {
fetchUserData: async (): Promise<any | null> => {
try {
const token = pedidoApiService.getAuthToken();
if (!token) return null;
const headers: HeadersInit = {
accept: 'application/json',
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${BFF_BASE_URL}/auth/me`, {
method: 'GET',
headers: {
'accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
headers,
credentials: 'include',
});
if (response.ok) {
const responseData = await response.json();
if (!response.ok && response.status === 401 && token) {
const retryResponse = await fetch(`${BFF_BASE_URL}/auth/me`, {
method: 'GET',
headers: {
accept: 'application/json',
},
credentials: 'include',
});
if (!retryResponse.ok) {
console.error('❌ Erro na requisição /auth/me:', retryResponse.status, retryResponse.statusText);
return null;
}
const responseData = await retryResponse.json();
// A resposta pode vir com data.data ou data diretamente
// Baseado na estrutura fornecida, deve ser data diretamente
@ -142,11 +158,24 @@ export const pedidoApiService = {
localStorage.setItem('user', JSON.stringify(userData));
return userData;
} else {
}
if (!response.ok) {
console.error('❌ Erro na requisição /auth/me:', response.status, response.statusText);
return null;
}
const responseData = await response.json();
// A resposta pode vir com data.data ou data diretamente
// Baseado na estrutura fornecida, deve ser data diretamente
const userData = responseData.data || responseData;
// Salvar no localStorage para referência (mas não para usar como source do ID)
localStorage.setItem('user', JSON.stringify(userData));
return userData;
} catch (error) {
console.error('💥 Erro ao buscar dados do usuário:', error);
return null;
@ -657,4 +686,4 @@ export const pedidoApiService = {
return { success: false, error: 'Erro de rede' };
}
}
};
};

View file

@ -108,20 +108,31 @@ class ProdutosVendaService {
*/
private async buscarUsuarioLogado(): Promise<any | null> {
try {
const token = localStorage.getItem('access_token');
if (!token) {
console.error('🔑 Token de acesso não encontrado');
return null;
}
const response = await fetch(`${this.baseUrl}/auth/me`, {
method: 'GET',
headers: this.getAuthHeaders(),
credentials: 'include',
});
if (!response.ok) {
const token = localStorage.getItem('access_token');
if (response.status === 401 && token) {
const retryResponse = await fetch(`${this.baseUrl}/auth/me`, {
method: 'GET',
headers: {
accept: 'application/json',
},
credentials: 'include',
});
if (!retryResponse.ok) {
console.error('❌ Erro ao buscar dados do usuário:', retryResponse.status, retryResponse.statusText);
return null;
}
return retryResponse.json();
}
console.error('❌ Erro ao buscar dados do usuário:', response.status, response.statusText);
return null;
}

View file

@ -27,22 +27,38 @@ export interface UserMeResponse {
export async function getUserMeData(): Promise<UserMeResponse | null> {
try {
const token = localStorage.getItem('access_token');
const headers: HeadersInit = {
Accept: 'application/json',
};
if (!token) {
console.error('🔑 Token de acesso não encontrado');
return null;
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
headers,
credentials: 'include',
});
if (!response.ok) {
if (response.status === 401 && token) {
const retryResponse = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
credentials: 'include',
});
if (!retryResponse.ok) {
console.error('❌ Erro ao buscar dados do usuário:', retryResponse.status, retryResponse.statusText);
return null;
}
return retryResponse.json();
}
console.error('❌ Erro ao buscar dados do usuário:', response.status, response.statusText);
return null;
}
@ -122,4 +138,4 @@ export async function getCurrentUserEmpresaId(): Promise<string | null> {
console.error('❌ Erro exception ao obter empresa_id do usuário:', error);
return null;
}
}
}

View file

@ -6,30 +6,52 @@
export async function getEmpresaIdRobust(): Promise<string | null> {
try {
const token = localStorage.getItem('access_token');
if (!token) {
console.error('🔑 Token de acesso não encontrado');
return null;
}
console.log('🔍 [ROBUST] Buscando dados do usuário via /me...');
const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
Accept: 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
},
credentials: 'include',
});
if (!response.ok) {
if (response.status === 401 && token) {
const retryResponse = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
credentials: 'include',
});
if (!retryResponse.ok) {
console.error('❌ [ROBUST] Erro ao buscar dados do usuário:', retryResponse.status, retryResponse.statusText);
return null;
}
const retryData = await retryResponse.json();
console.log('✅ [ROBUST] Dados completos do usuário:', retryData);
return extractEmpresaIdFromUserData(retryData);
}
console.error('❌ [ROBUST] Erro ao buscar dados do usuário:', response.status, response.statusText);
return null;
}
const userData = await response.json();
console.log('✅ [ROBUST] Dados completos do usuário:', userData);
return extractEmpresaIdFromUserData(userData);
} catch (error) {
console.error('❌ [ROBUST] Erro exception:', error);
return null;
}
}
function extractEmpresaIdFromUserData(userData: any): string | null {
// Estratégia 1: Buscar em empresasDados como array de strings
if (userData.empresasDados && Array.isArray(userData.empresasDados)) {
console.log('🔍 [ROBUST] empresasDados é um array com', userData.empresasDados.length, 'itens');
@ -101,9 +123,4 @@ export async function getEmpresaIdRobust(): Promise<string | null> {
console.error('❌ [ROBUST] Nenhuma estratégia conseguiu extrair empresa_id');
return null;
} catch (error) {
console.error('❌ [ROBUST] Erro exception:', error);
return null;
}
}
}