diff --git a/pom.xml b/pom.xml index c5f4dd4..40078d1 100644 --- a/pom.xml +++ b/pom.xml @@ -185,8 +185,10 @@ dotenv-java 2.3.1 - - + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/src/main/java/edu/eci/cvds/prometeo/config/JwtRequestFilter.java b/src/main/java/edu/eci/cvds/prometeo/config/JwtRequestFilter.java new file mode 100644 index 0000000..1f8e10d --- /dev/null +++ b/src/main/java/edu/eci/cvds/prometeo/config/JwtRequestFilter.java @@ -0,0 +1,75 @@ +package edu.eci.cvds.prometeo.config; + +import edu.eci.cvds.prometeo.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + public JwtRequestFilter(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) + throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + + System.out.println("🔍 Checking Authorization header..."); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + System.out.println("✅ Authorization header found: " + authHeader); + + try { + var claims = jwtUtil.extractClaims(authHeader); + + String username = claims.get("userName", String.class); + String role = claims.get("role", String.class).toUpperCase(); + String name = claims.get("name", String.class); + String idCard = claims.get("id", String.class); + + // Log extracted claims + System.out.println("✅ JWT Claims extracted:"); + System.out.println("username = " + username); + System.out.println("role = " + role); + System.out.println("name = " + name); + System.out.println("idCard = " + idCard); + + // Save attributes in the request + request.setAttribute("username", username); + request.setAttribute("role", role); + request.setAttribute("name", name); + request.setAttribute("institutionalId", idCard); + + // Set authentication in SecurityContext + var authorities = List.of(new SimpleGrantedAuthority("ROLE_" + role)); + var auth = new UsernamePasswordAuthenticationToken(username, null, authorities); + SecurityContextHolder.getContext().setAuthentication(auth); + + } catch (Exception e) { + System.out.println("❌ Error extracting JWT claims: " + e.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); + return; + } + } else { + System.out.println("⚠️ Authorization header is missing or does not start with 'Bearer '"); + } + + chain.doFilter(request, response); + System.out.println("🔍 Post-filter role: " + request.getAttribute("role")); + } +} \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/config/LoggingFilter.java b/src/main/java/edu/eci/cvds/prometeo/config/LoggingFilter.java new file mode 100644 index 0000000..84fb9e6 --- /dev/null +++ b/src/main/java/edu/eci/cvds/prometeo/config/LoggingFilter.java @@ -0,0 +1,24 @@ +package edu.eci.cvds.prometeo.config; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class LoggingFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + System.out.println("🔍 Request URI: " + request.getRequestURI()); + System.out.println("🔍 Method: " + request.getMethod()); + System.out.println("🔍 All Attributes: "); + request.getAttributeNames().asIterator().forEachRemaining(attr -> + System.out.println(attr + " = " + request.getAttribute(attr)) + ); + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/config/SecurityConfig.java b/src/main/java/edu/eci/cvds/prometeo/config/SecurityConfig.java index ba9a8af..b28a9b2 100644 --- a/src/main/java/edu/eci/cvds/prometeo/config/SecurityConfig.java +++ b/src/main/java/edu/eci/cvds/prometeo/config/SecurityConfig.java @@ -1,26 +1,40 @@ package edu.eci.cvds.prometeo.config; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +@Slf4j @Configuration @EnableWebSecurity public class SecurityConfig { - + + private final JwtRequestFilter jwtRequestFilter; + + public SecurityConfig(JwtRequestFilter jwtRequestFilter) { + this.jwtRequestFilter = jwtRequestFilter; + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // Configuración que desactiva toda la seguridad http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/**").permitAll() - ) - .formLogin(form -> form.disable()) - .httpBasic(basic -> basic.disable()); - + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth + + .requestMatchers("/api/users/create").authenticated() + + .requestMatchers("/api/users/trainer/**").hasRole("TRAINER") + + .anyRequest().hasAnyRole("TRAINER", "STUDENT", "ADMIN") + ) + .formLogin(form -> form.disable()) + .httpBasic(basic -> basic.disable()) + .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new LoggingFilter(), JwtRequestFilter.class); return http.build(); } } \ No newline at end of file diff --git a/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java b/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java index 0edda18..20d581e 100644 --- a/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java +++ b/src/main/java/edu/eci/cvds/prometeo/controller/UserController.java @@ -6,6 +6,7 @@ import edu.eci.cvds.prometeo.repository.RoutineRepository; import edu.eci.cvds.prometeo.service.*; import edu.eci.cvds.prometeo.dto.*; +import jakarta.servlet.http.HttpServletRequest; import io.swagger.v3.oas.annotations.enums.ParameterIn; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; @@ -116,6 +117,53 @@ public ResponseEntity> getUsersByRole( @Parameter(description = "Role name") @PathVariable String role) { return ResponseEntity.ok(userService.getUsersByRole(role)); } + + @PostMapping("/create") + @Operation(summary = "Create user from JWT", description = "Creates a new user using data from the JWT token") + @ApiResponse(responseCode = "201", description = "User created successfully", + content = @Content(schema = @Schema(implementation = User.class))) + @ApiResponse(responseCode = "400", description = "Invalid input data") + @ApiResponse(responseCode = "409", description = "User already exists") + public ResponseEntity createUser(HttpServletRequest request) { + try { + String institutionalId = (String) request.getAttribute("institutionalId"); + String username = (String) request.getAttribute("username"); + String name = (String) request.getAttribute("name"); + String role = (String) request.getAttribute("role"); + + // Log extracted attributes + System.out.println("🔍 Extracted attributes:"); + System.out.println("institutionalId = " + institutionalId); + System.out.println("username = " + username); + System.out.println("name = " + name); + System.out.println("role = " + role); + + // Validate attributes + if (institutionalId == null || name == null || role == null) { + System.out.println("❌ Missing required attributes"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); + } + + // Check if user already exists + if (userService.userExistsByInstitutionalId(institutionalId)) { + System.out.println("⚠️ User with institutionalId " + institutionalId + " already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(null); + } + + // Create user + UserDTO userDTO = new UserDTO(); + userDTO.setInstitutionalId(institutionalId); + userDTO.setName(name); + userDTO.setRole(role); + + User createdUser = userService.createUser(userDTO); + return new ResponseEntity<>(createdUser, HttpStatus.CREATED); + + } catch (Exception e) { + System.out.println("❌ Error creating user: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); + } +} @PutMapping("/{id}") @Operation(summary = "Update user", description = "Updates a user's basic information") @@ -127,15 +175,6 @@ public ResponseEntity updateUser( return ResponseEntity.ok(userService.updateUser(id, userDTO)); } - @PostMapping - @Operation(summary = "Create user", description = "Creates a new user in the system") - @ApiResponse(responseCode = "201", description = "User created successfully", content = @Content(schema = @Schema(implementation = User.class))) - public ResponseEntity createUser( - @Parameter(description = "User data") @RequestBody UserDTO userDTO) { - User createdUser = userService.createUser(userDTO); - return new ResponseEntity<>(createdUser, HttpStatus.CREATED); - } - @DeleteMapping("/{id}") @Operation(summary = "Delete user", description = "Deletes a user from the system") @ApiResponse(responseCode = "200", description = "User deleted successfully") @@ -835,6 +874,8 @@ public ResponseEntity> getTrainerSessions( @Parameter(description = "Trainer ID") @PathVariable UUID trainerId) { List sessions = gymSessionService.getSessionsByTrainer(trainerId); + System.out.println("🔍 Accessing /trainer/{trainerId}/sessions endpoint"); + System.out.println("🔍 Trainer ID: " + trainerId); return ResponseEntity.ok(sessions); } diff --git a/src/main/java/edu/eci/cvds/prometeo/repository/UserRepository.java b/src/main/java/edu/eci/cvds/prometeo/repository/UserRepository.java index c7fd992..c9c78e7 100644 --- a/src/main/java/edu/eci/cvds/prometeo/repository/UserRepository.java +++ b/src/main/java/edu/eci/cvds/prometeo/repository/UserRepository.java @@ -19,6 +19,8 @@ public interface UserRepository extends JpaRepository { List findByRole(String role); Optional findByInstitutionalId(String institutionalId); + boolean existsByInstitutionalId(String institutionalId); + /** * Finds all users assigned to a specific trainer. * diff --git a/src/main/java/edu/eci/cvds/prometeo/service/UserService.java b/src/main/java/edu/eci/cvds/prometeo/service/UserService.java index 2e8f85a..81b5e96 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/UserService.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/UserService.java @@ -38,7 +38,7 @@ public interface UserService { // ------------- Operaciones básicas de usuario ------------- - + /** * Obtener usuario por ID * @param id ID del usuario @@ -46,6 +46,13 @@ public interface UserService { */ User getUserById(String institutionalId); + boolean userExistsByInstitutionalId(String institutionalId); + /** + * Obtener usuario por ID de tarjeta de identificación + * @param idCard ID de tarjeta de identificación del usuario + * @return Entidad de usuario + */ + /** * Obtener usuario por ID institucional * @param institutionalId ID institucional del usuario diff --git a/src/main/java/edu/eci/cvds/prometeo/service/impl/UserServiceImpl.java b/src/main/java/edu/eci/cvds/prometeo/service/impl/UserServiceImpl.java index b181955..1dcb147 100644 --- a/src/main/java/edu/eci/cvds/prometeo/service/impl/UserServiceImpl.java +++ b/src/main/java/edu/eci/cvds/prometeo/service/impl/UserServiceImpl.java @@ -85,6 +85,10 @@ public User getUserById(String institutionalId) { .orElseThrow(() -> new RuntimeException("User not found with id: " + institutionalId)); } + @Override + public boolean userExistsByInstitutionalId(String institutionalId){ + return userRepository.existsByInstitutionalId(institutionalId); + } @Override public User getUserByInstitutionalId(String institutionalId) { return userRepository.findByInstitutionalId(institutionalId) diff --git a/src/main/java/edu/eci/cvds/prometeo/util/JwtUtil.java b/src/main/java/edu/eci/cvds/prometeo/util/JwtUtil.java new file mode 100644 index 0000000..427f7ad --- /dev/null +++ b/src/main/java/edu/eci/cvds/prometeo/util/JwtUtil.java @@ -0,0 +1,18 @@ +package edu.eci.cvds.prometeo.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + + private final String SECRET_KEY = "supersecretpassword1234567891011121314"; // Debe ser la misma que usa el microservicio de usuarios + + public Claims extractClaims(String token) { + return Jwts.parser() + .setSigningKey(SECRET_KEY.getBytes()) + .parseClaimsJws(token.replace("Bearer ", "")) + .getBody(); + } +} diff --git a/src/test/java/edu/eci/cvds/prometeo/controller/UserControllerTest.java b/src/test/java/edu/eci/cvds/prometeo/controller/UserControllerTest.java index 8868723..b414b6b 100644 --- a/src/test/java/edu/eci/cvds/prometeo/controller/UserControllerTest.java +++ b/src/test/java/edu/eci/cvds/prometeo/controller/UserControllerTest.java @@ -6,7 +6,7 @@ import edu.eci.cvds.prometeo.repository.RoutineExerciseRepository; import edu.eci.cvds.prometeo.repository.RoutineRepository; import edu.eci.cvds.prometeo.service.*; - +import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -117,17 +117,52 @@ public void testGetUsersByRole() { assertEquals(users, response.getBody()); verify(userService).getUsersByRole("STUDENT"); } - @Test - void testCreateUser() { - // Use the exact object instead of any() - when(userService.createUser(userDTO)).thenReturn(testUser); - - ResponseEntity response = userController.createUser(userDTO); - + + @Test + void createUserSuccessfully() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute("institutionalId")).thenReturn("A12345"); + when(request.getAttribute("username")).thenReturn("testuser"); + when(request.getAttribute("name")).thenReturn("Test User"); + when(request.getAttribute("role")).thenReturn("USER"); + + UserDTO userDTO = new UserDTO(); + userDTO.setInstitutionalId("A12345"); + userDTO.setName("Test User"); + userDTO.setRole("USER"); + + User createdUser = new User(); + createdUser.setInstitutionalId("A12345"); + createdUser.setName("Test User"); + createdUser.setRole("USER"); + + when(userService.userExistsByInstitutionalId("A12345")).thenReturn(false); + when(userService.createUser(userDTO)).thenReturn(createdUser); + + ResponseEntity response = userController.createUser(request); + assertEquals(HttpStatus.CREATED, response.getStatusCode()); - assertEquals(testUser, response.getBody()); + assertEquals(createdUser, response.getBody()); + verify(userService).userExistsByInstitutionalId("A12345"); verify(userService).createUser(userDTO); } + + @Test + void createUserFailsWhenAttributesAreMissing() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getAttribute("institutionalId")).thenReturn(null); + when(request.getAttribute("name")).thenReturn("Test User"); + when(request.getAttribute("role")).thenReturn("USER"); + + ResponseEntity response = userController.createUser(request); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + verify(userService, never()).userExistsByInstitutionalId(anyString()); + verify(userService, never()).createUser(any(UserDTO.class)); + } + + + @Test void testUpdateUser() { // Use exact matches instead of any()