Implement authentication flow

Implement mandatory data collection after Google login, including a dedicated page and webhook integration.
This commit is contained in:
gpt-engineer-app[bot] 2025-06-21 12:30:18 +00:00
parent cf27d1561e
commit 9ac63c926b
5 changed files with 270 additions and 11 deletions

View File

@ -1,9 +1,9 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { Suspense, lazy } from 'react';
import { Toaster } from "@/components/ui/sonner";
import Auth from './pages/Auth';
import EmailConfirmation from './pages/EmailConfirmation';
import CompleteProfile from './pages/CompleteProfile';
import ProtectedRoute from './components/auth/ProtectedRoute';
import { authStore } from './stores/authStore';
@ -27,18 +27,24 @@ function App() {
<Toaster />
<Routes>
{/* The auth route should not be inside ProtectedRoute */}
{/* Auth route */}
<Route
path="/auth"
element={isLoggedIn ? <Navigate to="/" replace /> : <Auth />}
/>
{/* Email confirmation route - should be accessible without authentication */}
{/* Email confirmation route */}
<Route
path="/email-confirmation"
element={<EmailConfirmation />}
/>
{/* Complete profile route - deve ser acessível para usuários logados */}
<Route
path="/complete-profile"
element={<CompleteProfile />}
/>
{/* Protected routes */}
<Route path="/" element={
<ProtectedRoute>

View File

@ -5,31 +5,31 @@ import LoadingState from '../whatsapp/LoadingState';
import { authStore } from '@/stores/authStore';
import { supabase } from '@/integrations/supabase/client';
import { Session } from '@supabase/supabase-js';
import { useProfileCompletion } from '@/hooks/useProfileCompletion';
interface ProtectedRouteProps {
children: ReactNode;
}
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
// We use local state here to avoid re-renders of the whole app
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
const location = useLocation();
// We still update the global store for other components to use
const setLoggedIn = authStore((state) => state.setLoggedIn);
const setUser = authStore((state) => state.setUser);
const isProfileComplete = authStore((state) => state.isProfileComplete);
// Hook para verificar completude do perfil
const { isChecking } = useProfileCompletion(session?.user?.email || '');
useEffect(() => {
// onAuthStateChange fires once on initial load with the current session,
// and then every time the auth state changes.
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session);
setLoggedIn(!!session);
setUser(session?.user ? { id: session.user.id } : null);
// Store or remove user email from localStorage
if (session?.user?.email) {
localStorage.setItem('userEmail', session.user.email);
} else {
@ -45,7 +45,8 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
};
}, [setLoggedIn, setUser]);
if (isLoading) {
// Mostrar loading enquanto verifica auth ou perfil
if (isLoading || isChecking) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingState message="Verificando autenticação..." />
@ -53,12 +54,17 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
);
}
// Se não tem sessão, redirecionar para login
if (!session) {
// Redirect to the login page if there is no session
return <Navigate to="/auth" state={{ from: location }} replace />;
}
// Render the protected content if a session exists
// Se tem sessão mas perfil não está completo, redirecionar para completar perfil
if (session && !isProfileComplete) {
return <Navigate to="/complete-profile" replace />;
}
// Se tudo ok, renderizar conteúdo protegido
return <>{children}</>;
};

View File

@ -0,0 +1,50 @@
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { authStore } from '@/stores/authStore';
export const useProfileCompletion = (userEmail: string) => {
const [isChecking, setIsChecking] = useState(true);
const setProfileComplete = authStore((state) => state.setProfileComplete);
useEffect(() => {
const checkProfileCompletion = async () => {
if (!userEmail) {
setIsChecking(false);
return;
}
try {
console.log('🔍 Verificando completude do perfil para:', userEmail);
const { data: usuario, error } = await supabase
.from('usuarios')
.select('nome, whatsapp')
.eq('email', userEmail.toLowerCase().trim())
.single();
if (error) {
console.error('❌ Erro ao verificar perfil:', error);
setProfileComplete(false);
} else if (usuario) {
// Verificar se tem nome e whatsapp preenchidos
const isComplete = !!(usuario.nome?.trim() && usuario.whatsapp?.trim());
console.log('📋 Perfil completo:', isComplete, { nome: usuario.nome, whatsapp: usuario.whatsapp });
setProfileComplete(isComplete);
} else {
console.log('👤 Usuário não encontrado na tabela usuarios');
setProfileComplete(false);
}
} catch (error) {
console.error('❌ Erro na verificação do perfil:', error);
setProfileComplete(false);
} finally {
setIsChecking(false);
}
};
checkProfileCompletion();
}, [userEmail, setProfileComplete]);
return { isChecking };
};

View File

