296 lines
11 KiB
TypeScript
296 lines
11 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Lock, Eye, EyeOff, CheckCircle } from "lucide-react";
|
|
import toast from "react-hot-toast";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { authService } from "../services";
|
|
|
|
const ResetPassword: React.FC = () => {
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
// Extrair access_token do hash da URL (#access_token=...)
|
|
// OU do query string (?token=...&type=recovery)
|
|
|
|
let token: string | null = null;
|
|
let type: string | null = null;
|
|
|
|
// Primeiro tenta no hash (formato padrão do Supabase após redirect)
|
|
const hash = window.location.hash;
|
|
console.log("[ResetPassword] Hash completo:", hash);
|
|
|
|
if (hash) {
|
|
const hashParams = new URLSearchParams(hash.substring(1));
|
|
token = hashParams.get("access_token");
|
|
type = hashParams.get("type");
|
|
|
|
console.log("[ResetPassword] Token do hash:", token ? token.substring(0, 20) + "..." : "null");
|
|
console.log("[ResetPassword] Type do hash:", type);
|
|
}
|
|
|
|
// Se não encontrou no hash, tenta no query string
|
|
if (!token) {
|
|
const search = window.location.search;
|
|
console.log("[ResetPassword] Query string completo:", search);
|
|
|
|
if (search) {
|
|
const queryParams = new URLSearchParams(search);
|
|
token = queryParams.get("token");
|
|
type = queryParams.get("type");
|
|
|
|
console.log("[ResetPassword] Token do query:", token ? token.substring(0, 20) + "..." : "null");
|
|
console.log("[ResetPassword] Type do query:", type);
|
|
}
|
|
}
|
|
|
|
if (token) {
|
|
setAccessToken(token);
|
|
console.log("[ResetPassword] ✅ Token de recuperação detectado e armazenado");
|
|
console.log("[ResetPassword] Type:", type);
|
|
} else {
|
|
console.error("[ResetPassword] ❌ Token não encontrado no hash nem no query string");
|
|
console.log("[ResetPassword] URL completa:", window.location.href);
|
|
toast.error("Link de recuperação inválido ou expirado");
|
|
setTimeout(() => navigate("/"), 3000);
|
|
}
|
|
}, [navigate]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Validações
|
|
if (!password.trim()) {
|
|
toast.error("Digite a nova senha");
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
toast.error("A senha deve ter pelo menos 6 caracteres");
|
|
return;
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
toast.error("As senhas não coincidem");
|
|
return;
|
|
}
|
|
|
|
if (!accessToken) {
|
|
toast.error("Token de recuperação não encontrado");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
console.log("[ResetPassword] Atualizando senha...");
|
|
|
|
// Atualizar senha usando o token de recuperação
|
|
await authService.updatePassword(accessToken, password);
|
|
|
|
console.log("[ResetPassword] Senha atualizada com sucesso!");
|
|
toast.success("Senha atualizada com sucesso!");
|
|
|
|
// Limpar formulário
|
|
setPassword("");
|
|
setConfirmPassword("");
|
|
|
|
// Redirecionar para login após 2 segundos
|
|
setTimeout(() => {
|
|
navigate("/");
|
|
}, 2000);
|
|
} catch (error: unknown) {
|
|
console.error("[ResetPassword] Erro ao atualizar senha:", error);
|
|
const err = error as {
|
|
response?: {
|
|
data?: {
|
|
error_description?: string;
|
|
message?: string;
|
|
msg?: string;
|
|
error_code?: string;
|
|
}
|
|
};
|
|
message?: string;
|
|
};
|
|
|
|
// Mensagem específica para senha igual
|
|
if (err?.response?.data?.error_code === "same_password") {
|
|
toast.error("A nova senha deve ser diferente da senha atual");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const errorMessage =
|
|
err?.response?.data?.msg ||
|
|
err?.response?.data?.error_description ||
|
|
err?.response?.data?.message ||
|
|
err?.message ||
|
|
"Erro ao atualizar senha. Tente novamente.";
|
|
toast.error(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Validações em tempo real
|
|
const hasMinLength = password.length >= 6;
|
|
const passwordsMatch = password === confirmPassword && confirmPassword !== "";
|
|
|
|
// Se não tiver token ainda, mostrar loading
|
|
if (!accessToken) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Verificando link de recuperação...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
|
|
<div className="max-w-md w-full">
|
|
{/* Header */}
|
|
<div className="text-center mb-8">
|
|
<div className="bg-gradient-to-r from-blue-600 to-blue-400 dark:from-blue-700 dark:to-blue-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 shadow-md">
|
|
<Lock className="w-8 h-8 text-white" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
|
Redefinir Senha
|
|
</h1>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Digite sua nova senha abaixo
|
|
</p>
|
|
</div>
|
|
|
|
{/* Formulário */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors">
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Nova Senha */}
|
|
<div>
|
|
<label
|
|
htmlFor="password"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
|
>
|
|
Nova Senha
|
|
</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-gray-100"
|
|
placeholder="Digite sua nova senha"
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="w-5 h-5" />
|
|
) : (
|
|
<Eye className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confirmar Senha */}
|
|
<div>
|
|
<label
|
|
htmlFor="confirmPassword"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
|
>
|
|
Confirmar Nova Senha
|
|
</label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
id="confirmPassword"
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
className="w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-gray-100"
|
|
placeholder="Confirme sua nova senha"
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
{showConfirmPassword ? (
|
|
<EyeOff className="w-5 h-5" />
|
|
) : (
|
|
<Eye className="w-5 h-5" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Indicadores de Validação */}
|
|
{password && (
|
|
<div className="space-y-2 text-sm">
|
|
<div
|
|
className={`flex items-center gap-2 ${
|
|
hasMinLength ? "text-green-600" : "text-gray-500"
|
|
}`}
|
|
>
|
|
<CheckCircle className="w-4 h-4" />
|
|
<span>Mínimo de 6 caracteres</span>
|
|
</div>
|
|
{confirmPassword && (
|
|
<div
|
|
className={`flex items-center gap-2 ${
|
|
passwordsMatch ? "text-green-600" : "text-red-600"
|
|
}`}
|
|
>
|
|
<CheckCircle className="w-4 h-4" />
|
|
<span>
|
|
{passwordsMatch
|
|
? "As senhas coincidem"
|
|
: "As senhas não coincidem"}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Botão Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !hasMinLength || !passwordsMatch}
|
|
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
|
|
>
|
|
{loading ? "Atualizando..." : "Redefinir Senha"}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Link para voltar */}
|
|
<div className="mt-6 text-center">
|
|
<button
|
|
onClick={() => navigate("/")}
|
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
|
>
|
|
Voltar para o login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ResetPassword;
|