From 0764274d65ba78cb29445999d4c5c3b83c1a17d9 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 7 Feb 2026 17:04:39 -0300 Subject: [PATCH] Integrate Go auth with HttpOnly cookies --- saveinmed-frontend/.env.example | 3 +- saveinmed-frontend/src/app/checkout/page.tsx | 36 +-- saveinmed-frontend/src/app/login/page.tsx | 218 +++++------------- .../src/contexts/AuthContext.tsx | 21 +- saveinmed-frontend/src/hooks/useAuth.ts | 35 +-- saveinmed-frontend/src/hooks/useAuthGuard.ts | 20 +- saveinmed-frontend/src/services/auth.ts | 90 ++++++++ 7 files changed, 186 insertions(+), 237 deletions(-) create mode 100644 saveinmed-frontend/src/services/auth.ts diff --git a/saveinmed-frontend/.env.example b/saveinmed-frontend/.env.example index eea2dbd..d5a77d0 100644 --- a/saveinmed-frontend/.env.example +++ b/saveinmed-frontend/.env.example @@ -6,6 +6,8 @@ NEXT_PUBLIC_APPWRITE_PROJECT_ID=seu_projeto_id NEXT_PUBLIC_APPWRITE_DATABASE_ID=seu_banco_de_dados_id # URL Frontend NEXT_PUBLIC_FRONTEND_URL=https://seu-frontend.com +# API Go SaveInMed +NEXT_PUBLIC_API_URL=http://localhost:8214 # API BFF SaveInMed NEXT_PUBLIC_BFF_API_URL=https://bff_url/api/v1 NEXT_PUBLIC_BFF_API_URL_MP=https://bff_url @@ -41,4 +43,3 @@ MERCADO_PAGO_ACCESS_TOKEN=APP_USR-seu_token_de_acesso MERCADO_PAGO_PUBLIC_KEY=APP_USR-seu_public_key NEXT_PUBLIC_MERCADO_PAGO_PUBLIC_KEY=APP_USR-seu_public_key # Em produção, use tokens do tipo APP_USR-... e mantenha esses segredos apenas no servidor (não no client) - diff --git a/saveinmed-frontend/src/app/checkout/page.tsx b/saveinmed-frontend/src/app/checkout/page.tsx index e8d7ce1..8211350 100644 --- a/saveinmed-frontend/src/app/checkout/page.tsx +++ b/saveinmed-frontend/src/app/checkout/page.tsx @@ -6,6 +6,7 @@ import { toast } from "react-hot-toast"; import Header from "@/components/Header"; import { useCarrinho } from "@/contexts/CarrinhoContext"; import { pedidoApiService } from "@/services/pedidoApiService"; +import { authService, GO_API_V1_BASE_URL } from "@/services/auth"; import { useEmpresa } from "@/contexts/EmpresaContext"; import { CheckCircle, Truck, CreditCard, ChevronLeft, MapPin } from "lucide-react"; @@ -38,40 +39,23 @@ export default function CheckoutPage() { // Fetch user profile and addresses const fetchData = async () => { try { - const token = pedidoApiService.getAuthToken(); - if (!token) return; - - // Fetch User - const meRes = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8214'}/api/v1/auth/me`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - if (meRes.ok) { - const userData = await meRes.json(); - setUserProfile(userData); - } + const userData = await authService.me(); + if (!userData) return; + setUserProfile(userData); // Fetch Addresses - const addrRes = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8214'}/api/v1/enderecos`, { - headers: { 'Authorization': `Bearer ${token}` } + const addrRes = await fetch(`${GO_API_V1_BASE_URL}/enderecos`, { + headers: { accept: 'application/json' }, + credentials: 'include' }); if (addrRes.ok) { const addrData = await addrRes.json(); if (Array.isArray(addrData) && addrData.length > 0) { setAddresses(addrData); setSelectedAddressId(addrData[0].id); - } else if (!addrData || addrData.length === 0) { - const mock = { - id: 'mock-1', - logradouro: empresa?.endereco || "Rua Principal", - numero: empresa?.numero || "100", - bairro: empresa?.bairro || "Centro", - cidade: empresa?.cidade || "São Paulo", - uf: empresa?.estado || "SP", - cep: empresa?.cep || "01000-000", - titulo: "Endereço Padrão (Mock)" - }; - setAddresses([mock]); - setSelectedAddressId('mock-1'); + } else { + setAddresses([]); + setSelectedAddressId(null); } } } catch (e) { diff --git a/saveinmed-frontend/src/app/login/page.tsx b/saveinmed-frontend/src/app/login/page.tsx index 70f3b7b..da3da0b 100644 --- a/saveinmed-frontend/src/app/login/page.tsx +++ b/saveinmed-frontend/src/app/login/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, Suspense } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEmpresa } from "@/contexts/EmpresaContext"; import { translateError } from "@/lib/error-translator"; +import { authService } from "@/services/auth"; import Image from "next/image"; /** @@ -23,7 +24,6 @@ const LoginPageContent = () => { const [isLogin, setIsLogin] = useState(true); // Alterna entre login e registro const [checkingAuth, setCheckingAuth] = useState(true); // Verificação inicial de autenticação const [showPassword, setShowPassword] = useState(false); // Controla visibilidade da senha - const [accessToken, setAccessToken] = useState(""); // Token de acesso do BFF const [showSuccessModal, setShowSuccessModal] = useState(false); // Modal de sucesso do cadastro const [showPendingModal, setShowPendingModal] = useState(false); // Modal de cadastro pendente @@ -44,38 +44,9 @@ const LoginPageContent = () => { useEffect(() => { const checkAuth = async () => { try { - // Verificar se há token armazenado - const storedToken = localStorage.getItem('access_token'); - const headers: HeadersInit = { - accept: "application/json", - }; - - if (storedToken) { - headers.Authorization = `Bearer ${storedToken}`; // Usar Authorization ao invés de Cookie - } - - // Verificar autenticação usando BFF com o token no header Authorization - const response = await fetch( - `${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`, - { - method: "GET", - headers, - credentials: "include", - } - ); - - - if (response.ok) { - const userData = await response.json(); - // ... (log logic) - } else { - const errorText = await response.text(); - console.log("❌ Falha no /me:", errorText); - - // Only remove token if explicitly unauthorized (401) - if (response.status === 401 && storedToken) { - localStorage.removeItem('access_token'); - } + const userData = await authService.me(); + if (userData) { + localStorage.setItem('user', JSON.stringify(userData)); } } catch (error) { } finally { @@ -95,44 +66,25 @@ const LoginPageContent = () => { setError(""); try { - // 1. Fazer login no BFF - const baseUrl = process.env.NEXT_PUBLIC_BFF_API_URL!; - const response = await fetch(`${baseUrl}/auth/login`, { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - }, - credentials: 'include', // Permite que o browser receba e armazene cookies - mode: 'cors', // Habilita CORS explicitamente - body: JSON.stringify({ - email: email, - password: password - }) - }); + // 1. Fazer login na API Go (cookie HttpOnly) + const loginData = await authService.login(email, password); - // Ler e logar o corpo da resposta (usando clone para não consumir o body original) - const respClone = response.clone(); - const respText = await respClone.text(); - let respBody: any = respText; - try { - respBody = JSON.parse(respText); - } catch { - // corpo não é JSON, manter como texto - } - - if (!response.ok) { + if (!loginData || (loginData as any).error) { // Extrair mensagem de erro do backend - let errorMessage = respBody?.message || respBody?.error || respBody?.detail || respText; + let errorMessage = + (loginData as any)?.message || + (loginData as any)?.error || + (loginData as any)?.detail || + "Falha no login"; // Tratar códigos de status específicos - if (response.status === 401) { + if ((loginData as any)?.status === 401) { errorMessage = "Email ou senha incorretos. Verifique suas credenciais."; - } else if (response.status === 403) { + } else if ((loginData as any)?.status === 403) { errorMessage = "Acesso negado. Sua conta pode estar inativa."; - } else if (response.status === 404) { + } else if ((loginData as any)?.status === 404) { errorMessage = "Usuário não encontrado. Verifique o email informado."; - } else if (response.status >= 500) { + } else if ((loginData as any)?.status >= 500) { errorMessage = "Erro interno do servidor. Tente novamente em alguns minutos."; } @@ -141,37 +93,13 @@ const LoginPageContent = () => { throw new Error(translatedError); } - const loginData = await response.json(); - - // 2. Capturar e armazenar o access_token - if (loginData.access_token) { - const token = loginData.access_token; - localStorage.setItem('access_token', token); - setAccessToken(token); + // 2. Verificar imediatamente se o /me funciona (cookie httpOnly) + const meData = await authService.me(); + if (meData) { + localStorage.setItem('user', JSON.stringify(meData)); } - // 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()); - } - - // 4. Armazenar informações do usuário no localStorage + // 3. Armazenar informações do usuário no localStorage if (loginData.user) { localStorage.setItem('user', JSON.stringify(loginData.user)); @@ -195,12 +123,12 @@ const LoginPageContent = () => { } } - // 5. Verificar e armazenar empresa ID se disponível + // 4. Verificar e armazenar empresa ID se disponível if (loginData.user?.empresaId) { setEmpresaId(loginData.user.empresaId); } - // 6. Redirecionar para o dashboard + // 5. Redirecionar para o dashboard router.push("/dashboard"); } catch (error: any) { @@ -208,6 +136,17 @@ const LoginPageContent = () => { // Definir mensagem de erro amigável let friendlyMessage = error.message; + const status = error?.status; + + if (status === 401) { + friendlyMessage = "Email ou senha incorretos. Verifique suas credenciais."; + } else if (status === 403) { + friendlyMessage = "Acesso negado. Sua conta pode estar inativa."; + } else if (status === 404) { + friendlyMessage = "Usuário não encontrado. Verifique o email informado."; + } else if (status >= 500) { + friendlyMessage = "Erro interno do servidor. Tente novamente em alguns minutos."; + } // Se for um erro de rede if (error.name === 'TypeError' || error.message.includes('fetch')) { @@ -218,7 +157,7 @@ const LoginPageContent = () => { friendlyMessage = "Erro inesperado. Verifique suas credenciais e tente novamente."; } - setError(friendlyMessage); + setError(translateError(friendlyMessage)); } finally { setLoading(false); } @@ -233,77 +172,24 @@ const LoginPageContent = () => { setError(""); try { - // 1. Fazer registro no BFF com dados corretos - const baseUrl = process.env.NEXT_PUBLIC_BFF_API_URL!; - const response = await fetch(`${baseUrl}/auth/register`, { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - role: "Seller", - name: name, - username: email, - email: email, - password: password - }) + // 1. Fazer registro na API Go + await authService.register({ + role: "Seller", + name: name, + username: email, + email: email, + password: password }); + // 2. Fazer login automático após registro para obter cookie HttpOnly + const loginData = await authService.login(email, password); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - - // Tratar códigos de status específicos - let errorMessage = errorData.message || 'Falha no registro'; - - if (response.status === 409 || errorMessage.includes('already exists') || errorMessage.includes('já existe')) { - errorMessage = 'Este email já está cadastrado. Tente fazer login ou use outro email.'; - } else if (response.status === 400) { - errorMessage = 'Dados inválidos. Verifique se todos os campos estão preenchidos corretamente.'; - } else if (response.status >= 500) { - errorMessage = 'Erro interno do servidor. Tente novamente em alguns minutos.'; - } - - // Aplicar tradução se disponível - const translatedError = translateError(errorMessage); - throw new Error(translatedError); - } - - const registerData = await response.json(); - - // 2. Fazer login automático após registro para obter token - const loginResponse = await fetch(`${baseUrl}/auth/login`, { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - }, - credentials: 'include', - mode: 'cors', - body: JSON.stringify({ - email: email, - password: password - }) - }); - - if (!loginResponse.ok) { - console.error("❌ Erro no login automático:", loginResponse.status); - throw new Error('Erro ao fazer login automático após registro'); - } - - const loginData = await loginResponse.json(); - - // 3. Armazenar token e dados do usuário - if (loginData.access_token) { - localStorage.setItem('access_token', loginData.access_token); - } - + // 3. Armazenar dados do usuário if (loginData.user) { localStorage.setItem('user', JSON.stringify(loginData.user)); } - // 4. Redirecionar para completar registro com token válido + // 4. Redirecionar para completar registro com sessão válida router.push(`/completar-registro?nome=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`); } catch (error: any) { @@ -311,6 +197,15 @@ const LoginPageContent = () => { // Definir mensagem de erro amigável let friendlyMessage = error.message; + const status = error?.status; + + if (status === 409 || friendlyMessage.includes('already exists') || friendlyMessage.includes('já existe')) { + friendlyMessage = 'Este email já está cadastrado. Tente fazer login ou use outro email.'; + } else if (status === 400) { + friendlyMessage = 'Dados inválidos. Verifique se todos os campos estão preenchidos corretamente.'; + } else if (status >= 500) { + friendlyMessage = 'Erro interno do servidor. Tente novamente em alguns minutos.'; + } // Se for um erro de rede if (error.name === 'TypeError' || error.message.includes('fetch')) { @@ -321,7 +216,7 @@ const LoginPageContent = () => { friendlyMessage = "Erro inesperado durante o cadastro. Tente novamente."; } - setError(friendlyMessage); + setError(translateError(friendlyMessage)); } finally { setLoading(false); } @@ -362,7 +257,6 @@ const LoginPageContent = () => { // Fazer logout para limpar dados do usuário localStorage.removeItem('access_token'); localStorage.removeItem('user'); - setAccessToken(""); // Redirecionar para página inicial router.push('/'); }; diff --git a/saveinmed-frontend/src/contexts/AuthContext.tsx b/saveinmed-frontend/src/contexts/AuthContext.tsx index 9f29f44..4be733d 100644 --- a/saveinmed-frontend/src/contexts/AuthContext.tsx +++ b/saveinmed-frontend/src/contexts/AuthContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect } from "react"; import { UserRole } from "@/types/auth"; +import { authService } from "@/services/auth"; interface UserData { $id: string; @@ -32,22 +33,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { const checkAuth = async () => { try { - // Verificação simples baseada em token - const storedToken = localStorage.getItem('access_token'); + const userData = await authService.me(); - if (storedToken) { + if (userData) { setIsAuthenticated(true); - const storedUser = localStorage.getItem('user'); - if (storedUser) { - try { - const parsedUser = JSON.parse(storedUser) as UserData; - setUser(parsedUser); - setUserRole(parsedUser.nivel ?? null); - } catch (e) { - console.error("Erro ao fazer parse do usuário:", e); - // Opcional: tentar buscar da API se o parse falhar ou se não tiver no storage - } - } + localStorage.setItem('user', JSON.stringify(userData)); + const parsedUser = userData as UserData; + setUser(parsedUser); + setUserRole(parsedUser.nivel ?? null); } else { setIsAuthenticated(false); setUser(null); diff --git a/saveinmed-frontend/src/hooks/useAuth.ts b/saveinmed-frontend/src/hooks/useAuth.ts index 8dac264..4b81a59 100644 --- a/saveinmed-frontend/src/hooks/useAuth.ts +++ b/saveinmed-frontend/src/hooks/useAuth.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; +import { authService } from '@/services/auth'; import { UserRole, UserData, AuthContextType, ROLE_PERMISSIONS, ROLE_ROUTES } from '@/types/auth'; /** @@ -90,10 +91,9 @@ export const useAuth = (): AuthContextType => { try { setLoading(true); - // Verificar se há token BFF armazenado - const storedToken = localStorage.getItem('access_token'); + const userData = await authService.me(); - if (!storedToken) { + if (!userData) { setIsAuthenticated(false); setUser(null); setUserRole(null); @@ -101,28 +101,15 @@ export const useAuth = (): AuthContextType => { return; } - // Marcar como autenticado se há token setIsAuthenticated(true); - - const storedUser = localStorage.getItem('user'); - if (storedUser) { - try { - const parsedUser = JSON.parse(storedUser) as UserData; - setUser(parsedUser); - const storedRole = - parsedUser?.nivel || - (parsedUser as unknown as { role?: string; userRole?: string })?.role || - (parsedUser as unknown as { role?: string; userRole?: string })?.userRole; - setUserRole(normalizeRole(storedRole)); - } catch (parseError) { - console.error('Erro ao ler usuário salvo:', parseError); - setUser(null); - setUserRole(null); - } - } else { - setUser(null); - setUserRole(null); - } + localStorage.setItem('user', JSON.stringify(userData)); + const parsedUser = userData as UserData; + setUser(parsedUser); + const storedRole = + parsedUser?.nivel || + (parsedUser as unknown as { role?: string; userRole?: string })?.role || + (parsedUser as unknown as { role?: string; userRole?: string })?.userRole; + setUserRole(normalizeRole(storedRole)); } catch (error) { console.error('Erro na verificação de autenticação:', error); diff --git a/saveinmed-frontend/src/hooks/useAuthGuard.ts b/saveinmed-frontend/src/hooks/useAuthGuard.ts index 7ba5921..cf4354c 100644 --- a/saveinmed-frontend/src/hooks/useAuthGuard.ts +++ b/saveinmed-frontend/src/hooks/useAuthGuard.ts @@ -1,7 +1,8 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; +import { authService } from "@/services/auth"; -// Hook desabilitado - sempre permite acesso para usuários com token +// Hook de autenticação usando sessão via cookie HttpOnly export const useAuthGuard = () => { const router = useRouter(); const [user, setUser] = useState(null); @@ -14,18 +15,17 @@ export const useAuthGuard = () => { try { setAuthError(null); - // Verificação simples baseada em token - const storedToken = localStorage.getItem('access_token'); - - if (!storedToken) { - console.log("ℹ️ useAuthGuard: Nenhum token encontrado, redirecionando para login"); + const userData = await authService.me(); + + if (!userData) { + console.log("ℹ️ useAuthGuard: Nenhuma sessão encontrada, redirecionando para login"); router.push("/login"); return; } - console.log("✅ useAuthGuard: Token encontrado, usuário autenticado"); - setUser({ email: 'usuario@exemplo.com' }); // Mock user - setRegistroCompleto(true); // Sempre completo + console.log("✅ useAuthGuard: Sessão encontrada, usuário autenticado"); + setUser(userData); + setRegistroCompleto(userData["registro-completo"] !== false); } catch (error) { console.error('❌ useAuthGuard: Erro na verificação:', error); @@ -40,4 +40,4 @@ export const useAuthGuard = () => { }, [router]); return { user, loading, registroCompleto, authError }; -}; \ No newline at end of file +}; diff --git a/saveinmed-frontend/src/services/auth.ts b/saveinmed-frontend/src/services/auth.ts new file mode 100644 index 0000000..e84b280 --- /dev/null +++ b/saveinmed-frontend/src/services/auth.ts @@ -0,0 +1,90 @@ +export const GO_API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8214'; +export const GO_API_V1_BASE_URL = `${GO_API_BASE_URL}/api/v1`; + +export interface AuthResponse { + user?: Record; + access_token?: string; + [key: string]: any; +} + +const AUTH_BASE_URL = `${GO_API_V1_BASE_URL}/auth`; + +const buildError = async (response: Response) => { + const message = await response.text(); + const error = new Error(message || response.statusText); + (error as Error & { status?: number }).status = response.status; + return error; +}; + +export const authService = { + login: async (email: string, password: string): Promise => { + const response = await fetch(`${AUTH_BASE_URL}/login`, { + method: 'POST', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + throw await buildError(response); + } + + return response.json(); + }, + + register: async (payload: Record): Promise => { + const response = await fetch(`${AUTH_BASE_URL}/register`, { + method: 'POST', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw await buildError(response); + } + + return response.json(); + }, + + me: async (): Promise | null> => { + const response = await fetch(`${AUTH_BASE_URL}/me`, { + method: 'GET', + headers: { + accept: 'application/json', + }, + credentials: 'include', + }); + + if (response.status === 401) { + return null; + } + + if (!response.ok) { + throw await buildError(response); + } + + const data = await response.json(); + return data.data || data; + }, + + logout: async (): Promise => { + const response = await fetch(`${AUTH_BASE_URL}/logout`, { + method: 'POST', + headers: { + accept: 'application/json', + }, + credentials: 'include', + }); + + if (!response.ok && response.status !== 204) { + throw await buildError(response); + } + }, +};