Expand action plan and support httpOnly auth
This commit is contained in:
parent
49cfbaae16
commit
d30e4a2ba7
9 changed files with 223 additions and 102 deletions
53
README.md
53
README.md
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (15–30min), refresh token longo (7–30 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`.
|
||||
|
|
|
|||
|
|
@ -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,14 +148,20 @@ const LoginPageContent = () => {
|
|||
const token = loginData.access_token;
|
||||
localStorage.setItem('access_token', token);
|
||||
setAccessToken(token);
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// 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
|
||||
},
|
||||
headers: meHeaders,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -165,10 +171,6 @@ const LoginPageContent = () => {
|
|||
console.log("❌ Falha no /me:", await meResponse.text());
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Token não recebido do servidor");
|
||||
}
|
||||
|
||||
// 4. Armazenar informações do usuário no localStorage
|
||||
if (loginData.user) {
|
||||
localStorage.setItem('user', JSON.stringify(loginData.user));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,29 +7,51 @@ 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue