feat(auth): migração final do UI frontend para Logto OIDC via GoTrue

This commit is contained in:
2026-08-17 19:14:49 +00:00
parent 0487439c2a
commit a5968c76f1
3 changed files with 60 additions and 346 deletions
+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
};