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 { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { Suspense, lazy } from 'react';
|
import { Suspense, lazy } from 'react';
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import Auth from './pages/Auth';
|
import Auth from './pages/Auth';
|
||||||
import EmailConfirmation from './pages/EmailConfirmation';
|
import EmailConfirmation from './pages/EmailConfirmation';
|
||||||
|
import CompleteProfile from './pages/CompleteProfile';
|
||||||
import ProtectedRoute from './components/auth/ProtectedRoute';
|
import ProtectedRoute from './components/auth/ProtectedRoute';
|
||||||
import { authStore } from './stores/authStore';
|
import { authStore } from './stores/authStore';
|
||||||
|
|
||||||
@ -27,18 +27,24 @@ function App() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* The auth route should not be inside ProtectedRoute */}
|
{/* Auth route */}
|
||||||
<Route
|
<Route
|
||||||
path="/auth"
|
path="/auth"
|
||||||
element={isLoggedIn ? <Navigate to="/" replace /> : <Auth />}
|
element={isLoggedIn ? <Navigate to="/" replace /> : <Auth />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Email confirmation route - should be accessible without authentication */}
|
{/* Email confirmation route */}
|
||||||
<Route
|
<Route
|
||||||
path="/email-confirmation"
|
path="/email-confirmation"
|
||||||
element={<EmailConfirmation />}
|
element={<EmailConfirmation />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Complete profile route - deve ser acessível para usuários logados */}
|
||||||
|
<Route
|
||||||
|
path="/complete-profile"
|
||||||
|
element={<CompleteProfile />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Protected routes */}
|
{/* Protected routes */}
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
|
|||||||
@ -5,31 +5,31 @@ import LoadingState from '../whatsapp/LoadingState';
|
|||||||
import { authStore } from '@/stores/authStore';
|
import { authStore } from '@/stores/authStore';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { Session } from '@supabase/supabase-js';
|
import { Session } from '@supabase/supabase-js';
|
||||||
|
import { useProfileCompletion } from '@/hooks/useProfileCompletion';
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
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 [session, setSession] = useState<Session | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// We still update the global store for other components to use
|
|
||||||
const setLoggedIn = authStore((state) => state.setLoggedIn);
|
const setLoggedIn = authStore((state) => state.setLoggedIn);
|
||||||
const setUser = authStore((state) => state.setUser);
|
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(() => {
|
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(
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||||
(_event, session) => {
|
(_event, session) => {
|
||||||
setSession(session);
|
setSession(session);
|
||||||
setLoggedIn(!!session);
|
setLoggedIn(!!session);
|
||||||
setUser(session?.user ? { id: session.user.id } : null);
|
setUser(session?.user ? { id: session.user.id } : null);
|
||||||
|
|
||||||
// Store or remove user email from localStorage
|
|
||||||
if (session?.user?.email) {
|
if (session?.user?.email) {
|
||||||
localStorage.setItem('userEmail', session.user.email);
|
localStorage.setItem('userEmail', session.user.email);
|
||||||
} else {
|
} else {
|
||||||
@ -45,7 +45,8 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|||||||
};
|
};
|
||||||
}, [setLoggedIn, setUser]);
|
}, [setLoggedIn, setUser]);
|
||||||
|
|
||||||
if (isLoading) {
|
// Mostrar loading enquanto verifica auth ou perfil
|
||||||
|
if (isLoading || isChecking) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<LoadingState message="Verificando autenticação..." />
|
<LoadingState message="Verificando autenticação..." />
|
||||||
@ -53,12 +54,17 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Se não tem sessão, redirecionar para login
|
||||||
if (!session) {
|
if (!session) {
|
||||||
// Redirect to the login page if there is no session
|
|
||||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
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}</>;
|
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 {
|
interface AuthState {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
user: { id: string } | null;
|
user: { id: string } | null;
|
||||||
|
isProfileComplete: boolean;
|
||||||
setLoggedIn: (status: boolean) => void;
|
setLoggedIn: (status: boolean) => void;
|
||||||
setUser: (user: { id: string } | null) => void;
|
setUser: (user: { id: string } | null) => void;
|
||||||
|
setProfileComplete: (complete: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authStore = create<AuthState>((set) => ({
|
export const authStore = create<AuthState>((set) => ({
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
user: null,
|
user: null,
|
||||||
|
isProfileComplete: true, // Default true para não afetar usuários existentes
|
||||||
setLoggedIn: (status) => set({ isLoggedIn: status }),
|
setLoggedIn: (status) => set({ isLoggedIn: status }),
|
||||||
setUser: (user) => set({ user }),
|
setUser: (user) => set({ user }),
|
||||||
|
setProfileComplete: (complete) => set({ isProfileComplete: complete }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user