Merge pull request #80 from rede5/codex/integrate-next.js-with-go-backend

Integrate Go auth service with HttpOnly cookie flow
This commit is contained in:
Tiago Yamamoto 2026-02-07 18:19:45 -03:00 committed by GitHub
commit a735cd70d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 186 additions and 237 deletions

View file

@ -6,6 +6,8 @@ NEXT_PUBLIC_APPWRITE_PROJECT_ID=seu_projeto_id
NEXT_PUBLIC_APPWRITE_DATABASE_ID=seu_banco_de_dados_id NEXT_PUBLIC_APPWRITE_DATABASE_ID=seu_banco_de_dados_id
# URL Frontend # URL Frontend
NEXT_PUBLIC_FRONTEND_URL=https://seu-frontend.com NEXT_PUBLIC_FRONTEND_URL=https://seu-frontend.com
# API Go SaveInMed
NEXT_PUBLIC_API_URL=http://localhost:8214
# API BFF SaveInMed # API BFF SaveInMed
NEXT_PUBLIC_BFF_API_URL=https://bff_url/api/v1 NEXT_PUBLIC_BFF_API_URL=https://bff_url/api/v1
NEXT_PUBLIC_BFF_API_URL_MP=https://bff_url 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 MERCADO_PAGO_PUBLIC_KEY=APP_USR-seu_public_key
NEXT_PUBLIC_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) # Em produção, use tokens do tipo APP_USR-... e mantenha esses segredos apenas no servidor (não no client)

View file

