From cc55f8b2b698e53cf4a90b76c2873cb8c3054676 Mon Sep 17 00:00:00 2001 From: Arturo Gomez Date: Tue, 9 Jun 2026 17:38:32 -0600 Subject: [PATCH 1/3] fix(api): corregir error 403 al recargar rutas del SPA en el frontend --- .../MyTodoList/config/SecurityConfig.java | 12 +++--------- .../MyTodoList/config/SpaController.java | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SpaController.java diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SecurityConfig.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SecurityConfig.java index 5f2ff10b1..975422239 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SecurityConfig.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SecurityConfig.java @@ -34,16 +34,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api/v1/auth/**", "/swagger-ui/**", "/swagger-ui.html", - "/v3/api-docs/**", - "/", - "/index.html", - "/static/**", - "/*.js", - "/*.css", - "/*.ico", - "/*.json" + "/v3/api-docs/**" ).permitAll() - .anyRequest().authenticated() + .requestMatchers("/api/**").authenticated() + .anyRequest().permitAll() ) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SpaController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SpaController.java new file mode 100644 index 000000000..2e2fab604 --- /dev/null +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/SpaController.java @@ -0,0 +1,17 @@ +package com.springboot.MyTodoList.config; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class SpaController { + + // Forward all non-file, non-API routes to index.html so React Router handles them + @RequestMapping(value = { + "/{path:[^\\.]*}", + "/{path:[^\\.]*}/**" + }) + public String forward() { + return "forward:/index.html"; + } +} From 372a5791638835bed56d8f10642b5a689c24f0db Mon Sep 17 00:00:00 2001 From: Arturo Gomez Date: Tue, 9 Jun 2026 17:41:59 -0600 Subject: [PATCH 2/3] fix(api): retornar 401 cuando el token JWT ha expirado para redirigir al login --- .../MyTodoList/config/JwtAuthFilter.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/JwtAuthFilter.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/JwtAuthFilter.java index fe2a517db..b574be6c1 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/JwtAuthFilter.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/config/JwtAuthFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -27,14 +28,22 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractToken(request); - if (token != null && jwtUtil.validateToken(token)) { - String userId = jwtUtil.extractUserId(token); - UserDetails userDetails = userDetailsService.loadUserByUsername(userId); - if (userDetails.isEnabled()) { - UsernamePasswordAuthenticationToken auth = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(auth); + if (token != null) { + if (jwtUtil.validateToken(token)) { + String userId = jwtUtil.extractUserId(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(userId); + if (userDetails.isEnabled()) { + UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } else { + // Token present but expired/invalid → 401 so the frontend redirects to login + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write("{\"error\":\"Token expired or invalid\"}"); + return; } } filterChain.doFilter(request, response); From 32ea2a7ec8ec84a91e44ce7a41c92a0c62334e35 Mon Sep 17 00:00:00 2001 From: Arturo Gomez Date: Tue, 9 Jun 2026 17:48:37 -0600 Subject: [PATCH 3/3] =?UTF-8?q?fix(frontend):=20mostrar=20sprint=20activo?= =?UTF-8?q?=20o=20m=C3=A1s=20reciente=20al=20entrar=20al=20tablero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/frontend/src/pages/TasksPage.jsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/MtdrSpring/backend/src/main/frontend/src/pages/TasksPage.jsx b/MtdrSpring/backend/src/main/frontend/src/pages/TasksPage.jsx index 6baecbc56..34da9f17f 100644 --- a/MtdrSpring/backend/src/main/frontend/src/pages/TasksPage.jsx +++ b/MtdrSpring/backend/src/main/frontend/src/pages/TasksPage.jsx @@ -44,7 +44,7 @@ export default function TasksPage() { const [showForm, setShowForm] = useState(false); const [editTask, setEditTask] = useState(null); const [saving, setSaving] = useState(false); - const [filters, setFilters] = useState({ status: '', sprint: '', priority: '' }); + const [filters, setFilters] = useState({ status: '', sprint: undefined, priority: '' }); const [showClosed, setShowClosed] = useState(false); const [donePrompt, setDonePrompt] = useState(null); const [actualHoursInput, setActualHoursInput] = useState(''); @@ -55,6 +55,7 @@ export default function TasksPage() { const isManager = userRole === 'MANAGER'; const load = useCallback(() => { + if (filters.sprint === undefined) return; setLoading(true); taskService.list(project.id, { ...filters, size: 500 }) .then((data) => setTasks(data.content || [])) @@ -68,10 +69,12 @@ export default function TasksPage() { sprintService.list(project.id).then((data) => { setSprints(data); const active = data.find((s) => s.status === 'ACTIVE'); - const fallback = data.filter((s) => s.status === 'PLANNING').sort((a, b) => a.startDate.localeCompare(b.startDate))[0]; - const defaultSprint = active || fallback; - if (defaultSprint) setFilters((f) => ({ ...f, sprint: String(defaultSprint.id) })); - }).catch(() => {}); + const mostRecent = [...data].sort((a, b) => b.startDate.localeCompare(a.startDate))[0]; + const defaultSprint = active || mostRecent; + setFilters((f) => ({ ...f, sprint: defaultSprint ? String(defaultSprint.id) : '' })); + }).catch(() => { + setFilters((f) => ({ ...f, sprint: '' })); + }); }, [project.id]); const handleCreate = async (form) => {