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
Artur Oliveira 2025-12-16 10:40:08 -03:00
parent 41059bdfc3
commit 116261e7ff
7 changed files with 88 additions and 9 deletions

View File

@ -72,6 +72,7 @@ public class JwtService {
}
private Claims extractAllClaims(String token) {
// parser() is deprecated in docs, but current JJWT version exposes only this entrypoint
return Jwts.parser()
.verifyWith(getSignInKey())
.build()

View File

@ -17,6 +17,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
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
@EnableWebSecurity
@ -30,6 +35,7 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
@ -59,4 +65,21 @@ public class SecurityConfig {
public PasswordEncoder passwordEncoder() {
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;
}
}

View File

@ -0,0 +1,3 @@
# Example env file for Vite
# Copy to .env and set your backend URL
VITE_BACKEND_URL=http://localhost:8080

View File

@ -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;

View File

@ -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 { Login } from './pages/Login';
@ -7,6 +7,7 @@ function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<Login />} />
</Routes>
</BrowserRouter>

View File

@ -1,9 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import { Toaster } from 'react-hot-toast';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<>
<App />
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
</>
</StrictMode>,
);

View File

@ -1,7 +1,10 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { Layout } from "../components/Layout";
import type { LoginProps } from "../types/Login";
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 = () => {
const [form, setForm] = useState<LoginProps>({
@ -9,6 +12,9 @@ export const Login = () => {
password: ""
});
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>) => {
setForm({
@ -21,15 +27,32 @@ export const Login = () => {
setShowPassword(!showPassword);
};
useEffect(() => {
console.log(form);
}, [form]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
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 (
<Layout className={Styles.layout}>
<img src="/logo.webp " alt="Logo" className={Styles.logo} />
<div className={Styles.card}>
<form>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email" className={Styles.label}>Email:</label>
<input
@ -61,7 +84,10 @@ export const Login = () => {
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
</button>
</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>
</div>
</Layout>
@ -77,4 +103,4 @@ const Styles = {
input: "bg-gray-50 border border-cardBorder text-text text-md rounded-lg outline-none block w-full p-2.5",
iconButton: "absolute right-3 top-1/2 text-text p-1 focus:outline-none",
button: "w-full bg-accent p-2 rounded-md mt-4 text-white font-bold text-lg hover:bg-hover transition duration-150",
};
};