From 52fbab5d36669eecf97cbb78c74fcd639a7d2c80 Mon Sep 17 00:00:00 2001 From: Samuel Gunda <samuel.gunda@kosickaakademia.sk> Date: Wed, 8 May 2024 13:32:57 +0200 Subject: [PATCH 1/4] tryout of implementing jwt token auth --- pom.xml | 81 ++++--- .../bootcamp/controllers/AuthController.java | 206 ++++++++++-------- .../controllers/StudentController.java | 71 +++--- .../bootcamp/controllers/TestController.java | 35 +++ .../bootcamp/controllers/UserController.java | 64 ------ .../gunda/bootcamp/entities/AuthCheck.java | 13 -- .../bootcamp/entities/LogoutRequest.java | 16 -- .../bootcamp/entities/UpdatePassword.java | 15 -- .../bootcamp/entities/UpdateStudent.java | 4 - .../gunda/bootcamp/entities/UpdateUser.java | 14 -- .../kasv/gunda/bootcamp/entities/User.java | 38 ---- .../exceptions/BlacklistedJwtException.java | 8 + .../exceptions/JwtTokenException.java | 8 + .../com/kasv/gunda/bootcamp/models/ERole.java | 7 + .../com/kasv/gunda/bootcamp/models/Role.java | 39 ++++ .../{entities => models}/Student.java | 2 +- .../com/kasv/gunda/bootcamp/models/User.java | 53 +++++ .../request}/LoginRequest.java | 13 +- .../payload/request/SignupRequest.java | 29 +++ .../payload/response/MessageResponse.java | 17 ++ .../payload/response/UserResponse.java | 45 ++++ .../bootcamp/repositories/RoleRepository.java | 13 ++ .../repositories/StudentRepository.java | 2 +- .../bootcamp/repositories/UserRepository.java | 17 +- .../bootcamp/security/WebSecurityConfig.java | 79 +++++++ .../security/jwt/AuthEntryPointJwt.java | 25 +++ .../security/jwt/AuthTokenFilter.java | 58 +++++ .../gunda/bootcamp/security/jwt/JwtUtils.java | 86 ++++++++ .../security/services/UserDetailsImpl.java | 102 +++++++++ .../services/UserDetailsServiceImpl.java | 26 +++ .../gunda/bootcamp/services/AuthService.java | 162 -------------- .../bootcamp/services/StudentService.java | 168 +++++++------- .../gunda/bootcamp/services/UserService.java | 83 ------- .../utilities/RandomPasswordGenerator.java | 13 -- .../bootcamp/utilities/TokenFunctions.java | 57 ----- .../bootcamp/utilities/TokenGenerator.java | 24 -- .../utilities/UserTimeoutFunctions.java | 59 ----- src/main/resources/application.properties | 8 +- .../bootcamp/BootcampApplicationTests.java | 5 - .../repositories/StudentRepositoryTests.java | 96 -------- .../repositories/UserRepositoryTests.java | 89 -------- .../bootcamp/services/AuthServicesTests.java | 5 - 42 files changed, 939 insertions(+), 1016 deletions(-) create mode 100644 src/main/java/com/kasv/gunda/bootcamp/controllers/TestController.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/controllers/UserController.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/entities/AuthCheck.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/entities/LogoutRequest.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/entities/UpdatePassword.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/entities/UpdateStudent.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/entities/UpdateUser.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/entities/User.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/models/ERole.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/models/Role.java rename src/main/java/com/kasv/gunda/bootcamp/{entities => models}/Student.java (92%) create mode 100644 src/main/java/com/kasv/gunda/bootcamp/models/User.java rename src/main/java/com/kasv/gunda/bootcamp/{entities => payload/request}/LoginRequest.java (50%) create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/request/SignupRequest.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/repositories/RoleRepository.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsServiceImpl.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/services/AuthService.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/services/UserService.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/utilities/RandomPasswordGenerator.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/utilities/TokenFunctions.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/utilities/TokenGenerator.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/utilities/UserTimeoutFunctions.java delete mode 100644 src/test/java/com/kasv/gunda/bootcamp/repositories/StudentRepositoryTests.java delete mode 100644 src/test/java/com/kasv/gunda/bootcamp/repositories/UserRepositoryTests.java delete mode 100644 src/test/java/com/kasv/gunda/bootcamp/services/AuthServicesTests.java diff --git a/pom.xml b/pom.xml index 63b55d5..2cb45c4 100644 --- a/pom.xml +++ b/pom.xml @@ -17,21 +17,47 @@ <java.version>21</java.version> </properties> <dependencies> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-data-rest</artifactId> + <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-web</artifactId> + <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-devtools</artifactId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.11.5</version> + </dependency> + + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.11.5</version> + <scope>runtime</scope> + </dependency> + + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.11.5</version> <scope>runtime</scope> - <optional>true</optional> </dependency> + <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> @@ -40,57 +66,40 @@ <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> - <optional>true</optional> + <scope>provided</scope> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.springframework.security</groupId> - <artifactId>spring-security-test</artifactId> - <scope>test</scope> + <artifactId>spring-boot-starter-mail</artifactId> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-data-jpa</artifactId> - </dependency> - <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-web</artifactId> - <version>6.1.4</version> - </dependency> - <dependency> - <groupId>com.google.code.gson</groupId> - <artifactId>gson</artifactId> - <version>2.10.1</version> + <artifactId>spring-boot-devtools</artifactId> + <scope>runtime</scope> + <optional>true</optional> </dependency> + + <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.1.0</version> </dependency> + <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-spring-web</artifactId> <version>3.0.0</version> </dependency> + <dependency> - <groupId>org.hibernate.orm</groupId> - <artifactId>hibernate-core</artifactId> - <version>6.4.4.Final</version> - </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-mail</artifactId> - </dependency> - <dependency> - <groupId>com.h2database</groupId> - <artifactId>h2</artifactId> - <version>2.1.214</version> - <scope>test</scope> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>2.10.1</version> </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java index 68a6a84..3b142f7 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java @@ -1,119 +1,151 @@ package com.kasv.gunda.bootcamp.controllers; -import com.google.gson.Gson; -import com.kasv.gunda.bootcamp.entities.AuthCheck; -import com.kasv.gunda.bootcamp.entities.LoginRequest; -import com.kasv.gunda.bootcamp.entities.LogoutRequest; -import com.kasv.gunda.bootcamp.entities.User; -import com.kasv.gunda.bootcamp.services.AuthService; +import com.kasv.gunda.bootcamp.models.ERole; +import com.kasv.gunda.bootcamp.models.Role; +import com.kasv.gunda.bootcamp.models.User; +import com.kasv.gunda.bootcamp.payload.request.LoginRequest; +import com.kasv.gunda.bootcamp.payload.request.SignupRequest; +import com.kasv.gunda.bootcamp.payload.response.MessageResponse; +import com.kasv.gunda.bootcamp.payload.response.UserResponse; +import com.kasv.gunda.bootcamp.repositories.RoleRepository; +import com.kasv.gunda.bootcamp.repositories.UserRepository; +import com.kasv.gunda.bootcamp.security.jwt.JwtUtils; +import com.kasv.gunda.bootcamp.security.services.UserDetailsImpl; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +@CrossOrigin(origins = "http://localhost:3000", maxAge = 3600) @RestController -@RequestMapping("/api") -@CrossOrigin +@RequestMapping("/api/auth") public class AuthController { + @Autowired + AuthenticationManager authenticationManager; - private final AuthService authService; - public AuthController(AuthService authService) { this.authService = authService; } + @Autowired + UserRepository userRepository; - @PostMapping("/login") - public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) { + @Autowired + RoleRepository roleRepository; - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - System.out.println(loginRequest.getUsername() + " " + loginRequest.getPassword()); - if (loginRequest.getUsername() == null || - loginRequest.getPassword() == null || - loginRequest.getUsername().isEmpty() || - loginRequest.getPassword().isEmpty()) { + @Autowired + PasswordEncoder encoder; - jsonResponse.put("error", "Invalid request. Please provide valid input data."); + @Autowired + JwtUtils jwtUtils; - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } + @PostMapping("/signin") + @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") + public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { - return this.authService.login(loginRequest); - } + Authentication authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); - @PostMapping("/isAuthenticated") - public ResponseEntity<String> isAuthenticated(@RequestBody AuthCheck authCheck) { + SecurityContextHolder.getContext().setAuthentication(authentication); - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();; - if (authCheck.getUsername() == null || - authCheck.getUsername().isEmpty() || - authCheck.getToken() == null || - authCheck.getToken().isEmpty()) { + ResponseCookie cookie = jwtUtils.generateJwtCookie(userDetails); - jsonResponse.put("error", "Invalid request. Please provide valid input data."); + List<String> roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toList()); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - return this.authService.isAuthenticated(authCheck); + System.out.println("User logged"); + return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) + .body(new UserResponse(userDetails.getId(), + userDetails.getUsername(), + userDetails.getEmail(), + roles)); } - @PostMapping("/logout") - public ResponseEntity<String> logout(@RequestBody LogoutRequest logoutRequest) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (logoutRequest.getUsername() == null || - logoutRequest.getToken() == null || - logoutRequest.getUsername().isEmpty() || - logoutRequest.getToken().isEmpty()) { - - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); + @PostMapping("/signup") + public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) { + if (userRepository.existsByUsername(signUpRequest.getUsername())) { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!")); } - return this.authService.logout(logoutRequest); - } - - @PostMapping("/register") - public ResponseEntity<String> register(@RequestBody User user) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (user.getUsername() == null || - user.getUsername().isEmpty() || - user.getPassword() == null || - user.getPassword().isEmpty() || - user.getEmail() == null || - user.getEmail().isEmpty()) { - - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); + if (userRepository.existsByEmail(signUpRequest.getEmail())) { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); } - return this.authService.register(user); - } - - @PostMapping("/forgotPassword") - public ResponseEntity<String> forgotPassword(@RequestBody User user) { + User user = new User(signUpRequest.getUsername(), + signUpRequest.getEmail(), + encoder.encode(signUpRequest.getPassword())); + + Set<String> strRoles = signUpRequest.getRole(); + Set<Role> roles = new HashSet<>(); + + if (strRoles == null) { + Role userRole = roleRepository.findByName(ERole.ROLE_USER) + .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); + roles.add(userRole); + } else { + strRoles.forEach(role -> { + switch (role) { + case "admin": + Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN) + .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); + roles.add(adminRole); + + break; + case "mod": + Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR) + .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); + roles.add(modRole); + + break; + default: + Role userRole = roleRepository.findByName(ERole.ROLE_USER) + .orElseThrow(() -> new RuntimeException("Error: Role is not found.")); + roles.add(userRole); + } + }); + } - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); + user.setRoles(roles); + userRepository.save(user); - if (user.getUsername() == null || - user.getUsername().isEmpty() || - user.getEmail() == null || - user.getEmail().isEmpty()) { + return ResponseEntity.ok(new MessageResponse("User registered successfully!")); + } - jsonResponse.put("error", "Invalid request. Please provide valid input data."); + @PostMapping("/signout") + @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") + public ResponseEntity<?> logoutUser() { + ResponseCookie cookie = jwtUtils.getCleanJwtCookie(); + return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) + .body(new MessageResponse("You've been signed out!")); + } - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); + @PostMapping("/validate") + @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") + public ResponseEntity<?> validateUser(HttpServletRequest request) { + String token = jwtUtils.getJwtFromCookies(request); + + if (token != null) { + if (jwtUtils.validateJwtToken(token)) { + System.out.println("Valid token!"); + return ResponseEntity.ok(new MessageResponse("Valid token!")); + } else { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Invalid token!")); + } + } else { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Missing token in cookie!")); } - - return this.authService.forgotPassword(user); } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java index 0d1e1f3..b37e6d1 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java @@ -1,13 +1,16 @@ package com.kasv.gunda.bootcamp.controllers; import com.google.gson.Gson; -import com.kasv.gunda.bootcamp.entities.AuthCheck; -import com.kasv.gunda.bootcamp.entities.LogoutRequest; -import com.kasv.gunda.bootcamp.entities.Student; +import com.kasv.gunda.bootcamp.models.Student; +import com.kasv.gunda.bootcamp.payload.response.MessageResponse; import com.kasv.gunda.bootcamp.services.StudentService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import com.kasv.gunda.bootcamp.security.jwt.JwtUtils; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -17,37 +20,32 @@ import java.util.Map; public class StudentController { private final StudentService studentService; + @Autowired + JwtUtils jwtUtils; + public StudentController(StudentService studentService) { this.studentService = studentService; } - @PostMapping("/students") - public String getAllStudents(@RequestBody(required = false) AuthCheck authCheck) { - - return studentService.getAllStudents(authCheck); - + /* Returns all students, + while admin can view all students, + students can only view their first name + and first letter of their last name */ + @GetMapping("/students") + @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") + public String getAllStudents(HttpServletRequest request) { +// studentService.getAllStudents(request); + System.out.println(request.getHeader("Authorization")); + System.out.println(Arrays.toString(request.getCookies())); + return "All students"; } @PostMapping("/student/{id}") - public ResponseEntity<String> getStudentById(@PathVariable Long id, @RequestBody AuthCheck authCheck) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (authCheck.getUsername() == null || - authCheck.getToken() == null || - authCheck.getUsername().isEmpty() || - authCheck.getToken().isEmpty() || - id == null) { - - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - return studentService.getStudentById(id, authCheck); + public ResponseEntity<String> getStudentById(@PathVariable Long id) { + return null; } + /* Registers a new student */ @PostMapping("/student/register") public ResponseEntity<String> registerStudent(@RequestBody Student student) { @@ -64,20 +62,19 @@ public class StudentController { return studentService.registerStudent(student); } + /* Updates student data */ @PutMapping("/student/update/{id}") - public ResponseEntity<String> updateLastName(@PathVariable Long id, @RequestBody LogoutRequest details) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if(details.getLastName() == null || details.getLastName().isEmpty() || id == null - || details.getToken() == null || details.getToken().isEmpty()) { - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - return studentService.updateLastName(id, details); + public ResponseEntity<String> updateStudent( @PathVariable Long id, @RequestBody Student student) { + return null; } + /* Returns the count of all students */ + @GetMapping("/students/count") + public ResponseEntity<String> getStudentsCount() { + Gson gson = new Gson(); + Map<String, Integer> jsonResponse = new HashMap<>(); + jsonResponse.put("count", studentService.getStudentsCount()); + return ResponseEntity.ok(gson.toJson(jsonResponse)); + } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/TestController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/TestController.java new file mode 100644 index 0000000..cad1353 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/TestController.java @@ -0,0 +1,35 @@ +package com.kasv.gunda.bootcamp.controllers; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api/test") +public class TestController { + @GetMapping("/all") + public String allAccess() { + return "Public Content."; + } + + @GetMapping("/user") + @PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')") + public String userAccess() { + return "User Content."; + } + + @GetMapping("/mod") + @PreAuthorize("hasRole('MODERATOR')") + public String moderatorAccess() { + return "Moderator Board."; + } + + @GetMapping("/admin") + @PreAuthorize("hasRole('ADMIN')") + public String adminAccess() { + return "Admin Board."; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/UserController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/UserController.java deleted file mode 100644 index 1bbb0ad..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/UserController.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.kasv.gunda.bootcamp.controllers; - -import com.google.gson.Gson; -import com.kasv.gunda.bootcamp.entities.UpdatePassword; - -import com.kasv.gunda.bootcamp.repositories.UserRepository; -import com.kasv.gunda.bootcamp.services.UserService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.HashMap; -import java.util.Map; - - -@CrossOrigin -@RestController -//@RequestMapping("/api/users") -public class UserController { - private final UserRepository userRepository; - private final UserService userService; - - public UserController(UserRepository userRepository, UserService userService) { - this.userRepository = userRepository; - this.userService = userService; - } - - @GetMapping("/exist/{username}") - public ResponseEntity<String> byUsername(@PathVariable String username) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (userRepository.existsByUsername(username)) { - jsonResponse.put("message", "User with username " + username + " exists"); - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - } else { - jsonResponse.put("message", "User with username " + username + " does not exist"); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - } - } - - @PutMapping("/admin/password") - public ResponseEntity<String> changePassword(@RequestBody UpdatePassword request) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (request.getToken() == null || request.getToken().isEmpty() - || request.getNewPassword() == null || request.getNewPassword().isEmpty() - || request.getOldPassword() == null || request.getOldPassword().isEmpty() - || request.getUsername() == null || request.getUsername().isEmpty()) { - - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - return userService.changePassword(request); - } - - @GetMapping("/info") - public ResponseEntity<String> getAllUsersAndStudents() { - return userService.getAllUsersAndStudents(); - } -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/AuthCheck.java b/src/main/java/com/kasv/gunda/bootcamp/entities/AuthCheck.java deleted file mode 100644 index 74864ba..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/AuthCheck.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kasv.gunda.bootcamp.entities; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class AuthCheck { - - private String token; - private String username; - -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/LogoutRequest.java b/src/main/java/com/kasv/gunda/bootcamp/entities/LogoutRequest.java deleted file mode 100644 index 85d17ec..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/LogoutRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.kasv.gunda.bootcamp.entities; - -import lombok.Getter; -import lombok.Setter; - -import java.util.Collection; - -@Getter -@Setter -public class LogoutRequest { - - private String token; - private String username; - private String lastName; - -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/UpdatePassword.java b/src/main/java/com/kasv/gunda/bootcamp/entities/UpdatePassword.java deleted file mode 100644 index 8e083f6..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/UpdatePassword.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.kasv.gunda.bootcamp.entities; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UpdatePassword { - - private String token; - private String username; - private String newPassword; - private String oldPassword; - -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/UpdateStudent.java b/src/main/java/com/kasv/gunda/bootcamp/entities/UpdateStudent.java deleted file mode 100644 index 4f44fb0..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/UpdateStudent.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.kasv.gunda.bootcamp.entities; - -public class UpdateStudent { -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/UpdateUser.java b/src/main/java/com/kasv/gunda/bootcamp/entities/UpdateUser.java deleted file mode 100644 index 55878b9..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/UpdateUser.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.kasv.gunda.bootcamp.entities; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UpdateUser { - - String token; - String email; - String username; - -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/User.java b/src/main/java/com/kasv/gunda/bootcamp/entities/User.java deleted file mode 100644 index bf74486..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/User.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.kasv.gunda.bootcamp.entities; - -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Setter -@Getter -@Table(name = "users") -public class User { - - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Id - @Column(name = "id") - private int id; - - @Column(name = "username") - private String username; - - @Column(name = "password") - private String password; - - @Column(name = "email") - private String email; - - // ToString method - @Override - public String toString() { - return "User{" + - "id=" + id + - ", username='" + username + '\'' + - ", email='" + email + '\'' + - '}'; - } -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java b/src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java new file mode 100644 index 0000000..274bae5 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java @@ -0,0 +1,8 @@ +package com.kasv.gunda.bootcamp.exceptions; + +public class BlacklistedJwtException extends RuntimeException { + + public BlacklistedJwtException(String message) { + super(message); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java b/src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java new file mode 100644 index 0000000..57dc924 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java @@ -0,0 +1,8 @@ +package com.kasv.gunda.bootcamp.exceptions; + +public class JwtTokenException extends RuntimeException { + + public JwtTokenException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/ERole.java b/src/main/java/com/kasv/gunda/bootcamp/models/ERole.java new file mode 100644 index 0000000..9373df4 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/models/ERole.java @@ -0,0 +1,7 @@ +package com.kasv.gunda.bootcamp.models; + +public enum ERole { + ROLE_USER, + ROLE_MODERATOR, + ROLE_ADMIN +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/Role.java b/src/main/java/com/kasv/gunda/bootcamp/models/Role.java new file mode 100644 index 0000000..76a449b --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/models/Role.java @@ -0,0 +1,39 @@ +package com.kasv.gunda.bootcamp.models; + +import jakarta.persistence.*; + +@Entity +@Table(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private ERole name; + + public Role() { + + } + + public Role(ERole name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public ERole getName() { + return name; + } + + public void setName(ERole name) { + this.name = name; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/Student.java b/src/main/java/com/kasv/gunda/bootcamp/models/Student.java similarity index 92% rename from src/main/java/com/kasv/gunda/bootcamp/entities/Student.java rename to src/main/java/com/kasv/gunda/bootcamp/models/Student.java index 27d1ab7..dd6c6cb 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/Student.java +++ b/src/main/java/com/kasv/gunda/bootcamp/models/Student.java @@ -1,4 +1,4 @@ -package com.kasv.gunda.bootcamp.entities; +package com.kasv.gunda.bootcamp.models; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/User.java b/src/main/java/com/kasv/gunda/bootcamp/models/User.java new file mode 100644 index 0000000..06eb89e --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/models/User.java @@ -0,0 +1,53 @@ +package com.kasv.gunda.bootcamp.models; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Getter +@Setter +@Table(name = "users", + uniqueConstraints = { + @UniqueConstraint(columnNames = "username"), + @UniqueConstraint(columnNames = "email") + }) +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + @Size(max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + @NotBlank + @Size(max = 120) + private String password; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set<Role> roles = new HashSet<>(); + + public User() { + } + + public User(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/entities/LoginRequest.java b/src/main/java/com/kasv/gunda/bootcamp/payload/request/LoginRequest.java similarity index 50% rename from src/main/java/com/kasv/gunda/bootcamp/entities/LoginRequest.java rename to src/main/java/com/kasv/gunda/bootcamp/payload/request/LoginRequest.java index 69c9f53..e948529 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/entities/LoginRequest.java +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/request/LoginRequest.java @@ -1,15 +1,16 @@ -package com.kasv.gunda.bootcamp.entities; +package com.kasv.gunda.bootcamp.payload.request; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.NotBlank; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -@Getter @Setter -@AllArgsConstructor -@NoArgsConstructor +@Getter public class LoginRequest { + @NotBlank private String username; + + @NotBlank private String password; + } diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/request/SignupRequest.java b/src/main/java/com/kasv/gunda/bootcamp/payload/request/SignupRequest.java new file mode 100644 index 0000000..0e8c2e6 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/request/SignupRequest.java @@ -0,0 +1,29 @@ +package com.kasv.gunda.bootcamp.payload.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + +@Setter +@Getter +public class SignupRequest { + @NotBlank + @Size(min = 3, max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + private Set<String> role; + + @NotBlank + @Size(min = 6, max = 40) + private String password; + +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java b/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java new file mode 100644 index 0000000..f6ff002 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java @@ -0,0 +1,17 @@ +package com.kasv.gunda.bootcamp.payload.response; + +public class MessageResponse { + private String message; + + public MessageResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java b/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java new file mode 100644 index 0000000..4132972 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java @@ -0,0 +1,45 @@ +package com.kasv.gunda.bootcamp.payload.response; + +import java.util.List; + +public class UserResponse { + private Long id; + private String username; + private String email; + private List<String> roles; + + public UserResponse(Long id, String username, String email, List<String> roles) { + this.id = id; + this.username = username; + this.email = email; + this.roles = roles; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public List<String> getRoles() { + return roles; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/RoleRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/RoleRepository.java new file mode 100644 index 0000000..0ff898b --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/RoleRepository.java @@ -0,0 +1,13 @@ +package com.kasv.gunda.bootcamp.repositories; + +import com.kasv.gunda.bootcamp.models.ERole; +import com.kasv.gunda.bootcamp.models.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository<Role, Long> { + Optional<Role> findByName(ERole name); +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRepository.java index af26fe4..ca1af92 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRepository.java +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRepository.java @@ -1,7 +1,7 @@ package com.kasv.gunda.bootcamp.repositories; -import com.kasv.gunda.bootcamp.entities.Student; +import com.kasv.gunda.bootcamp.models.Student; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java index 978ac6d..3033044 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java @@ -1,20 +1,19 @@ package com.kasv.gunda.bootcamp.repositories; -import com.kasv.gunda.bootcamp.entities.User; +import com.kasv.gunda.bootcamp.models.ERole; +import com.kasv.gunda.bootcamp.models.User; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -@Repository -public interface UserRepository extends JpaRepository<User, String> { +import java.util.Optional; - User findByUsername(String username); +@Repository +public interface UserRepository extends JpaRepository<User, Long> { + Optional<User> findByUsername(String username); Boolean existsByUsername(String username); - @Query("SELECT u.id FROM User u WHERE u.username = :username") - Integer findIdByUsername(@Param("username") String username); + Boolean existsByEmail(String email); - boolean existsByEmail(String email); + ERole findRoleByUsername(String username); } diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java b/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java new file mode 100644 index 0000000..5c83c89 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java @@ -0,0 +1,79 @@ +package com.kasv.gunda.bootcamp.security; + +import com.kasv.gunda.bootcamp.security.jwt.AuthEntryPointJwt; +import com.kasv.gunda.bootcamp.security.jwt.AuthTokenFilter; +import com.kasv.gunda.bootcamp.security.services.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +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.Arrays; + +@Configuration +@EnableMethodSecurity(prePostEnabled = true) +public class WebSecurityConfig { + @Autowired + UserDetailsServiceImpl userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> + auth.requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/test/all").permitAll() + .requestMatchers("/api/students/count").permitAll() + .anyRequest().authenticated() + ); + + http.authenticationProvider(authenticationProvider()); + + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java new file mode 100644 index 0000000..85cbcc5 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java @@ -0,0 +1,25 @@ +package com.kasv.gunda.bootcamp.security.jwt; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException, ServletException { + logger.error("Unauthorized error: {}", authException.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java new file mode 100644 index 0000000..d165a18 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java @@ -0,0 +1,58 @@ +package com.kasv.gunda.bootcamp.security.jwt; + +import com.kasv.gunda.bootcamp.security.services.UserDetailsServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthTokenFilter extends OncePerRequestFilter { + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + String username = jwtUtils.getUserNameFromJwtToken(jwt); + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, + null, + userDetails.getAuthorities()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e); + } + + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String jwt = jwtUtils.getJwtFromCookies(request); + return jwt; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java new file mode 100644 index 0000000..3f63397 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java @@ -0,0 +1,86 @@ +package com.kasv.gunda.bootcamp.security.jwt; + +import com.kasv.gunda.bootcamp.security.services.UserDetailsImpl; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; +import org.springframework.web.util.WebUtils; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtils { + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); + + @Value("${gunda.bootcamp.app.jwtSecret}") + private String jwtSecret; + + @Value("${gunda.bootcamp.app.jwtExpirationMs}") + private int jwtExpirationMs; + + @Value("${gunda.bootcamp.app.jwtCookieName}") + private String jwtCookie; + + public String getJwtFromCookies(HttpServletRequest request) { + Cookie cookie = WebUtils.getCookie(request, jwtCookie); + if (cookie != null) { + return cookie.getValue(); + } else { + return null; + } + } + + public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) { + String jwt = generateTokenFromUsername(userPrincipal.getUsername()); + ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).sameSite("None").secure(true).build(); + return cookie; + } + + public ResponseCookie getCleanJwtCookie() { + ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build(); + return cookie; + } + + public String getUserNameFromJwtToken(String token) { + return Jwts.parserBuilder().setSigningKey(key()).build() + .parseClaimsJws(token).getBody().getSubject(); + } + + private Key key() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + + return false; + } + + public String generateTokenFromUsername(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(key(), SignatureAlgorithm.HS256) + .compact(); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java b/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java new file mode 100644 index 0000000..5efcfd3 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java @@ -0,0 +1,102 @@ +package com.kasv.gunda.bootcamp.security.services; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kasv.gunda.bootcamp.models.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class UserDetailsImpl implements UserDetails { + private static final long serialVersionUID = 1L; + + private Long id; + + private String username; + + private String email; + + @JsonIgnore + private String password; + + private Collection<? extends GrantedAuthority> authorities; + + public UserDetailsImpl(Long id, String username, String email, String password, + Collection<? extends GrantedAuthority> authorities) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(User user) { + List<GrantedAuthority> authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.getName().name())) + .collect(Collectors.toList()); + + return new UserDetailsImpl( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + authorities); + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return authorities; + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + UserDetailsImpl user = (UserDetailsImpl) o; + return Objects.equals(id, user.id); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsServiceImpl.java b/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsServiceImpl.java new file mode 100644 index 0000000..0968a03 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsServiceImpl.java @@ -0,0 +1,26 @@ +package com.kasv.gunda.bootcamp.security.services; + +import com.kasv.gunda.bootcamp.models.User; +import com.kasv.gunda.bootcamp.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + @Autowired + UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); + + return UserDetailsImpl.build(user); + } + +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/services/AuthService.java b/src/main/java/com/kasv/gunda/bootcamp/services/AuthService.java deleted file mode 100644 index 3b3d8f6..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/services/AuthService.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.kasv.gunda.bootcamp.services; - -import com.google.gson.Gson; -import com.kasv.gunda.bootcamp.entities.AuthCheck; -import com.kasv.gunda.bootcamp.entities.LoginRequest; -import com.kasv.gunda.bootcamp.entities.LogoutRequest; -import com.kasv.gunda.bootcamp.entities.User; -import com.kasv.gunda.bootcamp.utilities.*; -import com.kasv.gunda.bootcamp.repositories.UserRepository; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; - -@Service -public class AuthService { - - private final UserRepository userRepository; - private final EmailFunctions emailFunctions; - private TokenFunctions tokenFunctions = TokenFunctions.getInstance(); - private UserTimeoutFunctions userTimeoutFunctions = UserTimeoutFunctions.getInstance(); - - public AuthService(UserRepository userRepository, EmailFunctions emailFunctions) { - this.userRepository = userRepository; - this.emailFunctions = emailFunctions; - } - - public ResponseEntity<String> login(LoginRequest loginRequest) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - User userFromDb = userRepository.findByUsername(loginRequest.getUsername()); - - if (userFromDb != null) { - if (userFromDb.getPassword().equals(loginRequest.getPassword()) && userTimeoutFunctions.isUserOnTimeout(userFromDb.getId()) == false){ - - String token = TokenGenerator.generateToken(); - tokenFunctions.storeToken((long) userFromDb.getId(), token); - - jsonResponse.put("token", token); - jsonResponse.put("username", userFromDb.getUsername()); - - userTimeoutFunctions.resetBadPasswordsCount(userFromDb.getId()); - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - - } else { - if (userTimeoutFunctions.isUserOnTimeout(userFromDb.getId())) { - jsonResponse.put("error", "Your account is locked. Please try again in 5 seconds."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - if (userTimeoutFunctions.getBadPasswordsCount(userFromDb.getId()) < 3) { - userTimeoutFunctions.incrementCount((long) userFromDb.getId()); - } - if (userTimeoutFunctions.getBadPasswordsCount(userFromDb.getId()) >= 3 ) { - userTimeoutFunctions.setUserTimeout( userFromDb.getId()); - jsonResponse.put("error", "Your account has been locked. Please try again in 5 seconds."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - jsonResponse.put( - "error", - "Invalid credentials. Please provide valid credentials. " + - userTimeoutFunctions.getBadPasswordsCount(userFromDb.getId()) + - "/3 failed attempts." - ); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - } else { - jsonResponse.put("error", "User with username " + loginRequest.getUsername() + " does not exist. Please provide valid credentials."); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - } - } - - public ResponseEntity<String> logout(LogoutRequest logoutRequest) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - long userId = (long) userRepository.findIdByUsername(logoutRequest.getUsername()); - - if (tokenFunctions.isTokenExists(userId)) { - if (tokenFunctions.isTokenValid(logoutRequest.getToken())) { - tokenFunctions.removeToken(userId); - jsonResponse.put("message", "User logged out successfully"); - return ResponseEntity.status(200).body("{}"); - } else { - jsonResponse.put("error", "Invalid token. Please provide valid token."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - } else { - jsonResponse.put("error", "User " + logoutRequest.getUsername() + " is not logged in."); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - } - } - - public ResponseEntity<String> forgotPassword(User user) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - User userFromDb = userRepository.findByUsername(user.getUsername()); - - if (userFromDb != null) { - if (userFromDb.getEmail().equals(user.getEmail())) { - String newPassword = RandomPasswordGenerator.generateCode(); - userFromDb.setPassword(newPassword); - userRepository.save(userFromDb); - emailFunctions.sendNewPassword(userFromDb.getEmail(), newPassword); - jsonResponse.put("message", "New password has been sent to your email."); - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - } else { - jsonResponse.put("error", "Invalid email. Please provide valid email."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - } else { - jsonResponse.put("error", "User with username " + user.getUsername() + " does not exist."); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - } - } - - public ResponseEntity<String> isAuthenticated(AuthCheck authCheck) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - long userId = (long) userRepository.findIdByUsername(authCheck.getUsername()); - - if (tokenFunctions.isTokenExists(userId)) { - if (tokenFunctions.isTokenValid(authCheck.getToken())) { - jsonResponse.put("message", "User is authenticated."); - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - } else { - jsonResponse.put("error", "Invalid token. Please provide valid token."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - } else { - jsonResponse.put("error", "User is not authenticated."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - } - - public ResponseEntity<String> register(User user) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (userRepository.existsByUsername(user.getUsername())) { - jsonResponse.put("error", "User with username " + user.getUsername() + " already exists."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - if (userRepository.existsByEmail(user.getEmail())) { - jsonResponse.put("error", "User with email " + user.getEmail() + " already exists."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - userRepository.save(user); - jsonResponse.put("message", "User registered successfully."); - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - } -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java b/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java index 5cbd01a..79769c3 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java +++ b/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java @@ -1,15 +1,13 @@ package com.kasv.gunda.bootcamp.services; import com.google.gson.Gson; -import com.kasv.gunda.bootcamp.entities.AuthCheck; -import com.kasv.gunda.bootcamp.entities.LogoutRequest; -import com.kasv.gunda.bootcamp.entities.Student; +import com.kasv.gunda.bootcamp.models.Student; import com.kasv.gunda.bootcamp.repositories.StudentRepository; -import com.kasv.gunda.bootcamp.repositories.UserRepository; -import com.kasv.gunda.bootcamp.utilities.TokenFunctions; +import com.kasv.gunda.bootcamp.security.jwt.JwtUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import org.springframework.web.bind.annotation.CrossOrigin; import java.util.HashMap; import java.util.List; @@ -18,65 +16,73 @@ import java.util.Map; @Service public class StudentService { - private TokenFunctions tokenFunctions = TokenFunctions.getInstance(); + private final JwtUtils jwtUtils; private final StudentRepository studentRepository; - private final UserRepository userRepository; - public StudentService(StudentRepository studentRepository, UserRepository userRepository) { - this.studentRepository = studentRepository; - this.userRepository = userRepository; - } - - public String getAllStudents(AuthCheck authCheck) { - - Gson gson = new Gson(); - - List<Student> students = studentRepository.findAll(); - - - if (authCheck.getToken() != null || !authCheck.getToken().isEmpty() || - authCheck.getUsername() != null || !authCheck.getUsername().isEmpty()) { - - if (!tokenFunctions.isTokenValid(authCheck.getToken())) { - for (Student student : students) { - student.setDob(null); - student.setLastName(student.getLastName().substring(0, 1)); - } - } - } else { - for (Student student : students) { - student.setDob(null); - student.setLastName(student.getLastName().substring(0, 1)); - } - } - return gson.toJson(students); + public StudentService(JwtUtils jwtUtils, StudentRepository studentRepository) { + this.jwtUtils = jwtUtils; + this.studentRepository = studentRepository; } - public ResponseEntity<String> getStudentById(Long id, AuthCheck authCheck) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - long userId = (long) userRepository.findIdByUsername(authCheck.getUsername()); - - if (tokenFunctions.isTokenExists(userId)) { - if (!tokenFunctions.isTokenValid(authCheck.getToken())) { - jsonResponse.put("error", "Invalid token. Please provide valid token."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - } else { - jsonResponse.put("error", "Invalid token. Please provide valid token."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - - if (!studentRepository.existsById(id)) { - jsonResponse.put("error", "Student with id " + id + " not found."); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - } - - return ResponseEntity.status(200).body(gson.toJson(studentRepository.findById(id))); +// public String getAllStudents(HttpServletRequest request) { +// +// String token = jwtUtils.getJwtFromCookies(request); +// String username = jwtUtils.getUserNameFromJwtToken(token); +// +// +// +// +// List<Student> students = studentRepository.findAll(); +// +// +// if () { +// +// if (!tokenFunctions.isTokenValid(authCheck.getToken())) { +// +// for (Student student : students) { +// student.setDob(null); +// student.setLastName(student.getLastName().substring(0, 1)); +// } +// +// } +// } else { +// for (Student student : students) { +// student.setDob(null); +// student.setLastName(student.getLastName().substring(0, 1)); +// } +// } +// return gson.toJson(students); +// } + +// public ResponseEntity<String> getStudentById(Long id, AuthCheck authCheck) { +// +// Gson gson = new Gson(); +// Map<String, String> jsonResponse = new HashMap<>(); +// +// long userId = (long) userRepository.findIdByUsername(authCheck.getUsername()); +// +// if (tokenFunctions.isTokenExists(userId)) { +// if (!tokenFunctions.isTokenValid(authCheck.getToken())) { +// jsonResponse.put("error", "Invalid token. Please provide valid token."); +// return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); +// } +// } else { +// jsonResponse.put("error", "Invalid token. Please provide valid token."); +// return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); +// } +// +// if (!studentRepository.existsById(id)) { +// jsonResponse.put("error", "Student with id " + id + " not found."); +// return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); +// } +// +// return ResponseEntity.status(200).body(gson.toJson(studentRepository.findById(id))); +// } + + public int getStudentsCount() { + return (int) studentRepository.count(); } public ResponseEntity<String> registerStudent(Student student) { @@ -107,26 +113,26 @@ public class StudentService { } - public ResponseEntity<String> updateLastName(Long id, LogoutRequest details) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (!tokenFunctions.isTokenValid(details.getToken())) { - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - if (!studentRepository.existsById(id)) { - jsonResponse.put("error", "Student with id " + id + " not found."); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - } - - Student student = studentRepository.findById(id); - student.setLastName(details.getLastName()); - - studentRepository.save(student); - - return ResponseEntity.status(200).body("{}"); - } +// public ResponseEntity<String> updateLastName(Long id, LogoutRequest details) { +// +// Gson gson = new Gson(); +// Map<String, String> jsonResponse = new HashMap<>(); +// +// if (!tokenFunctions.isTokenValid(details.getToken())) { +// jsonResponse.put("error", "Invalid request. Please provide valid input data."); +// return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); +// } +// +// if (!studentRepository.existsById(id)) { +// jsonResponse.put("error", "Student with id " + id + " not found."); +// return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); +// } +// +// Student student = studentRepository.findById(id); +// student.setLastName(details.getLastName()); +// +// studentRepository.save(student); +// +// return ResponseEntity.status(200).body("{}"); +// } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/services/UserService.java b/src/main/java/com/kasv/gunda/bootcamp/services/UserService.java deleted file mode 100644 index cef5310..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/services/UserService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.kasv.gunda.bootcamp.services; - -import com.google.gson.Gson; -import com.kasv.gunda.bootcamp.controllers.AuthController; -import com.kasv.gunda.bootcamp.entities.LogoutRequest; -import com.kasv.gunda.bootcamp.entities.UpdatePassword; -import com.kasv.gunda.bootcamp.entities.User; -import com.kasv.gunda.bootcamp.repositories.StudentRepository; -import com.kasv.gunda.bootcamp.repositories.UserRepository; -import com.kasv.gunda.bootcamp.utilities.TokenFunctions; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; - -@Service -public class UserService { - private final UserRepository userRepository; - - private final StudentRepository studentRepository; - private final AuthController authController; - - private TokenFunctions tokenFunctions = TokenFunctions.getInstance(); - - public UserService(UserRepository userRepository, StudentRepository studentRepository, AuthController authController) { - this.userRepository = userRepository; - this.studentRepository = studentRepository; - this.authController = authController; - } - - public ResponseEntity<String> changePassword(UpdatePassword request) { - - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (!userRepository.existsByUsername(request.getUsername())) { - jsonResponse.put("error", "User with username " + request.getUsername() + " does not exist."); - return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); - - } - - if (!tokenFunctions.isTokenValid(request.getToken())) { - jsonResponse.put("error", "Invalid token. Please provide a valid token."); - return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); - } - - User userFromDb = userRepository.findByUsername(request.getUsername()); - - if (userFromDb.getPassword().equals(request.getOldPassword())) { - if (request.getOldPassword().equals(request.getNewPassword())) { - jsonResponse.put("error", "New password cannot be the same as the old password."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - - } - userFromDb.setPassword(request.getNewPassword()); - userRepository.save(userFromDb); - - LogoutRequest logoutRequest = new LogoutRequest(); - logoutRequest.setToken(request.getToken()); - logoutRequest.setUsername(request.getUsername()); - authController.logout(logoutRequest); - - } else { - jsonResponse.put("error", "Invalid old password. Please provide a valid old password."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - jsonResponse.put("message", "Password changed successfully."); - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - - } - - public ResponseEntity<String> getAllUsersAndStudents() { - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - jsonResponse.put("users", gson.toJson(userRepository.findAll().size())); - jsonResponse.put("students", gson.toJson(studentRepository.findAll().size())); - - return ResponseEntity.status(200).body(gson.toJson(jsonResponse)); - } -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/utilities/RandomPasswordGenerator.java b/src/main/java/com/kasv/gunda/bootcamp/utilities/RandomPasswordGenerator.java deleted file mode 100644 index 3d2976e..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/utilities/RandomPasswordGenerator.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.kasv.gunda.bootcamp.utilities; - -public class RandomPasswordGenerator { - public static String generateCode() { - String randomChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - StringBuilder code = new StringBuilder(); - for (int i = 0; i < 10; i++) { - int randomIndex = (int) (randomChars.length() * Math.random()); - code.append(randomChars.charAt(randomIndex)); - } - return code.toString(); - } -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/utilities/TokenFunctions.java b/src/main/java/com/kasv/gunda/bootcamp/utilities/TokenFunctions.java deleted file mode 100644 index 4d17ac9..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/utilities/TokenFunctions.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.kasv.gunda.bootcamp.utilities; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class TokenFunctions { - - private final Map<Long, String> userTokens; - private static TokenFunctions instance; - - private TokenFunctions() { - userTokens = new ConcurrentHashMap<>(); - } - - public static synchronized TokenFunctions getInstance() { - if (instance == null) { - instance = new TokenFunctions(); - } - return instance; - } - - public void storeToken(Long userId, String token) { - userTokens.put(userId, token); - System.out.println(userTokens); - } - - public boolean isTokenValid(String token) { - return userTokens.containsValue(token); - } - - public void removeToken(Long userId) { - userTokens.remove(userId); - System.out.println(userTokens); - } - - public String getToken(Long userId) { - return userTokens.get(userId); - } - - public boolean isTokenExists(Long userId) { - return userTokens.containsKey(userId); - } - - public void clearTokens() { - userTokens.clear(); - } - - public long getUserId(String token) { - for (Map.Entry<Long, String> entry : userTokens.entrySet()) { - if (entry.getValue().equals(token)) { - return entry.getKey(); - } - } - return -1; - } - -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/utilities/TokenGenerator.java b/src/main/java/com/kasv/gunda/bootcamp/utilities/TokenGenerator.java deleted file mode 100644 index 3b34f1b..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/utilities/TokenGenerator.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.kasv.gunda.bootcamp.utilities; - -import java.security.SecureRandom; - - -public class TokenGenerator { - - private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - private static final int TOKEN_LENGTH = 40; - - public static String generateToken() { - SecureRandom random = new SecureRandom(); - StringBuilder token = new StringBuilder(TOKEN_LENGTH); - - for (int i = 0; i < TOKEN_LENGTH; i++) { - token.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length()))); - } - - return token.toString(); - } - - -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/utilities/UserTimeoutFunctions.java b/src/main/java/com/kasv/gunda/bootcamp/utilities/UserTimeoutFunctions.java deleted file mode 100644 index f6bf01a..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/utilities/UserTimeoutFunctions.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.kasv.gunda.bootcamp.utilities; - -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Component -public class UserTimeoutFunctions { - - private final Map<Long, Integer> badPasswordsCount; - private final Map<Long, Long> timeoutMap; - private static UserTimeoutFunctions instance; - - private UserTimeoutFunctions() { - badPasswordsCount = new ConcurrentHashMap<>(); - timeoutMap = new ConcurrentHashMap<>(); - } - - public static synchronized UserTimeoutFunctions getInstance() { - if (instance == null) { - instance = new UserTimeoutFunctions(); - } - return instance; - } - - public void incrementCount(Long userId) { - badPasswordsCount.put(userId, badPasswordsCount.getOrDefault(userId, 0) + 1); - System.out.println(badPasswordsCount); - } - - public int getBadPasswordsCount(long id) { - return Integer.parseInt(badPasswordsCount.getOrDefault(id, 0).toString()); - } - - public void resetBadPasswordsCount(long id) { - badPasswordsCount.put(id, 0); - System.out.println(badPasswordsCount); - } - - public void setUserTimeout(long id) { - long start = System.currentTimeMillis(); - long end = start + 5 * 1000; - timeoutMap.put(id, end); - resetBadPasswordsCount(id); - } - - public boolean isUserOnTimeout(long id) { - if (timeoutMap.containsKey(id)) { - long currentTime = System.currentTimeMillis(); - if (currentTime < timeoutMap.get(id)) { - return true; - } else { - timeoutMap.remove(id); - } - } - return false; - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a561132..c23118f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,7 +3,13 @@ spring.application.name=Bootcamp spring.datasource.url=jdbc:mariadb://localhost:3307/bootcamp spring.datasource.username=root spring.datasource.password= -spring.datasource.driver-class-name=org.mariadb.jdbc.Driver + +spring.jpa.hibernate.ddl-auto=update + +# App Properties +gunda.bootcamp.app.jwtCookieName= Authorization +gunda.bootcamp.app.jwtSecret= 15c8374b1a7b790f6888693fa0d2710cfa46bfa2b67b5a80c7aedf4109853c7c +gunda.bootcamp.app.jwtExpirationMs= 20000 spring.mail.host=smtp.gmail.com spring.mail.port=587 diff --git a/src/test/java/com/kasv/gunda/bootcamp/BootcampApplicationTests.java b/src/test/java/com/kasv/gunda/bootcamp/BootcampApplicationTests.java index f913b6e..ba3819c 100644 --- a/src/test/java/com/kasv/gunda/bootcamp/BootcampApplicationTests.java +++ b/src/test/java/com/kasv/gunda/bootcamp/BootcampApplicationTests.java @@ -1,9 +1,4 @@ package com.kasv.gunda.bootcamp; - -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest class BootcampApplicationTests { - } diff --git a/src/test/java/com/kasv/gunda/bootcamp/repositories/StudentRepositoryTests.java b/src/test/java/com/kasv/gunda/bootcamp/repositories/StudentRepositoryTests.java deleted file mode 100644 index 16b81f7..0000000 --- a/src/test/java/com/kasv/gunda/bootcamp/repositories/StudentRepositoryTests.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.kasv.gunda.bootcamp.repositories; - -import com.kasv.gunda.bootcamp.entities.Student; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -import java.util.Date; -import java.util.List; - -@DataJpaTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) -public class StudentRepositoryTests { - - @Autowired - private StudentRepository studentRepository; - - @Test - public void StudentRepository_SaveAll_ReturnSavedStudents() { - - // Arrange - Student student = Student.builder() - .firstName("BranÄi") - .lastName("KováÄ") - .dob(new Date()).build(); - - // Act - Student savedStudent = studentRepository.save(student); - - // Assert - Assertions.assertThat(savedStudent).isNotNull(); - Assertions.assertThat(savedStudent.getId()).isGreaterThan(0); - } - - @Test - public void findAll_ReturnsAllStudents() { - // Arrange - Student student1 = Student.builder().firstName("John").lastName("Doe").dob(new Date()).build(); - Student student2 = Student.builder().firstName("Jane").lastName("Doe").dob(new Date()).build(); - studentRepository.save(student1); - studentRepository.save(student2); - - // Act - List<Student> students = studentRepository.findAll(); - - // Assert - Assertions.assertThat(students).hasSize(2); - } - - @Test - public void existsById_ReturnsTrue_WhenStudentExists() { - // Arrange - Student student = Student.builder().firstName("John").lastName("Doe").dob(new Date()).build(); - Student savedStudent = studentRepository.save(student); - - // Act - boolean exists = studentRepository.existsById(savedStudent.getId()); - - // Assert - Assertions.assertThat(exists).isTrue(); - } - - @Test - public void existsById_ReturnsFalse_WhenStudentDoesNotExist() { - // Act - boolean exists = studentRepository.existsById(999L); // Assuming this ID does not exist - - // Assert - Assertions.assertThat(exists).isFalse(); - } - - @Test - public void findById_ReturnsStudent_WhenStudentExists() { - // Arrange - Student student = Student.builder().firstName("John").lastName("Doe").dob(new Date()).build(); - Student savedStudent = studentRepository.save(student); - - // Act - Student foundStudent = studentRepository.findById(savedStudent.getId()); - - // Assert - Assertions.assertThat(foundStudent).isEqualTo(savedStudent); - } - - @Test - public void findById_ReturnsNull_WhenStudentDoesNotExist() { - // Act - Student foundStudent = studentRepository.findById(999L); // Assuming this ID does not exist - - // Assert - Assertions.assertThat(foundStudent).isNull(); - } -} diff --git a/src/test/java/com/kasv/gunda/bootcamp/repositories/UserRepositoryTests.java b/src/test/java/com/kasv/gunda/bootcamp/repositories/UserRepositoryTests.java deleted file mode 100644 index e65497f..0000000 --- a/src/test/java/com/kasv/gunda/bootcamp/repositories/UserRepositoryTests.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.kasv.gunda.bootcamp.repositories; - -import com.kasv.gunda.bootcamp.entities.User; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -@DataJpaTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) -public class UserRepositoryTests { - - @Autowired - private UserRepository userRepository; - - @Test - public void UserRepository_SaveAll_ReturnSavedUsers() { - - // Arrange - User user = User.builder() - .username("Braňo") - .email("mail@test.com") - .password("password") - .build(); - - // Act - User savedUser = userRepository.save(user); - - // Assert - Assertions.assertThat(savedUser).isNotNull(); - Assertions.assertThat(savedUser.getId()).isGreaterThan(0); - } - - @Test - public void UserRepository_ExistsByUsername_ReturnsTrueIfUserExists() { - // Arrange - User user = User.builder() - .username("Braňo") - .email("mail@test.com") - .password("password") - .build(); - userRepository.save(user); - - // Act - Boolean exists = userRepository.existsByUsername(user.getUsername()); - - // Assert - Assertions.assertThat(exists).isTrue(); - } - - @Test - public void UserRepository_FindByUsername_ReturnsUserIfExists() { - // Arrange - User user = User.builder() - .username("Braňo") - .email("mail@test.com") - .password("password") - .build(); - userRepository.save(user); - - // Act - User foundUser = userRepository.findByUsername(user.getUsername()); - - // Assert - Assertions.assertThat(foundUser).isNotNull(); - Assertions.assertThat(foundUser.getUsername()).isEqualTo(user.getUsername()); - } - - @Test - public void UserRepository_FindIdByUsername_ReturnsUserIdIfExists() { - // Arrange - User user = User.builder() - .username("Braňo") - .email("mail@test.com") - .password("password") - .build(); - User savedUser = userRepository.save(user); - - // Act - Integer foundId = userRepository.findIdByUsername(user.getUsername()); - - // Assert - Assertions.assertThat(foundId).isNotNull(); - Assertions.assertThat(foundId).isEqualTo(savedUser.getId()); - } - -} diff --git a/src/test/java/com/kasv/gunda/bootcamp/services/AuthServicesTests.java b/src/test/java/com/kasv/gunda/bootcamp/services/AuthServicesTests.java deleted file mode 100644 index f0f952b..0000000 --- a/src/test/java/com/kasv/gunda/bootcamp/services/AuthServicesTests.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.kasv.gunda.bootcamp.services; - -public class AuthServicesTests { - -} -- GitLab From d9e7bbfc85966a007b145d32cf53a8675489d02f Mon Sep 17 00:00:00 2001 From: Samuel Gunda <samuel.gunda@kosickaakademia.sk> Date: Thu, 9 May 2024 12:38:21 +0200 Subject: [PATCH 2/4] refresh token addition --- .../bootcamp/controllers/AuthController.java | 76 +++++++++---------- .../exceptions/BlacklistedJwtException.java | 8 -- .../exceptions/JwtTokenException.java | 8 -- .../exceptions/TokenExceptionHandler.java | 25 ++++++ .../exceptions/TokenRefreshException.java | 14 ++++ .../gunda/bootcamp/models/RefreshToken.java | 27 +++++++ .../payload/request/TokenRefreshRequest.java | 12 +++ .../payload/response/JwtResponse.java | 45 +++++++++++ .../payload/response/MessageResponse.java | 12 ++- .../response/TokenRefreshResponse.java | 18 +++++ .../payload/response/UserResponse.java | 32 ++------ .../repositories/RefreshTokenRepository.java | 18 +++++ .../bootcamp/repositories/UserRepository.java | 2 - .../bootcamp/security/WebSecurityConfig.java | 32 +++++++- .../security/jwt/AuthEntryPointJwt.java | 22 +++++- .../security/jwt/AuthTokenFilter.java | 22 +++--- .../gunda/bootcamp/security/jwt/JwtUtils.java | 55 +++++--------- .../services/RefreshTokenService.java | 55 ++++++++++++++ .../security/services/UserDetailsImpl.java | 13 ++-- .../bootcamp/utilities/ErrorMessage.java | 32 ++++++++ src/main/resources/application.properties | 6 +- 21 files changed, 381 insertions(+), 153 deletions(-) delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java delete mode 100644 src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenExceptionHandler.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenRefreshException.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/models/RefreshToken.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/request/TokenRefreshRequest.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/response/JwtResponse.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/response/TokenRefreshResponse.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/utilities/ErrorMessage.java diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java index 3b142f7..c5bb30e 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java @@ -1,26 +1,27 @@ package com.kasv.gunda.bootcamp.controllers; +import com.kasv.gunda.bootcamp.exceptions.TokenRefreshException; import com.kasv.gunda.bootcamp.models.ERole; +import com.kasv.gunda.bootcamp.models.RefreshToken; import com.kasv.gunda.bootcamp.models.Role; import com.kasv.gunda.bootcamp.models.User; import com.kasv.gunda.bootcamp.payload.request.LoginRequest; import com.kasv.gunda.bootcamp.payload.request.SignupRequest; +import com.kasv.gunda.bootcamp.payload.request.TokenRefreshRequest; +import com.kasv.gunda.bootcamp.payload.response.JwtResponse; import com.kasv.gunda.bootcamp.payload.response.MessageResponse; -import com.kasv.gunda.bootcamp.payload.response.UserResponse; +import com.kasv.gunda.bootcamp.payload.response.TokenRefreshResponse; import com.kasv.gunda.bootcamp.repositories.RoleRepository; import com.kasv.gunda.bootcamp.repositories.UserRepository; import com.kasv.gunda.bootcamp.security.jwt.JwtUtils; import com.kasv.gunda.bootcamp.security.services.UserDetailsImpl; -import jakarta.servlet.http.HttpServletRequest; +import com.kasv.gunda.bootcamp.security.services.RefreshTokenService; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; @@ -49,8 +50,10 @@ public class AuthController { @Autowired JwtUtils jwtUtils; + @Autowired + RefreshTokenService refreshTokenService; + @PostMapping("/signin") - @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager @@ -58,20 +61,13 @@ public class AuthController { SecurityContextHolder.getContext().setAuthentication(authentication); - UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();; + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); - ResponseCookie cookie = jwtUtils.generateJwtCookie(userDetails); + String jwt = jwtUtils.generateJwtToken(userDetails); - List<String> roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority) - .collect(Collectors.toList()); + RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId()); - System.out.println("User logged"); - return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) - .body(new UserResponse(userDetails.getId(), - userDetails.getUsername(), - userDetails.getEmail(), - roles)); + return ResponseEntity.ok(new JwtResponse(jwt, refreshToken.getToken())); } @PostMapping("/signup") @@ -84,8 +80,8 @@ public class AuthController { return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); } - User user = new User(signUpRequest.getUsername(), - signUpRequest.getEmail(), + // Create new user's account + User user = new User(signUpRequest.getUsername(), signUpRequest.getEmail(), encoder.encode(signUpRequest.getPassword())); Set<String> strRoles = signUpRequest.getRole(); @@ -124,28 +120,28 @@ public class AuthController { return ResponseEntity.ok(new MessageResponse("User registered successfully!")); } - @PostMapping("/signout") - @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") - public ResponseEntity<?> logoutUser() { - ResponseCookie cookie = jwtUtils.getCleanJwtCookie(); - return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) - .body(new MessageResponse("You've been signed out!")); + @PostMapping("/refreshtoken") + public ResponseEntity<?> refreshtoken(@Valid @RequestBody TokenRefreshRequest request) { + String requestRefreshToken = request.getRefreshToken(); + + UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + return refreshTokenService.findByToken(requestRefreshToken) + .map(refreshTokenService::verifyExpiration) + .map(RefreshToken::getUser) + .map(user -> { + String token = jwtUtils.generateJwtToken(userDetails); + return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken)); + }) + .orElseThrow(() -> new TokenRefreshException(requestRefreshToken, + "Refresh token is not in database!")); } - @PostMapping("/validate") - @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") - public ResponseEntity<?> validateUser(HttpServletRequest request) { - String token = jwtUtils.getJwtFromCookies(request); - - if (token != null) { - if (jwtUtils.validateJwtToken(token)) { - System.out.println("Valid token!"); - return ResponseEntity.ok(new MessageResponse("Valid token!")); - } else { - return ResponseEntity.badRequest().body(new MessageResponse("Error: Invalid token!")); - } - } else { - return ResponseEntity.badRequest().body(new MessageResponse("Error: Missing token in cookie!")); - } + @PostMapping("/signout") + public ResponseEntity<?> logoutUser() { + UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + Long userId = userDetails.getId(); + refreshTokenService.deleteByUserId(userId); + return ResponseEntity.ok(new MessageResponse("Log out successful!")); } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java b/src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java deleted file mode 100644 index 274bae5..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/exceptions/BlacklistedJwtException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kasv.gunda.bootcamp.exceptions; - -public class BlacklistedJwtException extends RuntimeException { - - public BlacklistedJwtException(String message) { - super(message); - } -} diff --git a/src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java b/src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java deleted file mode 100644 index 57dc924..0000000 --- a/src/main/java/com/kasv/gunda/bootcamp/exceptions/JwtTokenException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.kasv.gunda.bootcamp.exceptions; - -public class JwtTokenException extends RuntimeException { - - public JwtTokenException(String message) { - super(message); - } -} \ No newline at end of file diff --git a/src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenExceptionHandler.java b/src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenExceptionHandler.java new file mode 100644 index 0000000..908aab7 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenExceptionHandler.java @@ -0,0 +1,25 @@ +package com.kasv.gunda.bootcamp.exceptions; + +import java.util.Date; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; + +import com.kasv.gunda.bootcamp.utilities.ErrorMessage; + +@RestControllerAdvice +public class TokenExceptionHandler { + + @ExceptionHandler(value = TokenRefreshException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) { + return new ErrorMessage( + HttpStatus.FORBIDDEN.value(), + new Date(), + ex.getMessage(), + request.getDescription(false)); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenRefreshException.java b/src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenRefreshException.java new file mode 100644 index 0000000..7ebab0d --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/exceptions/TokenRefreshException.java @@ -0,0 +1,14 @@ +package com.kasv.gunda.bootcamp.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.FORBIDDEN) +public class TokenRefreshException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public TokenRefreshException(String token, String message) { + super(String.format("Failed for [%s]: %s", token, message)); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/RefreshToken.java b/src/main/java/com/kasv/gunda/bootcamp/models/RefreshToken.java new file mode 100644 index 0000000..8d5d906 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/models/RefreshToken.java @@ -0,0 +1,27 @@ +package com.kasv.gunda.bootcamp.models; + +import java.time.Instant; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +@Entity(name = "refresh_token") +@Getter +@Setter +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private int id; + + @OneToOne + @JoinColumn(name = "user_id", referencedColumnName = "id") + private User user; + + @Column(nullable = false, unique = true) + private String token; + + @Column(nullable = false) + private Instant expiryDate; + +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/request/TokenRefreshRequest.java b/src/main/java/com/kasv/gunda/bootcamp/payload/request/TokenRefreshRequest.java new file mode 100644 index 0000000..105500f --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/request/TokenRefreshRequest.java @@ -0,0 +1,12 @@ +package com.kasv.gunda.bootcamp.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TokenRefreshRequest { + @NotBlank + private String refreshToken; +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/response/JwtResponse.java b/src/main/java/com/kasv/gunda/bootcamp/payload/response/JwtResponse.java new file mode 100644 index 0000000..c24c6a0 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/response/JwtResponse.java @@ -0,0 +1,45 @@ +package com.kasv.gunda.bootcamp.payload.response; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +public class JwtResponse { + private String token; + private String type = "Bearer"; + private String refreshToken; + private Long id; + private String username; + private String email; + private List<String> roles; + + public JwtResponse(String accessToken, String refreshToken) { + this.token = accessToken; + this.refreshToken = refreshToken; + } + + public String getAccessToken() { + return token; + } + + public void setAccessToken(String accessToken) { + this.token = accessToken; + } + + public String getTokenType() { + return type; + } + + public void setTokenType(String tokenType) { + this.type = tokenType; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java b/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java index f6ff002..c05cde8 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/response/MessageResponse.java @@ -1,5 +1,10 @@ package com.kasv.gunda.bootcamp.payload.response; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter public class MessageResponse { private String message; @@ -7,11 +12,4 @@ public class MessageResponse { this.message = message; } - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/response/TokenRefreshResponse.java b/src/main/java/com/kasv/gunda/bootcamp/payload/response/TokenRefreshResponse.java new file mode 100644 index 0000000..92e1f57 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/response/TokenRefreshResponse.java @@ -0,0 +1,18 @@ +package com.kasv.gunda.bootcamp.payload.response; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TokenRefreshResponse { + private String accessToken; + private String refreshToken; + private String tokenType = "Bearer"; + + public TokenRefreshResponse(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java b/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java index 4132972..c5c4864 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/response/UserResponse.java @@ -1,7 +1,12 @@ package com.kasv.gunda.bootcamp.payload.response; +import lombok.Getter; +import lombok.Setter; + import java.util.List; +@Getter +@Setter public class UserResponse { private Long id; private String username; @@ -15,31 +20,4 @@ public class UserResponse { this.roles = roles; } - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public List<String> getRoles() { - return roles; - } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java new file mode 100644 index 0000000..4153068 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java @@ -0,0 +1,18 @@ +package com.kasv.gunda.bootcamp.repositories; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Repository; + +import com.kasv.gunda.bootcamp.models.RefreshToken; +import com.kasv.gunda.bootcamp.models.User; + +@Repository +public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> { + Optional<RefreshToken> findByToken(String token); + + @Modifying + int deleteByUser(User user); +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java index 3033044..eff9ccd 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java @@ -14,6 +14,4 @@ public interface UserRepository extends JpaRepository<User, Long> { Boolean existsByUsername(String username); Boolean existsByEmail(String email); - - ERole findRoleByUsername(String username); } diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java b/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java index 5c83c89..6157388 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -24,8 +25,8 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; @Configuration -@EnableMethodSecurity(prePostEnabled = true) -public class WebSecurityConfig { +@EnableMethodSecurity +public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceImpl userDetailsService; @@ -37,6 +38,11 @@ public class WebSecurityConfig { return new AuthTokenFilter(); } +// @Override +// public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { +// authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); +// } + @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @@ -47,6 +53,12 @@ public class WebSecurityConfig { return authProvider; } +// @Bean +// @Override +// public AuthenticationManager authenticationManagerBean() throws Exception { +// return super.authenticationManagerBean(); +// } + @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); @@ -57,14 +69,26 @@ public class WebSecurityConfig { return new BCryptPasswordEncoder(); } +// @Override +// protected void configure(HttpSecurity http) throws Exception { +// http.cors().and().csrf().disable() +// .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() +// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() +// .authorizeRequests().antMatchers("/api/auth/**").permitAll() +// .antMatchers("/api/test/**").permitAll() +// .anyRequest().authenticated(); +// +// http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); +// } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.csrf(AbstractHttpConfigurer::disable) + http.csrf(csrf -> csrf.disable()) .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll() - .requestMatchers("/api/test/all").permitAll() + .requestMatchers("/api/test/**").permitAll() .requestMatchers("/api/students/count").permitAll() .anyRequest().authenticated() ); diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java index 85cbcc5..0eb65c4 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthEntryPointJwt.java @@ -1,15 +1,20 @@ package com.kasv.gunda.bootcamp.security.jwt; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; -import java.io.IOException; +import com.fasterxml.jackson.databind.ObjectMapper; @Component public class AuthEntryPointJwt implements AuthenticationEntryPoint { @@ -20,6 +25,19 @@ public class AuthEntryPointJwt implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { logger.error("Unauthorized error: {}", authException.getMessage()); - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map<String, Object> body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + +// response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java index d165a18..917d551 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/AuthTokenFilter.java @@ -12,6 +12,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -34,25 +35,26 @@ public class AuthTokenFilter extends OncePerRequestFilter { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); - - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userDetails, - null, - userDetails.getAuthorities()); - + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, + userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } - } catch (Exception e) { - logger.error("Cannot set user authentication: {}", e); + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e.getMessage()); } filterChain.doFilter(request, response); } private String parseJwt(HttpServletRequest request) { - String jwt = jwtUtils.getJwtFromCookies(request); - return jwt; + String headerAuth = request.getHeader("Authorization"); + + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7, headerAuth.length()); + } + + return null; } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java index 3f63397..e380bae 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java @@ -1,19 +1,18 @@ package com.kasv.gunda.bootcamp.security.jwt; -import com.kasv.gunda.bootcamp.security.services.UserDetailsImpl; -import io.jsonwebtoken.*; -import io.jsonwebtoken.io.Decoders; + import io.jsonwebtoken.security.Keys; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; -import org.springframework.web.util.WebUtils; + +import io.jsonwebtoken.*; + +import com.kasv.gunda.bootcamp.security.services.UserDetailsImpl; import java.security.Key; +import java.util.Base64; import java.util.Date; @Component @@ -26,28 +25,16 @@ public class JwtUtils { @Value("${gunda.bootcamp.app.jwtExpirationMs}") private int jwtExpirationMs; - @Value("${gunda.bootcamp.app.jwtCookieName}") - private String jwtCookie; - - public String getJwtFromCookies(HttpServletRequest request) { - Cookie cookie = WebUtils.getCookie(request, jwtCookie); - if (cookie != null) { - return cookie.getValue(); - } else { - return null; - } - } - - public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) { - String jwt = generateTokenFromUsername(userPrincipal.getUsername()); - ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).sameSite("None").secure(true).build(); - return cookie; + public String generateJwtToken(UserDetailsImpl userPrincipal) { + return Jwts.builder().setSubject(userPrincipal.getUsername()) + .claim("id", userPrincipal.getId()) + .claim("email", userPrincipal.getEmail()) + .claim("roles", userPrincipal.getAuthorities()) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)).signWith(key(), SignatureAlgorithm.HS256) + .compact(); } - public ResponseCookie getCleanJwtCookie() { - ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build(); - return cookie; - } public String getUserNameFromJwtToken(String token) { return Jwts.parserBuilder().setSigningKey(key()).build() @@ -55,13 +42,16 @@ public class JwtUtils { } private Key key() { - return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); + byte[] decodedSecret = Base64.getUrlDecoder().decode(jwtSecret); + return Keys.hmacShaKeyFor(decodedSecret); } public boolean validateJwtToken(String authToken) { try { Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken); return true; + } catch (SignatureException e) { + logger.error("Invalid JWT signature: {}", e.getMessage()); } catch (MalformedJwtException e) { logger.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { @@ -74,13 +64,4 @@ public class JwtUtils { return false; } - - public String generateTokenFromUsername(String username) { - return Jwts.builder() - .setSubject(username) - .setIssuedAt(new Date()) - .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) - .signWith(key(), SignatureAlgorithm.HS256) - .compact(); - } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java b/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java new file mode 100644 index 0000000..1ce54e8 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java @@ -0,0 +1,55 @@ +package com.kasv.gunda.bootcamp.security.services; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.kasv.gunda.bootcamp.exceptions.TokenRefreshException; +import com.kasv.gunda.bootcamp.models.RefreshToken; +import com.kasv.gunda.bootcamp.repositories.RefreshTokenRepository; +import com.kasv.gunda.bootcamp.repositories.UserRepository; + +@Service +public class RefreshTokenService { + @Value("${gunda.bootcamp.app.jwtRefreshExpirationMs}") + private Long refreshTokenDurationMs; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @Autowired + private UserRepository userRepository; + + public Optional<RefreshToken> findByToken(String token) { + return refreshTokenRepository.findByToken(token); + } + + public RefreshToken createRefreshToken(Long userId) { + RefreshToken refreshToken = new RefreshToken(); + + refreshToken.setUser(userRepository.findById(userId).get()); + refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs)); + refreshToken.setToken(UUID.randomUUID().toString()); + + refreshToken = refreshTokenRepository.save(refreshToken); + return refreshToken; + } + + public RefreshToken verifyExpiration(RefreshToken token) { + if (token.getExpiryDate().compareTo(Instant.now()) < 0) { + refreshTokenRepository.delete(token); + throw new TokenRefreshException(token.getToken(), "Refresh token was expired. Please make a new signin request"); + } + + return token; + } + + @Transactional + public int deleteByUserId(Long userId) { + return refreshTokenRepository.deleteByUser(userRepository.findById(userId).get()); + } +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java b/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java index 5efcfd3..3b161cd 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/services/UserDetailsImpl.java @@ -1,16 +1,17 @@ package com.kasv.gunda.bootcamp.security.services; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.kasv.gunda.bootcamp.models.User; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.kasv.gunda.bootcamp.models.User; +import com.fasterxml.jackson.annotation.JsonIgnore; + public class UserDetailsImpl implements UserDetails { private static final long serialVersionUID = 1L; diff --git a/src/main/java/com/kasv/gunda/bootcamp/utilities/ErrorMessage.java b/src/main/java/com/kasv/gunda/bootcamp/utilities/ErrorMessage.java new file mode 100644 index 0000000..9ee24b5 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/utilities/ErrorMessage.java @@ -0,0 +1,32 @@ +package com.kasv.gunda.bootcamp.utilities; +import java.util.Date; + +public class ErrorMessage { + private int statusCode; + private Date timestamp; + private String message; + private String description; + + public ErrorMessage(int statusCode, Date timestamp, String message, String description) { + this.statusCode = statusCode; + this.timestamp = timestamp; + this.message = message; + this.description = description; + } + + public int getStatusCode() { + return statusCode; + } + + public Date getTimestamp() { + return timestamp; + } + + public String getMessage() { + return message; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c23118f..36f4510 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,9 +7,11 @@ spring.datasource.password= spring.jpa.hibernate.ddl-auto=update # App Properties -gunda.bootcamp.app.jwtCookieName= Authorization +gunda.bootcamp.app.jwtCookieName= Jwt +gunda.bootcamp.app.jwtRefreshCookieName= Jwt-Refresh gunda.bootcamp.app.jwtSecret= 15c8374b1a7b790f6888693fa0d2710cfa46bfa2b67b5a80c7aedf4109853c7c -gunda.bootcamp.app.jwtExpirationMs= 20000 +gunda.bootcamp.app.jwtExpirationMs= 1200000 +gunda.bootcamp.app.jwtRefreshExpirationMs= 86400000 spring.mail.host=smtp.gmail.com spring.mail.port=587 -- GitLab From e36f74716c7ab0531d0b1fdbb07f144d2de6edce Mon Sep 17 00:00:00 2001 From: Samuel Gunda <samuel.gunda@kosickaakademia.sk> Date: Fri, 10 May 2024 02:33:06 +0200 Subject: [PATCH 3/4] refresh token function fix --- .../gunda/bootcamp/controllers/AuthController.java | 10 ++++------ src/main/resources/application.properties | 4 +--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java index c5bb30e..e77cc84 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java @@ -27,9 +27,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; import java.util.HashSet; -import java.util.List; import java.util.Set; -import java.util.stream.Collectors; @CrossOrigin(origins = "http://localhost:3000", maxAge = 3600) @RestController @@ -121,16 +119,15 @@ public class AuthController { } @PostMapping("/refreshtoken") - public ResponseEntity<?> refreshtoken(@Valid @RequestBody TokenRefreshRequest request) { + @CrossOrigin(origins = "http://localhost:3000", maxAge = 3600) + public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) { String requestRefreshToken = request.getRefreshToken(); - UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - return refreshTokenService.findByToken(requestRefreshToken) .map(refreshTokenService::verifyExpiration) .map(RefreshToken::getUser) .map(user -> { - String token = jwtUtils.generateJwtToken(userDetails); + String token = jwtUtils.generateJwtToken(UserDetailsImpl.build(user)); return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken)); }) .orElseThrow(() -> new TokenRefreshException(requestRefreshToken, @@ -138,6 +135,7 @@ public class AuthController { } @PostMapping("/signout") + @CrossOrigin(origins = "http://localhost:3000" , maxAge = 3600) public ResponseEntity<?> logoutUser() { UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Long userId = userDetails.getId(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 36f4510..c95eb14 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,10 +7,8 @@ spring.datasource.password= spring.jpa.hibernate.ddl-auto=update # App Properties -gunda.bootcamp.app.jwtCookieName= Jwt -gunda.bootcamp.app.jwtRefreshCookieName= Jwt-Refresh gunda.bootcamp.app.jwtSecret= 15c8374b1a7b790f6888693fa0d2710cfa46bfa2b67b5a80c7aedf4109853c7c -gunda.bootcamp.app.jwtExpirationMs= 1200000 +gunda.bootcamp.app.jwtExpirationMs= 30000 gunda.bootcamp.app.jwtRefreshExpirationMs= 86400000 spring.mail.host=smtp.gmail.com -- GitLab From 8b8ba65180687b8e460acf5fddd6037e0c665c2c Mon Sep 17 00:00:00 2001 From: Samuel Gunda <samuel.gunda@kosickaakademia.sk> Date: Thu, 16 May 2024 02:17:02 +0200 Subject: [PATCH 4/4] finished auth, recovery of student service according to new authorization process, addition of student registration using approval system, api preparation for react front-end needs --- .../bootcamp/controllers/AuthController.java | 13 +- .../controllers/StudentController.java | 61 ++++-- .../kasv/gunda/bootcamp/models/Status.java | 5 + .../kasv/gunda/bootcamp/models/Student.java | 9 +- .../bootcamp/models/StudentRegistration.java | 38 ++++ .../payload/request/StudentUpdateRequest.java | 14 ++ .../repositories/RefreshTokenRepository.java | 4 + .../StudentRegistrationRepository.java | 9 + .../bootcamp/repositories/UserRepository.java | 2 + .../bootcamp/security/WebSecurityConfig.java | 25 +-- .../gunda/bootcamp/security/jwt/JwtUtils.java | 5 + .../services/RefreshTokenService.java | 4 +- .../bootcamp/services/StudentService.java | 200 +++++++++--------- src/main/resources/application.properties | 4 +- 14 files changed, 234 insertions(+), 159 deletions(-) create mode 100644 src/main/java/com/kasv/gunda/bootcamp/models/Status.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/models/StudentRegistration.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/payload/request/StudentUpdateRequest.java create mode 100644 src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRegistrationRepository.java diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java index e77cc84..522578a 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/AuthController.java @@ -27,6 +27,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @CrossOrigin(origins = "http://localhost:3000", maxAge = 3600) @@ -137,9 +138,13 @@ public class AuthController { @PostMapping("/signout") @CrossOrigin(origins = "http://localhost:3000" , maxAge = 3600) public ResponseEntity<?> logoutUser() { - UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - Long userId = userDetails.getId(); - refreshTokenService.deleteByUserId(userId); - return ResponseEntity.ok(new MessageResponse("Log out successful!")); + Object principle = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + if (!Objects.equals(principle.toString(), "anonymousUser")) { + Long userId = ((UserDetailsImpl) principle).getId(); + refreshTokenService.deleteByUserId(userId); + } + + return ResponseEntity.ok().body(new MessageResponse("You've been signed out!")); } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java b/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java index b37e6d1..b425ac5 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java +++ b/src/main/java/com/kasv/gunda/bootcamp/controllers/StudentController.java @@ -2,21 +2,28 @@ package com.kasv.gunda.bootcamp.controllers; import com.google.gson.Gson; import com.kasv.gunda.bootcamp.models.Student; +import com.kasv.gunda.bootcamp.models.StudentRegistration; +import com.kasv.gunda.bootcamp.payload.request.StudentUpdateRequest; import com.kasv.gunda.bootcamp.payload.response.MessageResponse; import com.kasv.gunda.bootcamp.services.StudentService; import jakarta.servlet.http.HttpServletRequest; +import org.apache.coyote.Response; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import com.kasv.gunda.bootcamp.security.jwt.JwtUtils; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api") -@CrossOrigin +@CrossOrigin(origins = "http://localhost:3000") public class StudentController { private final StudentService studentService; @@ -32,40 +39,48 @@ public class StudentController { students can only view their first name and first letter of their last name */ @GetMapping("/students") - @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") - public String getAllStudents(HttpServletRequest request) { -// studentService.getAllStudents(request); - System.out.println(request.getHeader("Authorization")); - System.out.println(Arrays.toString(request.getCookies())); - return "All students"; + @CrossOrigin(origins = "http://localhost:3000") + public ResponseEntity<String> getAllStudents() { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); + + return ResponseEntity.ok(studentService.getAllStudents(authorities)); } - @PostMapping("/student/{id}") + @GetMapping("/student/{id}") + @CrossOrigin(origins = "http://localhost:3000") public ResponseEntity<String> getStudentById(@PathVariable Long id) { - return null; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); + + if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) { + return studentService.getStudentById(id); + } else { + return ResponseEntity.status(403).body("You are not authorized to view this resource."); + } } /* Registers a new student */ @PostMapping("/student/register") - public ResponseEntity<String> registerStudent(@RequestBody Student student) { + @CrossOrigin(origins = "http://localhost:3000") + public ResponseEntity<String> registerStudent(@RequestBody StudentRegistration studentForm, HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); + String authorizationHeader = request.getHeader("Authorization"); - Gson gson = new Gson(); - Map<String, String> jsonResponse = new HashMap<>(); - - if (student.getFirstName() == null || student.getFirstName().isEmpty() || - student.getLastName() == null || student.getLastName().isEmpty() || - student.getDob() == null) { - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); - } - - return studentService.registerStudent(student); + return studentService.registerStudent(studentForm, authorities, authorizationHeader); } /* Updates student data */ @PutMapping("/student/update/{id}") - public ResponseEntity<String> updateStudent( @PathVariable Long id, @RequestBody Student student) { - return null; + @CrossOrigin(origins = "http://localhost:3000") + public ResponseEntity<String> updateStudent(@PathVariable Long id, @RequestBody StudentUpdateRequest updateRequest, HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); + String authorizationHeader = request.getHeader("Authorization"); + + return studentService.updateStudent(id, updateRequest, authorities, authorizationHeader); } /* Returns the count of all students */ diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/Status.java b/src/main/java/com/kasv/gunda/bootcamp/models/Status.java new file mode 100644 index 0000000..41e95b7 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/models/Status.java @@ -0,0 +1,5 @@ +package com.kasv.gunda.bootcamp.models; + +public enum Status { + PENDING, DECLINED, ACCEPTED +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/Student.java b/src/main/java/com/kasv/gunda/bootcamp/models/Student.java index dd6c6cb..8d71f06 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/models/Student.java +++ b/src/main/java/com/kasv/gunda/bootcamp/models/Student.java @@ -1,10 +1,6 @@ package com.kasv.gunda.bootcamp.models; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.*; import java.util.Date; @@ -25,5 +21,8 @@ public class Student { private String lastName; private Date dob; + @OneToOne + @JoinColumn(name = "user_id") + private User user; } diff --git a/src/main/java/com/kasv/gunda/bootcamp/models/StudentRegistration.java b/src/main/java/com/kasv/gunda/bootcamp/models/StudentRegistration.java new file mode 100644 index 0000000..00ba2af --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/models/StudentRegistration.java @@ -0,0 +1,38 @@ +package com.kasv.gunda.bootcamp.models; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Entity +@Getter +@Setter +@Table(name = "student_registrations") +public class StudentRegistration { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + + private String firstName; + + private String lastName; + + private Date dob; + + @Enumerated(EnumType.STRING) + private Status status = Status.PENDING; + + public StudentRegistration(User user, String firstName, String lastName, Date dob) { + this.userId = user.getId(); + this.firstName = firstName; + this.lastName = lastName; + this.dob = dob; + } + + public StudentRegistration() {} +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/payload/request/StudentUpdateRequest.java b/src/main/java/com/kasv/gunda/bootcamp/payload/request/StudentUpdateRequest.java new file mode 100644 index 0000000..89e3678 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/payload/request/StudentUpdateRequest.java @@ -0,0 +1,14 @@ +package com.kasv.gunda.bootcamp.payload.request; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +public class StudentUpdateRequest { + private String firstName; + private String lastName; + private Date dob; +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java index 4153068..cfbd530 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/RefreshTokenRepository.java @@ -13,6 +13,10 @@ import com.kasv.gunda.bootcamp.models.User; public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> { Optional<RefreshToken> findByToken(String token); + Optional<RefreshToken> findByUser(User user); + @Modifying int deleteByUser(User user); + + int deleteByUserId(Long userId); } diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRegistrationRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRegistrationRepository.java new file mode 100644 index 0000000..457c352 --- /dev/null +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/StudentRegistrationRepository.java @@ -0,0 +1,9 @@ +package com.kasv.gunda.bootcamp.repositories; + +import com.kasv.gunda.bootcamp.models.Status; +import com.kasv.gunda.bootcamp.models.StudentRegistration; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StudentRegistrationRepository extends JpaRepository<StudentRegistration, Integer> { + +} diff --git a/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java b/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java index eff9ccd..4b8d4ed 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java +++ b/src/main/java/com/kasv/gunda/bootcamp/repositories/UserRepository.java @@ -11,6 +11,8 @@ import java.util.Optional; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); + User getUserById(Long id); + Boolean existsByUsername(String username); Boolean existsByEmail(String email); diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java b/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java index 6157388..2b5bf4f 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/WebSecurityConfig.java @@ -26,7 +26,7 @@ import java.util.Arrays; @Configuration @EnableMethodSecurity -public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { +public class WebSecurityConfig { @Autowired UserDetailsServiceImpl userDetailsService; @@ -38,11 +38,6 @@ public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { return new AuthTokenFilter(); } -// @Override -// public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { -// authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); -// } - @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @@ -53,12 +48,6 @@ public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { return authProvider; } -// @Bean -// @Override -// public AuthenticationManager authenticationManagerBean() throws Exception { -// return super.authenticationManagerBean(); -// } - @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); @@ -69,18 +58,6 @@ public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { return new BCryptPasswordEncoder(); } -// @Override -// protected void configure(HttpSecurity http) throws Exception { -// http.cors().and().csrf().disable() -// .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() -// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() -// .authorizeRequests().antMatchers("/api/auth/**").permitAll() -// .antMatchers("/api/test/**").permitAll() -// .anyRequest().authenticated(); -// -// http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); -// } - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java index e380bae..cf3bf68 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/jwt/JwtUtils.java @@ -41,6 +41,11 @@ public class JwtUtils { .parseClaimsJws(token).getBody().getSubject(); } + public Long getUserIdFromJwtToken(String token) { + return Jwts.parserBuilder().setSigningKey(key()).build() + .parseClaimsJws(token).getBody().get("id", Long.class); + } + private Key key() { byte[] decodedSecret = Base64.getUrlDecoder().decode(jwtSecret); return Keys.hmacShaKeyFor(decodedSecret); diff --git a/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java b/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java index 1ce54e8..196446b 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java +++ b/src/main/java/com/kasv/gunda/bootcamp/security/services/RefreshTokenService.java @@ -49,7 +49,7 @@ public class RefreshTokenService { } @Transactional - public int deleteByUserId(Long userId) { - return refreshTokenRepository.deleteByUser(userRepository.findById(userId).get()); + public void deleteByUserId(Long userId) { + refreshTokenRepository.findByUser(userRepository.findById(userId).get()).ifPresent(refreshTokenRepository::delete); } } diff --git a/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java b/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java index 79769c3..3aaa91a 100644 --- a/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java +++ b/src/main/java/com/kasv/gunda/bootcamp/services/StudentService.java @@ -2,137 +2,139 @@ package com.kasv.gunda.bootcamp.services; import com.google.gson.Gson; import com.kasv.gunda.bootcamp.models.Student; +import com.kasv.gunda.bootcamp.models.StudentRegistration; +import com.kasv.gunda.bootcamp.payload.request.StudentUpdateRequest; +import com.kasv.gunda.bootcamp.repositories.StudentRegistrationRepository; import com.kasv.gunda.bootcamp.repositories.StudentRepository; +import com.kasv.gunda.bootcamp.repositories.UserRepository; import com.kasv.gunda.bootcamp.security.jwt.JwtUtils; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; @Service public class StudentService { private final JwtUtils jwtUtils; private final StudentRepository studentRepository; + private final StudentRegistrationRepository studentRegistrationRepository; + private final UserRepository userRepository; - - - public StudentService(JwtUtils jwtUtils, StudentRepository studentRepository) { + public StudentService(JwtUtils jwtUtils, StudentRepository studentRepository, StudentRegistrationRepository studentRegistrationRepository, UserRepository userRepository) { this.jwtUtils = jwtUtils; this.studentRepository = studentRepository; + this.studentRegistrationRepository = studentRegistrationRepository; + this.userRepository = userRepository; } -// public String getAllStudents(HttpServletRequest request) { -// -// String token = jwtUtils.getJwtFromCookies(request); -// String username = jwtUtils.getUserNameFromJwtToken(token); -// -// -// -// -// List<Student> students = studentRepository.findAll(); -// -// -// if () { -// -// if (!tokenFunctions.isTokenValid(authCheck.getToken())) { -// -// for (Student student : students) { -// student.setDob(null); -// student.setLastName(student.getLastName().substring(0, 1)); -// } -// -// } -// } else { -// for (Student student : students) { -// student.setDob(null); -// student.setLastName(student.getLastName().substring(0, 1)); -// } -// } -// return gson.toJson(students); -// } - -// public ResponseEntity<String> getStudentById(Long id, AuthCheck authCheck) { -// -// Gson gson = new Gson(); -// Map<String, String> jsonResponse = new HashMap<>(); -// -// long userId = (long) userRepository.findIdByUsername(authCheck.getUsername()); -// -// if (tokenFunctions.isTokenExists(userId)) { -// if (!tokenFunctions.isTokenValid(authCheck.getToken())) { -// jsonResponse.put("error", "Invalid token. Please provide valid token."); -// return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); -// } -// } else { -// jsonResponse.put("error", "Invalid token. Please provide valid token."); -// return ResponseEntity.status(401).body(gson.toJson(jsonResponse)); -// } -// -// if (!studentRepository.existsById(id)) { -// jsonResponse.put("error", "Student with id " + id + " not found."); -// return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); -// } -// -// return ResponseEntity.status(200).body(gson.toJson(studentRepository.findById(id))); -// } + public String getAllStudents(Collection<? extends GrantedAuthority> authorities) { + + Gson gson = new Gson(); + List<Student> students = studentRepository.findAll(); + + if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) { + + return gson.toJson(students); + + } + + for (Student student : students) { + student.setDob(null); + student.setLastName(student.getLastName().substring(0, 1)); + } + + return gson.toJson(students); + } public int getStudentsCount() { return (int) studentRepository.count(); } - public ResponseEntity<String> registerStudent(Student student) { - + public ResponseEntity<String> registerStudent(StudentRegistration sur, Collection<? extends GrantedAuthority> authorities , String authorizationHeader) { Gson gson = new Gson(); Map<String, String> jsonResponse = new HashMap<>(); - if (student == null) { - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); + if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) { + try { + Student student = new Student(); + student.setFirstName(sur.getFirstName()); + student.setLastName(sur.getLastName()); + student.setDob(sur.getDob()); + student.setUser(userRepository.getUserById(sur.getUserId())); + studentRepository.save(student); + return ResponseEntity.status(201).body(""); + } catch (Exception e) { + jsonResponse.put("error", "Somethings wrong I can feel it"); + return ResponseEntity.status(403).body(gson.toJson(jsonResponse)); + } } - if (student.getFirstName() == null || student.getFirstName().isEmpty() || - student.getLastName() == null || student.getLastName().isEmpty() || - student.getDob() == null) { - jsonResponse.put("error", "Invalid request. Please provide valid input data."); - return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + String token = authorizationHeader.substring(7); + long usersId = jwtUtils.getUserIdFromJwtToken(token); + + if (usersId != sur.getUserId()) { + jsonResponse.put("error", "You are not authorized to perform this action."); + return ResponseEntity.status(403).body(gson.toJson(jsonResponse)); + } + + try { + studentRegistrationRepository.save(sur); + return ResponseEntity.status(201).body("Your registration is pending approval."); + } catch (Exception e) { + jsonResponse.put("error", e.getMessage()); + return ResponseEntity.status(403).body(gson.toJson(jsonResponse)); + } } - if(studentRepository.existsById(student.getId())) { - jsonResponse.put("error", "Student with id " + student.getId() + " already exists."); + jsonResponse.put("error", "You are not authorized to perform this action."); + return ResponseEntity.status(403).body(gson.toJson(jsonResponse)); + } + + public ResponseEntity<String> getStudentById(Long id) { + + Gson gson = new Gson(); + Map<String, String> jsonResponse = new HashMap<>(); + + if (!studentRepository.existsById(id)) { + jsonResponse.put("error", "Student with id " + id + " not found."); + return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); + } + + return ResponseEntity.status(200).body(gson.toJson(studentRepository.findById(id))); + } + + public ResponseEntity<String> updateStudent(long id, StudentUpdateRequest sur, Collection<? extends GrantedAuthority> authorities, String authorizationHeader) { + + Gson gson = new Gson(); + Map<String, String> jsonResponse = new HashMap<>(); + + if (sur == null || !studentRepository.existsById(id)) { + jsonResponse.put("error", "Invalid request. Please provide valid input data."); return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); } - studentRepository.save(student); + Student studentFromDatabase = studentRepository.findById(id); - return ResponseEntity.status(201).body("{}"); - } + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + String token = authorizationHeader.substring(7); + long usersId = jwtUtils.getUserIdFromJwtToken(token); + + if (usersId != studentFromDatabase.getUser().getId()) { + if (authorities.stream().noneMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))){ + jsonResponse.put("error", "You are not authorized to perform this action."); + return ResponseEntity.status(403).body(gson.toJson(jsonResponse)); + } + } + } + studentFromDatabase.setFirstName(sur.getFirstName() != null ? sur.getFirstName() : studentFromDatabase.getFirstName()); + studentFromDatabase.setLastName(sur.getLastName() != null ? sur.getLastName() : studentFromDatabase.getLastName()); + studentFromDatabase.setDob(sur.getDob() != null ? sur.getDob() : studentFromDatabase.getDob()); -// public ResponseEntity<String> updateLastName(Long id, LogoutRequest details) { -// -// Gson gson = new Gson(); -// Map<String, String> jsonResponse = new HashMap<>(); -// -// if (!tokenFunctions.isTokenValid(details.getToken())) { -// jsonResponse.put("error", "Invalid request. Please provide valid input data."); -// return ResponseEntity.status(400).body(gson.toJson(jsonResponse)); -// } -// -// if (!studentRepository.existsById(id)) { -// jsonResponse.put("error", "Student with id " + id + " not found."); -// return ResponseEntity.status(404).body(gson.toJson(jsonResponse)); -// } -// -// Student student = studentRepository.findById(id); -// student.setLastName(details.getLastName()); -// -// studentRepository.save(student); -// -// return ResponseEntity.status(200).body("{}"); -// } + studentRepository.save(studentFromDatabase); + return ResponseEntity.status(200).body(""); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c95eb14..36b17e6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,8 +8,8 @@ spring.jpa.hibernate.ddl-auto=update # App Properties gunda.bootcamp.app.jwtSecret= 15c8374b1a7b790f6888693fa0d2710cfa46bfa2b67b5a80c7aedf4109853c7c -gunda.bootcamp.app.jwtExpirationMs= 30000 -gunda.bootcamp.app.jwtRefreshExpirationMs= 86400000 +gunda.bootcamp.app.jwtExpirationMs= 300000 +gunda.bootcamp.app.jwtRefreshExpirationMs= 86400000 spring.mail.host=smtp.gmail.com spring.mail.port=587 -- GitLab