feat(auth): migração final do UI frontend para Logto OIDC via GoTrue
This commit is contained in:
@@ -7,7 +7,7 @@ interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<{ error: unknown }>;
|
||||
signIn: () => Promise<{ error: unknown }>;
|
||||
signUp: (email: string, password: string) => Promise<{ error: unknown }>;
|
||||
signOut: () => Promise<void>;
|
||||
updatePassword: (password: string) => Promise<{ error: unknown }>;
|
||||
@@ -334,15 +334,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user?.id, authInitialized]);
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const signIn = async () => {
|
||||
try {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'keycloak',
|
||||
options: {
|
||||
scopes: 'openid email profile',
|
||||
}
|
||||
});
|
||||
return { error };
|
||||
} catch (error) {
|
||||
logger.error('Erro crítico no login:', error);
|
||||
logger.error('Erro crítico no login SSO:', error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
+18
-10
@@ -9,6 +9,7 @@ import { AuthProvider } from './hooks/useAuth'
|
||||
import { IconStyleProvider } from './hooks/useIconStyle'
|
||||
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||
import { AppRoutes } from './components/AppRoutes'
|
||||
import { LogtoProvider, LogtoConfig } from '@logto/react'
|
||||
|
||||
import './index.css'
|
||||
|
||||
@@ -28,6 +29,11 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
})
|
||||
|
||||
const config: LogtoConfig = {
|
||||
endpoint: 'https://logto-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io',
|
||||
appId: '4qun0u1tfce1fdn3pxrgq',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Render da aplicação
|
||||
// ============================================================================
|
||||
@@ -36,16 +42,18 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<IconStyleProvider>
|
||||
<ThemeProvider defaultTheme="system">
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</IconStyleProvider>
|
||||
</AuthProvider>
|
||||
<LogtoProvider config={config}>
|
||||
<AuthProvider>
|
||||
<IconStyleProvider>
|
||||
<ThemeProvider defaultTheme="system">
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</IconStyleProvider>
|
||||
</AuthProvider>
|
||||
</LogtoProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
|
||||
+34
-330
@@ -16,57 +16,35 @@ import { Eye, EyeOff, Mail, Lock } from 'lucide-react';
|
||||
|
||||
const Auth = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||
const [loginData, setLoginData] = useState({ email: '', password: '' });
|
||||
const [signupData, setSignupData] = useState({ email: '', password: '', confirmPassword: '' });
|
||||
const { signIn, signUp, user, loading, isRecoveryFlow, session } = useAuth();
|
||||
const { signIn, user, loading, isRecoveryFlow, session } = useAuth();
|
||||
const { brandSettings } = useBrandSettings();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
|
||||
const recoveryType = searchParams.get('type');
|
||||
const hashParams = new URLSearchParams(location.hash.substring(1));
|
||||
const hashType = hashParams.get('type');
|
||||
const accessToken = hashParams.get('access_token');
|
||||
const refreshToken = hashParams.get('refresh_token');
|
||||
|
||||
const shouldShowReset = Boolean(
|
||||
recoveryType === 'recovery' ||
|
||||
hashType === 'recovery' ||
|
||||
isRecoveryFlow ||
|
||||
(accessToken && refreshToken)
|
||||
);
|
||||
|
||||
// Estado para controlar se está em modo de recuperação
|
||||
const [showPasswordReset, setShowPasswordReset] = useState(false);
|
||||
const [showPasswordReset, setShowPasswordReset] = useState(shouldShowReset);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('🔍 Auth.tsx - Verificando fluxo de recuperação...');
|
||||
console.log('Location:', {
|
||||
pathname: location.pathname,
|
||||
search: location.search,
|
||||
hash: location.hash
|
||||
});
|
||||
console.log('Auth states:', {
|
||||
user: !!user,
|
||||
session: !!session,
|
||||
isRecoveryFlow,
|
||||
loading
|
||||
});
|
||||
|
||||
// Verificar se é fluxo de recuperação
|
||||
const recoveryType = searchParams.get('type');
|
||||
const hashParams = new URLSearchParams(location.hash.substring(1));
|
||||
const hashType = hashParams.get('type');
|
||||
const accessToken = hashParams.get('access_token');
|
||||
const refreshToken = hashParams.get('refresh_token');
|
||||
|
||||
console.log('🔍 Parâmetros de recuperação:', {
|
||||
recoveryType,
|
||||
hashType,
|
||||
accessToken: !!accessToken,
|
||||
refreshToken: !!refreshToken,
|
||||
isRecoveryFlow
|
||||
});
|
||||
|
||||
// Se há type=recovery OU isRecoveryFlow OU tokens, mostrar tela de reset
|
||||
const shouldShowReset = recoveryType === 'recovery' ||
|
||||
hashType === 'recovery' ||
|
||||
isRecoveryFlow ||
|
||||
(accessToken && refreshToken);
|
||||
|
||||
console.log('🔑 Deve mostrar reset de senha:', shouldShowReset);
|
||||
setShowPasswordReset(Boolean(shouldShowReset));
|
||||
}, [searchParams, location.hash, location.search, isRecoveryFlow, user, session]);
|
||||
if (shouldShowReset !== showPasswordReset) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
setShowPasswordReset(shouldShowReset);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [shouldShowReset, showPasswordReset, loading, location.pathname]);
|
||||
|
||||
// Redirecionar usuários autenticados para página principal (exceto em fluxo de recuperação)
|
||||
useEffect(() => {
|
||||
@@ -139,112 +117,18 @@ const Auth = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const validatePassword = (password: string) => {
|
||||
const minLength = password.length >= 8;
|
||||
const hasUpperCase = /[A-Z]/.test(password);
|
||||
const hasLowerCase = /[a-z]/.test(password);
|
||||
const hasNumbers = /\d/.test(password);
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
|
||||
const score = [minLength, hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChar].filter(Boolean).length;
|
||||
|
||||
return {
|
||||
score,
|
||||
minLength,
|
||||
hasUpperCase,
|
||||
hasLowerCase,
|
||||
hasNumbers,
|
||||
hasSpecialChar,
|
||||
isValid: score >= 4 && minLength
|
||||
};
|
||||
};
|
||||
|
||||
const passwordStrength = validatePassword(signupData.password);
|
||||
|
||||
// Email validation
|
||||
const isValidEmail = (email: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!loginData.email || !loginData.password) {
|
||||
toast.error('Por favor, preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEmail(loginData.email)) {
|
||||
toast.error('Por favor, insira um email válido');
|
||||
return;
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
setIsLoading(true);
|
||||
const { error } = await signIn(loginData.email, loginData.password);
|
||||
const { error } = await signIn();
|
||||
|
||||
if (error) {
|
||||
if (error.message.includes('Invalid login credentials')) {
|
||||
toast.error('Email ou senha incorretos');
|
||||
} else {
|
||||
toast.error('Erro ao fazer login: ' + error.message);
|
||||
}
|
||||
} else {
|
||||
toast.success('Login realizado com sucesso!');
|
||||
navigate('/');
|
||||
toast.error('Erro ao redirecionar para o login: ' + (error as Error).message);
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
// No else block needed as it redirects to external OIDC provider
|
||||
};
|
||||
|
||||
const handleSignup = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!signupData.email || !signupData.password || !signupData.confirmPassword) {
|
||||
toast.error('Por favor, preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEmail(signupData.email)) {
|
||||
toast.error('Por favor, insira um email válido');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordStrength.isValid) {
|
||||
toast.error('A senha deve ter pelo menos 8 caracteres e incluir maiúsculas, minúsculas, números e símbolos');
|
||||
return;
|
||||
}
|
||||
|
||||
if (signupData.password !== signupData.confirmPassword) {
|
||||
toast.error('As senhas não coincidem');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { error } = await signUp(signupData.email, signupData.password);
|
||||
|
||||
if (error) {
|
||||
if (error.message.includes('User already registered')) {
|
||||
toast.error('Este email já está cadastrado');
|
||||
} else {
|
||||
toast.error('Erro ao criar conta: ' + error.message);
|
||||
}
|
||||
} else {
|
||||
toast.success('Conta criada com sucesso! Verifique seu email.');
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const getPasswordStrengthColor = (score: number) => {
|
||||
if (score <= 2) return 'bg-red-500';
|
||||
if (score <= 3) return 'bg-yellow-500';
|
||||
return 'bg-green-500';
|
||||
};
|
||||
|
||||
const getPasswordStrengthText = (score: number) => {
|
||||
if (score <= 2) return 'Fraca';
|
||||
if (score <= 3) return 'Média';
|
||||
return 'Forte';
|
||||
};
|
||||
|
||||
console.log('📱 Renderizando página de login/cadastro');
|
||||
return (
|
||||
@@ -281,188 +165,14 @@ const Auth = () => {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="login" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="login" className="text-sm">Entrar</TabsTrigger>
|
||||
<TabsTrigger value="signup" className="text-sm">Cadastrar</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="login" className="space-y-4">
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-email" className="text-sm font-medium">
|
||||
Email
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder="seu@email.com"
|
||||
value={loginData.email}
|
||||
onChange={(e) => setLoginData({ ...loginData, email: e.target.value })}
|
||||
className="pl-10 h-12"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-password" className="text-sm font-medium">
|
||||
Senha
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="login-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Sua senha"
|
||||
value={loginData.password}
|
||||
onChange={(e) => setLoginData({ ...loginData, password: e.target.value })}
|
||||
className="pl-10 pr-10 h-12"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium text-sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Entrando...' : 'Entrar'}
|
||||
</Button>
|
||||
|
||||
{/* Botão Esqueci a Senha */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowForgotPassword(true)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Esqueci a senha
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="signup" className="space-y-4">
|
||||
<form onSubmit={handleSignup} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="signup-email" className="text-sm font-medium">
|
||||
Email
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="signup-email"
|
||||
type="email"
|
||||
placeholder="seu@email.com"
|
||||
value={signupData.email}
|
||||
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
|
||||
className="pl-10 h-12"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="signup-password" className="text-sm font-medium">
|
||||
Senha
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="signup-password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
value={signupData.password}
|
||||
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })}
|
||||
className="pl-10 pr-10 h-12"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Password Strength Indicator */}
|
||||
{signupData.password && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 bg-muted rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${getPasswordStrengthColor(passwordStrength.score)}`}
|
||||
style={{ width: `${(passwordStrength.score / 5) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{getPasswordStrengthText(passwordStrength.score)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs space-y-1">
|
||||
<div className={`flex items-center gap-1 ${passwordStrength.minLength ? 'text-green-600' : 'text-red-600'}`}>
|
||||
<span className="w-2 h-2 rounded-full bg-current"></span>
|
||||
Mínimo 8 caracteres
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 ${passwordStrength.hasUpperCase ? 'text-green-600' : 'text-red-600'}`}>
|
||||
<span className="w-2 h-2 rounded-full bg-current"></span>
|
||||
Letra maiúscula
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 ${passwordStrength.hasNumbers ? 'text-green-600' : 'text-red-600'}`}>
|
||||
<span className="w-2 h-2 rounded-full bg-current"></span>
|
||||
Número
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 ${passwordStrength.hasSpecialChar ? 'text-green-600' : 'text-red-600'}`}>
|
||||
<span className="w-2 h-2 rounded-full bg-current"></span>
|
||||
Símbolo especial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="signup-confirm" className="text-sm font-medium">
|
||||
Confirmar Senha
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="signup-confirm"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Confirme sua senha"
|
||||
value={signupData.confirmPassword}
|
||||
onChange={(e) => setSignupData({ ...signupData, confirmPassword: e.target.value })}
|
||||
className="pl-10 h-12"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium text-sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Criando conta...' : 'Criar conta'}
|
||||
</Button>
|
||||
</form>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<CardContent className="pt-6">
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-full h-14 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium text-lg rounded-xl transition-all hover:scale-[1.02] shadow-md"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Redirecionando...' : 'Acessar Sistema Seguramente'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -471,12 +181,6 @@ const Auth = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Esqueci a Senha */}
|
||||
<ForgotPasswordModal
|
||||
isOpen={showForgotPassword}
|
||||
onClose={() => setShowForgotPassword(false)}
|
||||
/>
|
||||
</BeamsBackground>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user