diff --git a/src/App.tsx b/src/App.tsx index 94773a1..be2abfb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ - import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -8,6 +7,7 @@ import { supabase } from "@/integrations/supabase/client"; import { useAuthStore } from "@/stores/authStore"; import ProtectedRoute from "@/components/auth/ProtectedRoute"; import Layout from "@/components/layout/Layout"; +import ResetPassword from '@/pages/ResetPassword'; // Pages import Index from "./pages/Index"; @@ -61,6 +61,7 @@ function App() { } /> } /> } /> + } /> {/* Rotas protegidas */} }> diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 0000000..b6095a1 --- /dev/null +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,121 @@ + +import { useState } from 'react'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { ArrowLeft, Mail, CheckCircle2 } from "lucide-react"; +import { supabase } from '@/integrations/supabase/client'; + +interface ForgotPasswordFormProps { + onBackToLogin: () => void; +} + +const ForgotPasswordForm = ({ onBackToLogin }: ForgotPasswordFormProps) => { + const [email, setEmail] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [emailSent, setEmailSent] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/reset-password`, + }); + + if (error) { + setError(error.message); + } else { + setEmailSent(true); + } + } catch (error) { + setError('Erro inesperado. Tente novamente.'); + } finally { + setIsLoading(false); + } + }; + + if (emailSent) { + return ( + + +
+ +
+ Email enviado! + + Enviamos um link para redefinir sua senha para {email} + +
+ +
+

+ Verifique sua caixa de entrada e spam. O link expira em 1 hora. +

