feat(auth): implementar funcionalidade de login de usuário
- Configura base URL da API através de variável de ambiente (.env.example). - Cria instância do Axios para comunicação com a API e gerenciamento de token de autenticação. - Adiciona lógica de submissão do formulário de login, incluindo chamadas à API. - Gerencia estados de carregamento e exibe mensagens de erro/sucesso. - Integra react-hot-toast para notificações de sistema. - Redireciona a rota raiz (/) para a página de login.master
parent
41059bdfc3
commit
116261e7ff
|
|
@ -72,6 +72,7 @@ public class JwtService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Claims extractAllClaims(String token) {
|
private Claims extractAllClaims(String token) {
|
||||||
|
// parser() is deprecated in docs, but current JJWT version exposes only this entrypoint
|
||||||
return Jwts.parser()
|
return Jwts.parser()
|
||||||
.verifyWith(getSignInKey())
|
.verifyWith(getSignInKey())
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
|
@ -30,6 +35,7 @@ public class SecurityConfig {
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
|
.cors(Customizer.withDefaults())
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
|
@ -59,4 +65,21 @@ public class SecurityConfig {
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
configuration.setAllowedOrigins(List.of(
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173"
|
||||||
|
));
|
||||||
|
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
configuration.setAllowedHeaders(List.of("*"));
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
configuration.setExposedHeaders(List.of("Set-Cookie"));
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Example env file for Vite
|
||||||
|
# Copy to .env and set your backend URL
|
||||||
|
VITE_BACKEND_URL=http://localhost:8080
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const baseURL = (import.meta.env.VITE_BACKEND_URL as string) || 'http://localhost:8080';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setAuthToken = (token?: string) => {
|
||||||
|
if (token) {
|
||||||
|
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
|
} else {
|
||||||
|
delete api.defaults.headers.common['Authorization'];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { Login } from './pages/Login';
|
import { Login } from './pages/Login';
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<>
|
||||||
<App />
|
<App />
|
||||||
|
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
||||||
|
</>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Layout } from "../components/Layout";
|
import { Layout } from "../components/Layout";
|
||||||
import type { LoginProps } from "../types/Login";
|
import type { LoginProps } from "../types/Login";
|
||||||
import { Eye, EyeOff } from "lucide-icons-react";
|
import { Eye, EyeOff } from "lucide-icons-react";
|
||||||
|
import api from "../Api";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const [form, setForm] = useState<LoginProps>({
|
const [form, setForm] = useState<LoginProps>({
|
||||||
|
|
@ -9,6 +12,9 @@ export const Login = () => {
|
||||||
password: ""
|
password: ""
|
||||||
});
|
});
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setForm({
|
setForm({
|
||||||
|
|
@ -21,15 +27,32 @@ export const Login = () => {
|
||||||
setShowPassword(!showPassword);
|
setShowPassword(!showPassword);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
console.log(form);
|
e.preventDefault();
|
||||||
}, [form]);
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post("/api/auth/login", {
|
||||||
|
username: form.email,
|
||||||
|
password: form.password
|
||||||
|
});
|
||||||
|
navigate("/");
|
||||||
|
toast.success("Login realizado com sucesso!");
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err?.response?.data?.message || "Falha ao autenticar. Verifique as credenciais.";
|
||||||
|
setError(message);
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className={Styles.layout}>
|
<Layout className={Styles.layout}>
|
||||||
<img src="/logo.webp " alt="Logo" className={Styles.logo} />
|
<img src="/logo.webp " alt="Logo" className={Styles.logo} />
|
||||||
<div className={Styles.card}>
|
<div className={Styles.card}>
|
||||||
<form>
|
<form onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className={Styles.label}>Email:</label>
|
<label htmlFor="email" className={Styles.label}>Email:</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -61,7 +84,10 @@ export const Login = () => {
|
||||||
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" className={Styles.button}>Login</button>
|
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
||||||
|
<button type="submit" disabled={loading} className={Styles.button}>
|
||||||
|
{loading ? "Autenticando..." : "Login"}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue