feat(auth): proteger dashboard e melhorar UX

- Remove HTTP Basic e trata 401 redirecionando para /login
- Adiciona ProtectedRoute garantindo acesso ao dashboard apenas autenticado
- Refina modais e menu com bulk upload e tipagens exportadas
master
Artur Oliveira 2025-12-16 14:26:18 -03:00
parent b6ba3b8593
commit a69aca5dc8
8 changed files with 47 additions and 9 deletions

View File

@ -12,6 +12,7 @@ import org.springframework.security.config.annotation.authentication.configurati
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@ -44,7 +45,7 @@ public class SecurityConfig {
) )
.authenticationProvider(authenticationProvider()) .authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults()); .httpBasic(AbstractHttpConfigurer::disable);
return http.build(); return http.build();
} }

View File

@ -116,4 +116,18 @@ api.interceptors.request.use(async (config) => {
return config; return config;
}); });
api.interceptors.response.use(
(response) => response,
(error) => {
if (error?.response?.status === 401) {
setAuthToken(undefined);
const currentPath = window.location.pathname;
if (currentPath !== "/login") {
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
export default api; export default api;

View File

@ -2,13 +2,18 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css'; import './App.css';
import { Login } from './pages/Login'; import { Login } from './pages/Login';
import { Dashboard } from './pages/Dashboard'; import { Dashboard } from './pages/Dashboard';
import { ProtectedRoute } from './routes/ProtectedRoute';
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@ -134,8 +134,8 @@ const Stat = ({
); );
const Styles = { const Styles = {
modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 px-4", modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 backdrop-blur-sm px-4 !mt-0",
modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl", modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl transform transition-all duration-200 animate-fade-up",
modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder", modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder",
modalTitle: "text-lg font-semibold text-text", modalTitle: "text-lg font-semibold text-text",
closeButton: "text-2xl leading-none text-text-secondary hover:text-text", closeButton: "text-2xl leading-none text-text-secondary hover:text-text",

View File

@ -1,4 +1,4 @@
interface HeaderActionsProps { export interface HeaderActionsProps {
isMenuOpen: boolean; isMenuOpen: boolean;
onToggleMenu: () => void; onToggleMenu: () => void;
onAddServer: () => void; onAddServer: () => void;

View File

@ -73,8 +73,8 @@ export const ProfileModal = ({
}; };
const Styles = { const Styles = {
modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 px-4", modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 backdrop-blur-sm px-4 !mt-0",
modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl", modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl transform transition-all duration-200 animate-fade-up",
modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder", modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder",
modalTitle: "text-lg font-semibold text-text", modalTitle: "text-lg font-semibold text-text",
closeButton: "text-2xl leading-none text-text-secondary hover:text-text", closeButton: "text-2xl leading-none text-text-secondary hover:text-text",

View File

@ -100,8 +100,8 @@ export const ServerModal = ({
}; };
const Styles = { const Styles = {
modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 px-4", modalOverlay: "fixed inset-0 z-40 flex items-center justify-center bg-black/40 backdrop-blur-sm px-4 !mt-0",
modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl", modal: "w-full max-w-2xl rounded-2xl border border-cardBorder bg-card p-6 shadow-xl transform transition-all duration-200 animate-fade-up",
modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder", modalHeader: "flex items-start justify-between gap-4 pb-4 border-b border-cardBorder",
modalTitle: "text-lg font-semibold text-text", modalTitle: "text-lg font-semibold text-text",
closeButton: "text-2xl leading-none text-text-secondary hover:text-text", closeButton: "text-2xl leading-none text-text-secondary hover:text-text",

View File

@ -0,0 +1,18 @@
import type { ReactNode } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { getAccessToken } from "../Api";
interface ProtectedRouteProps {
children: ReactNode;
}
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const token = getAccessToken();
const location = useLocation();
if (!token) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
};