+
+ +
+
+ ); + } + + return ( + + +
+ +
+ Esqueceu sua senha? + + Digite seu email e enviaremos um link para redefinir sua senha + +
+ +
+
+ + setEmail(e.target.value)} + required + disabled={isLoading} + /> +
+ + {error && ( + + {error} + + )} + + + + +
+
+
+ ); +}; + +export default ForgotPasswordForm; diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index a2b671c..44589b1 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -1,73 +1,117 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { supabase } from "@/integrations/supabase/client"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; import { toast } from "sonner"; +import { supabase } from '@/integrations/supabase/client'; +import { useNavigate } from 'react-router-dom'; interface LoginFormProps { isLoading: boolean; - setIsLoading: (isLoading: boolean) => void; + setIsLoading: (loading: boolean) => void; + onForgotPassword: () => void; } -const LoginForm = ({ isLoading, setIsLoading }: LoginFormProps) => { +const LoginForm = ({ isLoading, setIsLoading, onForgotPassword }: LoginFormProps) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); const navigate = useNavigate(); - - const [loginEmail, setLoginEmail] = useState(''); - const [loginSenha, setLoginSenha] = useState(''); - - const handleLogin = async (e: React.FormEvent) => { + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); + setError(''); - const { error } = await supabase.auth.signInWithPassword({ - email: loginEmail, - password: loginSenha, - }); - - setIsLoading(false); - - if (error) { - toast.error("Erro no login", { - description: "Email ou senha incorretos. Verifique seus dados." + try { + const { data, error } = await supabase.auth.signInWithPassword({ + email: email.trim().toLowerCase(), + password: password, }); - } else { - toast.success("Login realizado com sucesso", { - description: "Bem-vindo de volta!" - }); - // Redirecionar para o dashboard após login bem-sucedido - navigate('/dashboard'); + + if (error) { + console.error('Erro no login:', error); + + if (error.message.includes('Invalid login credentials')) { + setError('Email ou senha incorretos. Verifique suas credenciais.'); + } else if (error.message.includes('Email not confirmed')) { + setError('Email não confirmado. Verifique sua caixa de entrada.'); + } else { + setError(error.message); + } + return; + } + + if (data.user) { + console.log('✅ Login realizado com sucesso:', data.user.email); + + toast.success("Login realizado com sucesso!", { + description: `Bem-vindo, ${data.user.email}!`, + }); + + // Redirecionar para dashboard + navigate('/'); + } + } catch (error) { + console.error('Erro inesperado no login:', error); + setError('Erro inesperado. Tente novamente.'); + } finally { + setIsLoading(false); } }; return ( -
+
+ setLoginEmail(e.target.value)} - disabled={isLoading} + placeholder="seu@email.com" + value={email} + onChange={(e) => setEmail(e.target.value)} required + disabled={isLoading} />
+
+ setLoginSenha(e.target.value)} - disabled={isLoading} + placeholder="Sua senha" + value={password} + onChange={(e) => setPassword(e.target.value)} required + disabled={isLoading} />
+ + {error && ( + + + {error} + + )} + + +
+ +
); }; diff --git a/src/components/auth/ProtectedRoute.tsx b/src/components/auth/ProtectedRoute.tsx index 764147d..0cef0ff 100644 --- a/src/components/auth/ProtectedRoute.tsx +++ b/src/components/auth/ProtectedRoute.tsx @@ -18,65 +18,96 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => { const setLoggedIn = useAuthStore((state) => state.setLoggedIn); const setUser = useAuthStore((state) => state.setUser); + const setSession = useAuthStore((state) => state.setSession); const isProfileComplete = useAuthStore((state) => state.isProfileComplete); // Hook para verificar completude do perfil const { isChecking } = useProfileCompletion(session?.user?.email || ''); useEffect(() => { + let mounted = true; + // Verificar sessão inicial - supabase.auth.getSession().then(({ data: { session } }) => { - console.log('🔐 [PROTECTED_ROUTE] Sessão inicial:', session?.user?.email); - setSession(session); - setLoggedIn(!!session); - setUser(session?.user ? { id: session.user.id } : null); - - if (session?.user?.email) { - localStorage.setItem('userEmail', session.user.email); - console.log('👤 [PROTECTED_ROUTE] Email salvo no localStorage:', session.user.email); + const getInitialSession = async () => { + try { + const { data: { session }, error } = await supabase.auth.getSession(); - // Disparar evento customizado para notificar outros componentes sobre o login - window.dispatchEvent(new CustomEvent('userLoggedIn', { - detail: { email: session.user.email } - })); - } else { - localStorage.removeItem('userEmail'); - } - - setIsLoading(false); - }); - - // Listener para mudanças de auth - const { data: { subscription } } = supabase.auth.onAuthStateChange( - (_event, session) => { - console.log('🔄 [PROTECTED_ROUTE] Mudança de auth:', _event, session?.user?.email); - setSession(session); - setLoggedIn(!!session); - setUser(session?.user ? { id: session.user.id } : null); - - if (session?.user?.email) { - localStorage.setItem('userEmail', session.user.email); - console.log('👤 [PROTECTED_ROUTE] Email atualizado no localStorage:', session.user.email); + if (error) { + console.error('🔐 [PROTECTED_ROUTE] Erro ao obter sessão:', error); + } + + if (mounted) { + console.log('🔐 [PROTECTED_ROUTE] Sessão inicial:', session?.user?.email || 'Nenhuma sessão'); + setSession(session); + setLoggedIn(!!session); + setUser(session?.user ? { id: session.user.id } : null); - // Disparar evento customizado para notificar sobre o login - if (_event === 'SIGNED_IN') { - console.log('🎉 [PROTECTED_ROUTE] Login detectado, disparando evento'); + if (session?.user?.email) { + localStorage.setItem('userEmail', session.user.email); + console.log('👤 [PROTECTED_ROUTE] Email salvo no localStorage:', session.user.email); + + // Disparar evento customizado para notificar outros componentes sobre o login window.dispatchEvent(new CustomEvent('userLoggedIn', { detail: { email: session.user.email } })); + } else { + localStorage.removeItem('userEmail'); } - } else { - localStorage.removeItem('userEmail'); + + setIsLoading(false); } + } catch (error) { + console.error('🔐 [PROTECTED_ROUTE] Erro inesperado ao obter sessão:', error); + if (mounted) { + setIsLoading(false); + } + } + }; - setIsLoading(false); + getInitialSession(); + + // Listener para mudanças de auth com debounce + let timeoutId: NodeJS.Timeout; + + const { data: { subscription } } = supabase.auth.onAuthStateChange( + (event, session) => { + console.log('🔄 [PROTECTED_ROUTE] Mudança de auth:', event, session?.user?.email || 'Sem sessão'); + + // Debounce para evitar múltiplas atualizações + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + if (mounted) { + setSession(session); + setLoggedIn(!!session); + setUser(session?.user ? { id: session.user.id } : null); + + if (session?.user?.email) { + localStorage.setItem('userEmail', session.user.email); + console.log('👤 [PROTECTED_ROUTE] Email atualizado no localStorage:', session.user.email); + + // Disparar evento customizado para notificar sobre o login + if (event === 'SIGNED_IN') { + console.log('🎉 [PROTECTED_ROUTE] Login detectado, disparando evento'); + window.dispatchEvent(new CustomEvent('userLoggedIn', { + detail: { email: session.user.email } + })); + } + } else { + localStorage.removeItem('userEmail'); + } + + setIsLoading(false); + } + }, 100); // Debounce de 100ms } ); return () => { + mounted = false; + clearTimeout(timeoutId); subscription.unsubscribe(); }; - }, [setLoggedIn, setUser]); + }, [setLoggedIn, setUser, setSession]); // Mostrar loading enquanto verifica auth ou perfil if (isLoading || isChecking) { diff --git a/src/components/auth/SocialLoginButtons.tsx b/src/components/auth/SocialLoginButtons.tsx index ec0008f..ee1edbc 100644 --- a/src/components/auth/SocialLoginButtons.tsx +++ b/src/components/auth/SocialLoginButtons.tsx @@ -14,22 +14,22 @@ const SocialLoginButtons = () => { console.log('🔐 Iniciando login com Google...'); console.log('🌐 URL atual:', window.location.origin); - // Determinar a URL de redirecionamento correta baseada no hostname - SEMPRE para /dashboard + // Determinar a URL de redirecionamento correta baseada no hostname let redirectUrl; const hostname = window.location.hostname; if (hostname === 'localhost') { - redirectUrl = 'http://localhost:3000/dashboard'; + redirectUrl = 'http://localhost:3000/'; } else if (hostname.includes('financehome.innova1001.com.br')) { - redirectUrl = 'https://financehome.innova1001.com.br/dashboard'; + redirectUrl = 'https://financehome.innova1001.com.br/'; } else if (hostname.includes('lovableproject.com')) { - redirectUrl = `${window.location.origin}/dashboard`; + redirectUrl = `${window.location.origin}/`; } else { - // Fallback para qualquer outro domínio - sempre para dashboard - redirectUrl = `${window.location.origin}/dashboard`; + // Fallback para qualquer outro domínio + redirectUrl = `${window.location.origin}/`; } - console.log('🔗 URL de redirecionamento (DASHBOARD):', redirectUrl); + console.log('🔗 URL de redirecionamento:', redirectUrl); const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'google', @@ -37,7 +37,7 @@ const SocialLoginButtons = () => { redirectTo: redirectUrl, queryParams: { access_type: 'offline', - prompt: 'consent', + prompt: 'select_account', } } }); @@ -51,6 +51,7 @@ const SocialLoginButtons = () => { }); } else { console.log('✅ Redirecionamento iniciado...'); + // Não fazer nada aqui, deixar o redirecionamento acontecer naturalmente } } catch (error) { console.error('❌ Erro geral no login com Google:', error); diff --git a/src/components/whatsappGroups/CreateGroupForm.tsx b/src/components/whatsappGroups/CreateGroupForm.tsx index 281ae49..5292d1c 100644 --- a/src/components/whatsappGroups/CreateGroupForm.tsx +++ b/src/components/whatsappGroups/CreateGroupForm.tsx @@ -1,14 +1,12 @@ import { useEffect, useState } from 'react'; -import { useExistingInstanceCheck } from '@/hooks/whatsapp/useExistingInstanceCheck'; -import { useGroupCreation } from '@/hooks/whatsappGroups/useGroupCreation'; import { listarGruposWhatsApp } from '@/services/gruposWhatsAppService'; import LoadingState from './LoadingState'; -import NoInstanceState from './NoInstanceState'; import GroupCreationForm from './GroupCreationForm'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; -import { Info, Loader2 } from 'lucide-react'; +import { Info } from 'lucide-react'; +import { useGroupCreation } from '@/hooks/whatsappGroups/useGroupCreation'; interface CreateGroupFormProps { userEmail: string; @@ -20,13 +18,6 @@ const CreateGroupForm = ({ userEmail, onSuccess }: CreateGroupFormProps) => { const [jaTemGrupo, setJaTemGrupo] = useState(false); const [grupoExistente, setGrupoExistente] = useState(null); - const { - hasExistingInstance, - checkingExistingInstance, - existingInstanceData, - recheckInstance - } = useExistingInstanceCheck(userEmail); - const { cadastrando, handleCadastrarGrupo } = useGroupCreation(userEmail, onSuccess); // Verificar se o usuário já tem grupos cadastrados @@ -56,12 +47,11 @@ const CreateGroupForm = ({ userEmail, onSuccess }: CreateGroupFormProps) => { }; useEffect(() => { - recheckInstance(); verificarGruposExistentes(); }, [userEmail]); - if (checkingExistingInstance || verificandoGrupos) { - return ; + if (verificandoGrupos) { + return ; } // Se já tem grupo, mostrar informação @@ -93,18 +83,14 @@ const CreateGroupForm = ({ userEmail, onSuccess }: CreateGroupFormProps) => { ); } - // Se não tem instância conectada, mostrar estado de sem instância - if (!hasExistingInstance) { - return ; - } - const handleSubmit = (nomeGrupo: string) => { - handleCadastrarGrupo(nomeGrupo, existingInstanceData); + // Agora pode criar grupo sem verificar instância + handleCadastrarGrupo(nomeGrupo, { whatsapp: userEmail }); }; return ( (null); - const [webhookEnviado, setWebhookEnviado] = useState(false); - const [workflowAtivado, setWorkflowAtivado] = useState(false); const [mensagemStatus, setMensagemStatus] = useState<{ tipo: 'info' | 'success' | 'error'; texto: string; @@ -85,23 +83,8 @@ const CreateGroupFormSimple = ({ userEmail, onSuccess }: CreateGroupFormProps) = setCarregando(true); setMensagemStatus(null); - setWebhookEnviado(false); - setWorkflowAtivado(false); try { - // Verificar se o usuário tem instância WhatsApp - setMensagemStatus({ tipo: 'info', texto: 'Verificando sua instância do WhatsApp...' }); - - const instanciaInfo = await verificarInstanciaWhatsApp(); - - if (!instanciaInfo.hasInstance) { - setMensagemStatus({ - tipo: 'error', - texto: 'Você precisa ter uma instância do WhatsApp conectada. Acesse o menu "WhatsApp" primeiro.' - }); - return; - } - // Criar ou encontrar grupo setMensagemStatus({ tipo: 'info', texto: 'Cadastrando grupo...' }); @@ -122,7 +105,6 @@ const CreateGroupFormSimple = ({ userEmail, onSuccess }: CreateGroupFormProps) = try { console.log(`🔔 Enviando webhook ativarworkflow para usuário: ${userEmail}`); await activateUserWorkflow(userEmail); - setWorkflowAtivado(true); console.log('✅ [GRUPO] Webhook ativarworkflow enviado com sucesso no cadastro do grupo'); } catch (workflowError) { console.error('❌ Erro ao enviar webhook ativarworkflow:', workflowError); @@ -136,7 +118,6 @@ const CreateGroupFormSimple = ({ userEmail, onSuccess }: CreateGroupFormProps) = try { console.log(`🔔 Enviando email para N8N configurar webhook: ${userEmail}`); await createEvolutionWebhook(userEmail); - setWebhookEnviado(true); console.log('✅ [GRUPO] Email enviado com sucesso para N8N no cadastro do grupo'); } catch (webhookError) { console.error('❌ Erro ao enviar email para N8N:', webhookError); diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index 63876fe..2424a53 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -6,16 +6,17 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Boxes } from "@/components/ui/background-boxes"; import { Button } from "@/components/ui/button"; import { ArrowLeft } from "lucide-react"; -import { cn } from "@/lib/utils"; import LoginForm from '@/components/auth/LoginForm'; import RegisterForm from '@/components/auth/RegisterForm'; import SocialLoginButtons from '@/components/auth/SocialLoginButtons'; import AuthSecurityFeatures from '@/components/auth/AuthSecurityFeatures'; +import ForgotPasswordForm from '@/components/auth/ForgotPasswordForm'; import { toast } from "sonner"; const Auth = () => { const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState("login"); + const [showForgotPassword, setShowForgotPassword] = useState(false); const location = useLocation(); // Show success message when redirected from registration or email confirmation @@ -32,6 +33,35 @@ const Auth = () => { } }, [location.state]); + const handleForgotPassword = () => { + setShowForgotPassword(true); + }; + + const handleBackToLogin = () => { + setShowForgotPassword(false); + setActiveTab("login"); + }; + + if (showForgotPassword) { + return ( +
+
+ + + + + + +
+ +
+
+ ); + } + return (
@@ -66,7 +96,11 @@ const Auth = () => { - + diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..c3c568f --- /dev/null +++ b/src/pages/ResetPassword.tsx @@ -0,0 +1,147 @@ + +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { CheckCircle2, Lock } from "lucide-react"; +import { supabase } from '@/integrations/supabase/client'; +import { toast } from "sonner"; + +const ResetPassword = () => { + const navigate = useNavigate(); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + useEffect(() => { + // Verificar se há um token de recuperação na URL + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const accessToken = hashParams.get('access_token'); + const refreshToken = hashParams.get('refresh_token'); + + if (!accessToken || !refreshToken) { + toast.error("Link inválido ou expirado"); + navigate('/auth'); + } + }, [navigate]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + if (password !== confirmPassword) { + setError('As senhas não coincidem'); + setIsLoading(false); + return; + } + + if (password.length < 6) { + setError('A senha deve ter pelo menos 6 caracteres'); + setIsLoading(false); + return; + } + + try { + const { error } = await supabase.auth.updateUser({ + password: password + }); + + if (error) { + setError(error.message); + } else { + setSuccess(true); + toast.success("Senha redefinida com sucesso!"); + setTimeout(() => { + navigate('/auth'); + }, 3000); + } + } catch (error) { + setError('Erro inesperado. Tente novamente.'); + } finally { + setIsLoading(false); + } + }; + + if (success) { + return ( +
+ + +
+ +
+ Senha redefinida! + + Sua senha foi alterada com sucesso. Você será redirecionado para o login. + +
+
+
+ ); + } + + return ( +
+ + +
+ +
+ Nova senha + + Digite sua nova senha + +
+ +
+
+ + setPassword(e.target.value)} + required + disabled={isLoading} + minLength={6} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + disabled={isLoading} + minLength={6} + /> +
+ + {error && ( + + {error} + + )} + + +
+
+
+
+ ); +}; + +export default ResetPassword;