@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { supabase } from '@/integrations/supabase/client';
import { authStore } from '@/stores/authStore';
import { sendNewUserWebhook } from '@/services/newUserWebhookService';
import { formatarWhatsapp } from '@/utils/whatsappFormatter';
const CompleteProfile = () => {
const navigate = useNavigate();
const [nome, setNome] = useState('');
const [empresa, setEmpresa] = useState('');
const [whatsapp, setWhatsapp] = useState('55');
const [isLoading, setIsLoading] = useState(false);
const [userEmail, setUserEmail] = useState('');
const [userId, setUserId] = useState('');
const setProfileComplete = authStore((state) => state.setProfileComplete);
useEffect(() => {
const getUserData = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
setUserEmail(user.email || '');
setUserId(user.id);
// Pre-fill nome if available from Google
const displayName = user.user_metadata?.full_name || user.user_metadata?.name || '';
if (displayName) {
setNome(displayName);
}
} else {
// Se não há usuário logado, redirecionar para auth
navigate('/auth');
}
};
getUserData();
}, [navigate]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validações
if (!nome.trim() || nome.trim().length < 2) {
toast.error("Nome obrigatório", {
description: "Por favor, informe seu nome completo (mínimo 2 caracteres)."
});
return;
}
if (!whatsapp || whatsapp.replace(/\D/g, '').length < 11) {
toast.error("WhatsApp obrigatório", {
description: "Por favor, informe um número de WhatsApp válido."
});
return;
}
setIsLoading(true);
try {
console.log('📝 Salvando dados complementares do perfil...');
// Atualizar dados do usuário no Supabase
const { error: updateError } = await supabase
.from('usuarios')
.update({
nome: nome.trim(),
empresa: empresa.trim() || null,
whatsapp: whatsapp.replace(/\D/g, '')
})
.eq('email', userEmail.toLowerCase().trim());
if (updateError) {
console.error('❌ Erro ao atualizar usuário:', updateError);
throw new Error('Erro ao salvar dados do perfil');
}
console.log('✅ Dados do perfil salvos com sucesso');
// Enviar webhook para N8N
console.log('📡 Enviando webhook para N8N...');
const webhookSuccess = await sendNewUserWebhook(
userEmail,
userId,
whatsapp.replace(/\D/g, '')
);
if (webhookSuccess) {
console.log('✅ Webhook N8N enviado com sucesso');
} else {
console.warn('⚠️ Falha no webhook N8N, mas perfil foi salvo');
}
// Marcar perfil como completo
setProfileComplete(true);
toast.success("Perfil completado!", {
description: "Seus dados foram salvos e sua conta está ativa.",
});
// Redirecionar para dashboard
navigate('/');
} catch (error) {
console.error('❌ Erro ao completar perfil:', error);
toast.error("Erro ao salvar dados", {
description: "Ocorreu um erro ao salvar seus dados. Tente novamente.",
});
} finally {
setIsLoading(false);
}
};
const handleWhatsappChange = (value: string) => {
setWhatsapp(formatarWhatsapp(value));
};
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 px-4">
<Card className="w-full max-w-md">
<CardHeader className="space-y-1 text-center">
<CardTitle className="text-2xl font-bold">Complete seu Perfil</CardTitle>
<CardDescription>
Para continuar, precisamos de algumas informações obrigatórias para configurar sua conta
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="nome">Nome Completo *</Label>
<Input
id="nome"
type="text"
placeholder="Seu nome completo"
value={nome}
onChange={(e) => setNome(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="empresa">Empresa (Opcional)</Label>
<Input
id="empresa"
type="text"
placeholder="Nome da sua empresa"
value={empresa}
onChange={(e) => setEmpresa(e.target.value)}
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="whatsapp">WhatsApp *</Label>
<Input
id="whatsapp"
type="tel"
placeholder="(11) 99999-9999"
value={whatsapp}
onChange={(e) => handleWhatsappChange(e.target.value)}
required
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
Necessário para integração com WhatsApp e notificações
</p>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Salvando..." : "Completar Perfil"}
</Button>
</form>
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Por que estes dados?</strong><br />
Nome e WhatsApp são necessários para configurar automaticamente seu workspace e integrações personalizadas.
</p>
</div>
</CardContent>
</Card>
</div>
);
};
export default CompleteProfile;

View File

@ -4,13 +4,17 @@ import { create } from 'zustand';
interface AuthState {
isLoggedIn: boolean;
user: { id: string } | null;
isProfileComplete: boolean;
setLoggedIn: (status: boolean) => void;
setUser: (user: { id: string } | null) => void;
setProfileComplete: (complete: boolean) => void;
}
export const authStore = create<AuthState>((set) => ({
isLoggedIn: false,
user: null,
isProfileComplete: true, // Default true para não afetar usuários existentes
setLoggedIn: (status) => set({ isLoggedIn: status }),
setUser: (user) => set({ user }),
setProfileComplete: (complete) => set({ isProfileComplete: complete }),
}));