diff --git a/README.md b/README.md index 0fd3e4b..9e4b94b 100644 --- a/README.md +++ b/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. --- diff --git a/docs/PLANO_BACKEND_AUTENTICACAO_ACESSO.md b/docs/PLANO_BACKEND_AUTENTICACAO_ACESSO.md index 7138d35..61def62 100644 --- a/docs/PLANO_BACKEND_AUTENTICACAO_ACESSO.md +++ b/docs/PLANO_BACKEND_AUTENTICACAO_ACESSO.md @@ -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`. diff --git a/saveinmed-frontend/src/app/login/page.tsx b/saveinmed-frontend/src/app/login/page.tsx index 38c6be1..70f3b7b 100644 --- a/saveinmed-frontend/src/app/login/page.tsx +++ b/saveinmed-frontend/src/app/login/page.tsx @@ -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 diff --git a/saveinmed-frontend/src/components/Header.tsx b/saveinmed-frontend/src/components/Header.tsx index d6fd119..2b387fa 100644 --- a/saveinmed-frontend/src/components/Header.tsx +++ b/saveinmed-frontend/src/components/Header.tsx @@ -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"); diff --git a/saveinmed-frontend/src/services/carrinhoApiService.ts b/saveinmed-frontend/src/services/carrinhoApiService.ts index ae228b0..79ebedb 100644 --- a/saveinmed-frontend/src/services/carrinhoApiService.ts +++ b/saveinmed-frontend/src/services/carrinhoApiService.ts @@ -59,23 +59,46 @@ export const carrinhoApiService = { fetchUserData: async (): Promise => { 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' }; } }, -}; \ No newline at end of file +}; diff --git a/saveinmed-frontend/src/services/pedidoApiService.ts b/saveinmed-frontend/src/services/pedidoApiService.ts index 3a6481f..a38b75c 100644 --- a/saveinmed-frontend/src/services/pedidoApiService.ts +++ b/saveinmed-frontend/src/services/pedidoApiService.ts @@ -118,19 +118,35 @@ export const pedidoApiService = { fetchUserData: async (): Promise => { 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' }; } } -}; \ No newline at end of file +}; diff --git a/saveinmed-frontend/src/services/produtosVendaService.ts b/saveinmed-frontend/src/services/produtosVendaService.ts index 213faa1..ef16feb 100644 --- a/saveinmed-frontend/src/services/produtosVendaService.ts +++ b/saveinmed-frontend/src/services/produtosVendaService.ts @@ -108,20 +108,31 @@ class ProdutosVendaService { */ private async buscarUsuarioLogado(): Promise { 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; } diff --git a/saveinmed-frontend/src/utils/authUtils.ts b/saveinmed-frontend/src/utils/authUtils.ts index f6e45d9..ee0137b 100644 --- a/saveinmed-frontend/src/utils/authUtils.ts +++ b/saveinmed-frontend/src/utils/authUtils.ts @@ -27,22 +27,38 @@ export interface UserMeResponse { export async function getUserMeData(): Promise { 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 { console.error('❌ Erro exception ao obter empresa_id do usuário:', error); return null; } -} \ No newline at end of file +} diff --git a/saveinmed-frontend/src/utils/robustEmpresaId.ts b/saveinmed-frontend/src/utils/robustEmpresaId.ts index 1c03c58..1ca88eb 100644 --- a/saveinmed-frontend/src/utils/robustEmpresaId.ts +++ b/saveinmed-frontend/src/utils/robustEmpresaId.ts @@ -6,30 +6,52 @@ export async function getEmpresaIdRobust(): Promise { 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 { console.error('❌ [ROBUST] Nenhuma estratégia conseguiu extrair empresa_id'); return null; - - } catch (error) { - console.error('❌ [ROBUST] Erro exception:', error); - return null; - } -} \ No newline at end of file +}