Implement authentication flow
Implement mandatory data collection after Google login, including a dedicated page and webhook integration.
This commit is contained in:
parent
cf27d1561e
commit
9ac63c926b
12
src/App.tsx
12
src/App.tsx
@ -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>
|
||||
|
||||
@ -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}</>;
|
||||
};
|
||||
|
||||
|
||||
50
src/hooks/useProfileCompletion.ts
Normal file
50
src/hooks/useProfileCompletion.ts
Normal 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 };
|
||||
};
|
||||
193
src/pages/CompleteProfile.tsx
Normal file
193
src/pages/CompleteProfile.tsx
Normal 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;
|
||||
@ -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 }),
|
||||
}));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user