@ -6,6 +6,7 @@ import { toast } from "react-hot-toast";
import Header from "@/components/Header"; import Header from "@/components/Header";
import { useCarrinho } from "@/contexts/CarrinhoContext"; import { useCarrinho } from "@/contexts/CarrinhoContext";
import { pedidoApiService } from "@/services/pedidoApiService"; import { pedidoApiService } from "@/services/pedidoApiService";
import { authService, GO_API_V1_BASE_URL } from "@/services/auth";
import { useEmpresa } from "@/contexts/EmpresaContext"; import { useEmpresa } from "@/contexts/EmpresaContext";
import { CheckCircle, Truck, CreditCard, ChevronLeft, MapPin } from "lucide-react"; import { CheckCircle, Truck, CreditCard, ChevronLeft, MapPin } from "lucide-react";
@ -38,40 +39,23 @@ export default function CheckoutPage() {
// Fetch user profile and addresses // Fetch user profile and addresses
const fetchData = async () => { const fetchData = async () => {
try { try {
const token = pedidoApiService.getAuthToken(); const userData = await authService.me();
if (!token) return; if (!userData) return;
setUserProfile(userData);
// 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);
}
// Fetch Addresses // Fetch Addresses
const addrRes = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8214'}/api/v1/enderecos`, { const addrRes = await fetch(`${GO_API_V1_BASE_URL}/enderecos`, {
headers: { 'Authorization': `Bearer ${token}` } headers: { accept: 'application/json' },
credentials: 'include'
}); });
if (addrRes.ok) { if (addrRes.ok) {
const addrData = await addrRes.json(); const addrData = await addrRes.json();
if (Array.isArray(addrData) && addrData.length > 0) { if (Array.isArray(addrData) && addrData.length > 0) {
setAddresses(addrData); setAddresses(addrData);
setSelectedAddressId(addrData[0].id); setSelectedAddressId(addrData[0].id);
} else if (!addrData || addrData.length === 0) { } else {
const mock = { setAddresses([]);
id: 'mock-1', setSelectedAddressId(null);
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');
} }
} }
} catch (e) { } catch (e) {

View file

@ -4,6 +4,7 @@ import { useState, useEffect, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEmpresa } from "@/contexts/EmpresaContext"; import { useEmpresa } from "@/contexts/EmpresaContext";
import { translateError } from "@/lib/error-translator"; import { translateError } from "@/lib/error-translator";
import { authService } from "@/services/auth";
import Image from "next/image"; import Image from "next/image";
/** /**
@ -23,7 +24,6 @@ const LoginPageContent = () => {
const [isLogin, setIsLogin] = useState<boolean>(true); // Alterna entre login e registro const [isLogin, setIsLogin] = useState<boolean>(true); // Alterna entre login e registro
const [checkingAuth, setCheckingAuth] = useState<boolean>(true); // Verificação inicial de autenticação const [checkingAuth, setCheckingAuth] = useState<boolean>(true); // Verificação inicial de autenticação
const [showPassword, setShowPassword] = useState<boolean>(false); // Controla visibilidade da senha const [showPassword, setShowPassword] = useState<boolean>(false); // Controla visibilidade da senha
const [accessToken, setAccessToken] = useState<string>(""); // Token de acesso do BFF
const [showSuccessModal, setShowSuccessModal] = useState<boolean>(false); // Modal de sucesso do cadastro const [showSuccessModal, setShowSuccessModal] = useState<boolean>(false); // Modal de sucesso do cadastro
const [showPendingModal, setShowPendingModal] = useState<boolean>(false); // Modal de cadastro pendente const [showPendingModal, setShowPendingModal] = useState<boolean>(false); // Modal de cadastro pendente
@ -44,38 +44,9 @@ const LoginPageContent = () => {
useEffect(() => { useEffect(() => {
const checkAuth = async () => { const checkAuth = async () => {
try { try {
// Verificar se há token armazenado const userData = await authService.me();
const storedToken = localStorage.getItem('access_token'); if (userData) {
const headers: HeadersInit = { localStorage.setItem('user', JSON.stringify(userData));
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');
}
} }
} catch (error) { } catch (error) {
} finally { } finally {
@ -95,44 +66,25 @@ const LoginPageContent = () => {
setError(""); setError("");
try { try {
// 1. Fazer login no BFF // 1. Fazer login na API Go (cookie HttpOnly)
const baseUrl = process.env.NEXT_PUBLIC_BFF_API_URL!; const loginData = await authService.login(email, password);
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
})
});
// Ler e logar o corpo da resposta (usando clone para não consumir o body original) if (!loginData || (loginData as any).error) {
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) {
// Extrair mensagem de erro do backend // 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 // 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."; 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."; 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."; 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."; errorMessage = "Erro interno do servidor. Tente novamente em alguns minutos.";
} }
@ -141,37 +93,13 @@ const LoginPageContent = () => {
throw new Error(translatedError); throw new Error(translatedError);
} }
const loginData = await response.json(); // 2. Verificar imediatamente se o /me funciona (cookie httpOnly)
const meData = await authService.me();
// 2. Capturar e armazenar o access_token if (meData) {
if (loginData.access_token) { localStorage.setItem('user', JSON.stringify(meData));
const token = loginData.access_token;
localStorage.setItem('access_token', token);
setAccessToken(token);
} }
// 3. Verificar imediatamente se o /me funciona (cookie httpOnly ou Authorization) // 3. Armazenar informações do usuário no localStorage
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
if (loginData.user) { if (loginData.user) {
localStorage.setItem('user', JSON.stringify(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) { if (loginData.user?.empresaId) {
setEmpresaId(loginData.user.empresaId); setEmpresaId(loginData.user.empresaId);
} }
// 6. Redirecionar para o dashboard // 5. Redirecionar para o dashboard
router.push("/dashboard"); router.push("/dashboard");
} catch (error: any) { } catch (error: any) {
@ -208,6 +136,17 @@ const LoginPageContent = () => {
// Definir mensagem de erro amigável // Definir mensagem de erro amigável
let friendlyMessage = error.message; 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 // Se for um erro de rede
if (error.name === 'TypeError' || error.message.includes('fetch')) { if (error.name === 'TypeError' || error.message.includes('fetch')) {
@ -218,7 +157,7 @@ const LoginPageContent = () => {
friendlyMessage = "Erro inesperado. Verifique suas credenciais e tente novamente."; friendlyMessage = "Erro inesperado. Verifique suas credenciais e tente novamente.";
} }
setError(friendlyMessage); setError(translateError(friendlyMessage));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -233,77 +172,24 @@ const LoginPageContent = () => {
setError(""); setError("");
try { try {
// 1. Fazer registro no BFF com dados corretos // 1. Fazer registro na API Go
const baseUrl = process.env.NEXT_PUBLIC_BFF_API_URL!; await authService.register({
const response = await fetch(`${baseUrl}/auth/register`, { role: "Seller",
method: 'POST', name: name,
headers: { username: email,
'accept': 'application/json', email: email,
'Content-Type': 'application/json', password: password
},
body: JSON.stringify({
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) { // 3. Armazenar dados do usuário
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);
}
if (loginData.user) { if (loginData.user) {
localStorage.setItem('user', JSON.stringify(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)}`); router.push(`/completar-registro?nome=${encodeURIComponent(name)}&email=${encodeURIComponent(email)}`);
} catch (error: any) { } catch (error: any) {
@ -311,6 +197,15 @@ const LoginPageContent = () => {
// Definir mensagem de erro amigável // Definir mensagem de erro amigável
let friendlyMessage = error.message; 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 // Se for um erro de rede
if (error.name === 'TypeError' || error.message.includes('fetch')) { if (error.name === 'TypeError' || error.message.includes('fetch')) {
@ -321,7 +216,7 @@ const LoginPageContent = () => {
friendlyMessage = "Erro inesperado durante o cadastro. Tente novamente."; friendlyMessage = "Erro inesperado durante o cadastro. Tente novamente.";
} }
setError(friendlyMessage); setError(translateError(friendlyMessage));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -362,7 +257,6 @@ const LoginPageContent = () => {
// Fazer logout para limpar dados do usuário // Fazer logout para limpar dados do usuário
localStorage.removeItem('access_token'); localStorage.removeItem('access_token');
localStorage.removeItem('user'); localStorage.removeItem('user');
setAccessToken("");
// Redirecionar para página inicial // Redirecionar para página inicial
router.push('/'); router.push('/');
}; };

View file

@ -2,6 +2,7 @@
import React, { createContext, useContext, useState, useEffect } from "react"; import React, { createContext, useContext, useState, useEffect } from "react";
import { UserRole } from "@/types/auth"; import { UserRole } from "@/types/auth";
import { authService } from "@/services/auth";
interface UserData { interface UserData {
$id: string; $id: string;
@ -32,22 +33,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
useEffect(() => { useEffect(() => {
const checkAuth = async () => { const checkAuth = async () => {
try { try {
// Verificação simples baseada em token const userData = await authService.me();
const storedToken = localStorage.getItem('access_token');
if (storedToken) { if (userData) {
setIsAuthenticated(true); setIsAuthenticated(true);
const storedUser = localStorage.getItem('user'); localStorage.setItem('user', JSON.stringify(userData));
if (storedUser) { const parsedUser = userData as UserData;
try { setUser(parsedUser);
const parsedUser = JSON.parse(storedUser) as UserData; setUserRole(parsedUser.nivel ?? null);
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
}
}
} else { } else {
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { authService } from '@/services/auth';
import { UserRole, UserData, AuthContextType, ROLE_PERMISSIONS, ROLE_ROUTES } from '@/types/auth'; import { UserRole, UserData, AuthContextType, ROLE_PERMISSIONS, ROLE_ROUTES } from '@/types/auth';
/** /**
@ -90,10 +91,9 @@ export const useAuth = (): AuthContextType => {
try { try {
setLoading(true); setLoading(true);
// Verificar se há token BFF armazenado const userData = await authService.me();
const storedToken = localStorage.getItem('access_token');
if (!storedToken) { if (!userData) {
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);
setUserRole(null); setUserRole(null);
@ -101,28 +101,15 @@ export const useAuth = (): AuthContextType => {
return; return;
} }
// Marcar como autenticado se há token
setIsAuthenticated(true); setIsAuthenticated(true);
localStorage.setItem('user', JSON.stringify(userData));
const storedUser = localStorage.getItem('user'); const parsedUser = userData as UserData;
if (storedUser) { setUser(parsedUser);
try { const storedRole =
const parsedUser = JSON.parse(storedUser) as UserData; parsedUser?.nivel ||
setUser(parsedUser); (parsedUser as unknown as { role?: string; userRole?: string })?.role ||
const storedRole = (parsedUser as unknown as { role?: string; userRole?: string })?.userRole;
parsedUser?.nivel || setUserRole(normalizeRole(storedRole));
(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);
}
} catch (error) { } catch (error) {
console.error('Erro na verificação de autenticação:', error); console.error('Erro na verificação de autenticação:', error);

View file

@ -1,7 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; 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 = () => { export const useAuthGuard = () => {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState<any>(null); const [user, setUser] = useState<any>(null);
@ -14,18 +15,17 @@ export const useAuthGuard = () => {
try { try {
setAuthError(null); setAuthError(null);
// Verificação simples baseada em token const userData = await authService.me();
const storedToken = localStorage.getItem('access_token');
if (!userData) {
if (!storedToken) { console.log(" useAuthGuard: Nenhuma sessão encontrada, redirecionando para login");
console.log(" useAuthGuard: Nenhum token encontrado, redirecionando para login");
router.push("/login"); router.push("/login");
return; return;
} }
console.log("✅ useAuthGuard: Token encontrado, usuário autenticado"); console.log("✅ useAuthGuard: Sessão encontrada, usuário autenticado");
setUser({ email: 'usuario@exemplo.com' }); // Mock user setUser(userData);
setRegistroCompleto(true); // Sempre completo setRegistroCompleto(userData["registro-completo"] !== false);
} catch (error) { } catch (error) {
console.error('❌ useAuthGuard: Erro na verificação:', error); console.error('❌ useAuthGuard: Erro na verificação:', error);
@ -40,4 +40,4 @@ export const useAuthGuard = () => {
}, [router]); }, [router]);
return { user, loading, registroCompleto, authError }; return { user, loading, registroCompleto, authError };
}; };

View file

@ -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<string, any>;
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<AuthResponse> => {
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<string, any>): Promise<AuthResponse> => {
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<Record<string, any> | 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<void> => {
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);
}
},
};