diff --git a/pom.xml b/pom.xml index c06b865..3e4f30a 100644 --- a/pom.xml +++ b/pom.xml @@ -40,10 +40,15 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-actuator + 3.4.5 + com.github.Podzilla podzilla-utils-lib - v1.1.5 + v1.1.6 org.springframework.boot @@ -107,6 +112,10 @@ jakarta.validation-api 3.0.2 + + org.springframework.boot + spring-boot-starter-validation + org.mockito mockito-core diff --git a/src/main/java/com/podzilla/auth/controller/AdminController.java b/src/main/java/com/podzilla/auth/controller/AdminController.java index 0976764..fbefc3e 100644 --- a/src/main/java/com/podzilla/auth/controller/AdminController.java +++ b/src/main/java/com/podzilla/auth/controller/AdminController.java @@ -1,5 +1,6 @@ package com.podzilla.auth.controller; +import com.podzilla.auth.dto.AddCourierRequest; import com.podzilla.auth.model.User; import com.podzilla.auth.service.AdminService; import io.swagger.v3.oas.annotations.Operation; @@ -8,12 +9,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.PostMapping; + import java.util.List; import java.util.UUID; @@ -71,4 +75,18 @@ public void deleteUser( LOGGER.debug("Admin requested to delete user with userId={}", userId); adminService.deleteUser(userId); } + + @PostMapping("/courier") + @Operation(summary = "Add a new courier", + description = "Allows an admin to add a new courier to the system.") + @ApiResponse(responseCode = "200", + description = "Courier added successfully") + public void addCourier( + @Parameter(description = "Courier details") + @RequestBody final AddCourierRequest addCourierRequest) { + + LOGGER.debug("Admin requested to add a new courier with details={}", + addCourierRequest); + adminService.addCourier(addCourierRequest); + } } diff --git a/src/main/java/com/podzilla/auth/controller/AuthenticationController.java b/src/main/java/com/podzilla/auth/controller/AuthenticationController.java index 0d1c85f..a4cd768 100644 --- a/src/main/java/com/podzilla/auth/controller/AuthenticationController.java +++ b/src/main/java/com/podzilla/auth/controller/AuthenticationController.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -43,7 +44,7 @@ public AuthenticationController( description = "User logged in successfully" ) public ResponseEntity login( - @RequestBody final LoginRequest loginRequest, + @Valid @RequestBody final LoginRequest loginRequest, final HttpServletResponse response) { String email = authenticationService.login(loginRequest, response); LOGGER.info("User {} logged in", email); @@ -62,7 +63,7 @@ public ResponseEntity login( description = "User registered successfully" ) public ResponseEntity registerUser( - @RequestBody final SignupRequest signupRequest) { + @Valid @RequestBody final SignupRequest signupRequest) { authenticationService.registerAccount(signupRequest); LOGGER.info("User {} registered", signupRequest.getEmail()); return new ResponseEntity<>("Account registered.", diff --git a/src/main/java/com/podzilla/auth/controller/UserController.java b/src/main/java/com/podzilla/auth/controller/UserController.java index eea7dc5..d642efc 100644 --- a/src/main/java/com/podzilla/auth/controller/UserController.java +++ b/src/main/java/com/podzilla/auth/controller/UserController.java @@ -1,19 +1,18 @@ package com.podzilla.auth.controller; +import com.podzilla.auth.dto.UpdateRequest; +import com.podzilla.auth.dto.UserDetailsRequest; import com.podzilla.auth.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; -import java.util.UUID; - @RestController @RequestMapping("/user") public class UserController { @@ -27,14 +26,24 @@ public UserController(final UserService userService) { this.userService = userService; } - @PutMapping("/update/{userId}") + @PutMapping("/update") @Operation(summary = "Update user name", description = "Allows user to update their name.") @ApiResponse(responseCode = "200", description = "User profile updated successfully") - public void updateProfile(@PathVariable final UUID userId, - @Valid @RequestBody final String name) { - LOGGER.debug("Received updateProfile request for userId={}", userId); - userService.updateUserProfile(userId, name); + public void updateProfile(@RequestBody final UpdateRequest + updateRequest) { + LOGGER.debug("Received updateProfile request"); + userService.updateUserProfile(updateRequest); + } + + @GetMapping("/details") + @Operation(summary = "Get user details", + description = "Fetches the details of the current user.") + @ApiResponse(responseCode = "200", + description = "User details fetched successfully") + public UserDetailsRequest getUserDetails() { + LOGGER.debug("Received getUserDetails request"); + return userService.getUserDetails(); } } diff --git a/src/main/java/com/podzilla/auth/dto/AddCourierRequest.java b/src/main/java/com/podzilla/auth/dto/AddCourierRequest.java new file mode 100644 index 0000000..0336544 --- /dev/null +++ b/src/main/java/com/podzilla/auth/dto/AddCourierRequest.java @@ -0,0 +1,21 @@ +package com.podzilla.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class AddCourierRequest { + @NotBlank(message = "Name is required") + private String name; + + @Email + @NotBlank(message = "Email is required") + private String email; + + @NotBlank(message = "Password is required") + private String password; + + @NotBlank(message = "Mobile number is required") + private String mobileNumber; +} diff --git a/src/main/java/com/podzilla/auth/dto/CustomUserDetails.java b/src/main/java/com/podzilla/auth/dto/CustomUserDetails.java index 1337bd7..4803fba 100644 --- a/src/main/java/com/podzilla/auth/dto/CustomUserDetails.java +++ b/src/main/java/com/podzilla/auth/dto/CustomUserDetails.java @@ -11,6 +11,7 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Set; +import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) @Builder @@ -21,6 +22,8 @@ public class CustomUserDetails implements UserDetails { private String username; + private UUID id; + @JsonIgnore private String password; diff --git a/src/main/java/com/podzilla/auth/dto/LoginRequest.java b/src/main/java/com/podzilla/auth/dto/LoginRequest.java index dce141e..1030318 100644 --- a/src/main/java/com/podzilla/auth/dto/LoginRequest.java +++ b/src/main/java/com/podzilla/auth/dto/LoginRequest.java @@ -1,9 +1,16 @@ package com.podzilla.auth.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data public class LoginRequest { + + @Email + @NotBlank(message = "Email is required") private String email; + + @NotBlank(message = "Password is required") private String password; } diff --git a/src/main/java/com/podzilla/auth/dto/SignupRequest.java b/src/main/java/com/podzilla/auth/dto/SignupRequest.java index 5749166..380f4ff 100644 --- a/src/main/java/com/podzilla/auth/dto/SignupRequest.java +++ b/src/main/java/com/podzilla/auth/dto/SignupRequest.java @@ -1,10 +1,26 @@ package com.podzilla.auth.dto; +import com.podzilla.mq.events.DeliveryAddress; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data public class SignupRequest { + @NotBlank(message = "Name is required") private String name; + + @Email + @NotBlank(message = "Email is required") private String email; + + @NotBlank(message = "Password is required") private String password; + + @NotBlank(message = "Mobile number is required") + private String mobileNumber; + + @Valid + private DeliveryAddress address; } diff --git a/src/main/java/com/podzilla/auth/dto/UpdateRequest.java b/src/main/java/com/podzilla/auth/dto/UpdateRequest.java new file mode 100644 index 0000000..9258d09 --- /dev/null +++ b/src/main/java/com/podzilla/auth/dto/UpdateRequest.java @@ -0,0 +1,11 @@ +package com.podzilla.auth.dto; + +import com.podzilla.mq.events.DeliveryAddress; +import lombok.Data; + +@Data +public class UpdateRequest { + private String name; + private DeliveryAddress address; + private String mobileNumber; +} diff --git a/src/main/java/com/podzilla/auth/dto/UserDetailsRequest.java b/src/main/java/com/podzilla/auth/dto/UserDetailsRequest.java new file mode 100644 index 0000000..6f75b6f --- /dev/null +++ b/src/main/java/com/podzilla/auth/dto/UserDetailsRequest.java @@ -0,0 +1,18 @@ +package com.podzilla.auth.dto; + +import com.podzilla.mq.events.DeliveryAddress; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class UserDetailsRequest { + private String email; + private String name; + private String mobileNumber; + private DeliveryAddress address; +} diff --git a/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java b/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java index 19eb55d..bbef77c 100644 --- a/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/podzilla/auth/exception/GlobalExceptionHandler.java @@ -1,16 +1,41 @@ package com.podzilla.auth.exception; +import io.micrometer.common.lang.NonNull; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + @Override // Good practice to use @Override + protected ResponseEntity handleMethodArgumentNotValid( + final MethodArgumentNotValidException ex, + @NonNull final HttpHeaders headers, + @NonNull final HttpStatusCode status, + @NonNull final WebRequest request) { + + StringBuilder errorMessage = new StringBuilder("Validation failed: "); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + errorMessage.append(error.getField()).append(": ") + .append(error.getDefaultMessage()).append("; "); + } + + ErrorResponse errorResponse = new ErrorResponse( + errorMessage.toString(), (HttpStatus) status); + + return new ResponseEntity<>(errorResponse, status); + + } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handleAccessDeniedException( diff --git a/src/main/java/com/podzilla/auth/model/Address.java b/src/main/java/com/podzilla/auth/model/Address.java new file mode 100644 index 0000000..420f7b4 --- /dev/null +++ b/src/main/java/com/podzilla/auth/model/Address.java @@ -0,0 +1,46 @@ +package com.podzilla.auth.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Getter; + +import java.util.UUID; + +@Entity +@Table(name = "addresses") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class Address { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @OneToOne(optional = false) + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + private User user; + + private String street; + + private String city; + + private String state; + + private String country; + + private String postalCode; +} diff --git a/src/main/java/com/podzilla/auth/model/ERole.java b/src/main/java/com/podzilla/auth/model/ERole.java index e877932..40b0494 100644 --- a/src/main/java/com/podzilla/auth/model/ERole.java +++ b/src/main/java/com/podzilla/auth/model/ERole.java @@ -2,5 +2,6 @@ public enum ERole { ROLE_USER, - ROLE_ADMIN + ROLE_ADMIN, + ROLE_COURIER } diff --git a/src/main/java/com/podzilla/auth/model/User.java b/src/main/java/com/podzilla/auth/model/User.java index 6b9b230..d0b6ac5 100644 --- a/src/main/java/com/podzilla/auth/model/User.java +++ b/src/main/java/com/podzilla/auth/model/User.java @@ -10,6 +10,7 @@ import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.FetchType; @@ -18,7 +19,6 @@ import java.util.UUID; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -37,17 +37,21 @@ public class User { @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @NotBlank(message = "Name is required") private String name; - @NotBlank(message = "Email is required") @Email @Column(unique = true) private String email; - @NotBlank(message = "Password is required") private String password; + @Column(unique = true) + private String mobileNumber; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, + orphanRemoval = true) + private Address address; + @Builder.Default @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "users_roles", diff --git a/src/main/java/com/podzilla/auth/repository/AddressRepository.java b/src/main/java/com/podzilla/auth/repository/AddressRepository.java new file mode 100644 index 0000000..7c5fa0e --- /dev/null +++ b/src/main/java/com/podzilla/auth/repository/AddressRepository.java @@ -0,0 +1,11 @@ +package com.podzilla.auth.repository; + +import com.podzilla.auth.model.Address; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface AddressRepository extends JpaRepository { + Optional
findByUserId(UUID userId); +} diff --git a/src/main/java/com/podzilla/auth/repository/UserRepository.java b/src/main/java/com/podzilla/auth/repository/UserRepository.java index 683717a..53b057a 100644 --- a/src/main/java/com/podzilla/auth/repository/UserRepository.java +++ b/src/main/java/com/podzilla/auth/repository/UserRepository.java @@ -11,4 +11,5 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Boolean existsByEmail(String email); + Boolean existsByMobileNumber(String mobileNumber); } diff --git a/src/main/java/com/podzilla/auth/seeder/DatabaseSeeder.java b/src/main/java/com/podzilla/auth/seeder/DatabaseSeeder.java new file mode 100644 index 0000000..cbb6d65 --- /dev/null +++ b/src/main/java/com/podzilla/auth/seeder/DatabaseSeeder.java @@ -0,0 +1,24 @@ +package com.podzilla.auth.seeder; + +import com.podzilla.auth.model.ERole; +import com.podzilla.auth.model.Role; +import com.podzilla.auth.repository.RoleRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class DatabaseSeeder implements CommandLineRunner { + + private final RoleRepository roleRepository; + + public DatabaseSeeder(final RoleRepository roleRepository) { + this.roleRepository = roleRepository; + } + + @Override + public void run(final String... args) throws Exception { + roleRepository.save(new Role(ERole.ROLE_USER)); + roleRepository.save(new Role(ERole.ROLE_ADMIN)); + roleRepository.save(new Role(ERole.ROLE_COURIER)); + } +} diff --git a/src/main/java/com/podzilla/auth/service/AdminService.java b/src/main/java/com/podzilla/auth/service/AdminService.java index 3801478..a642552 100644 --- a/src/main/java/com/podzilla/auth/service/AdminService.java +++ b/src/main/java/com/podzilla/auth/service/AdminService.java @@ -1,13 +1,20 @@ package com.podzilla.auth.service; +import com.podzilla.auth.dto.AddCourierRequest; import com.podzilla.auth.dto.CustomGrantedAuthority; import com.podzilla.auth.dto.CustomUserDetails; +import com.podzilla.auth.model.ERole; import com.podzilla.auth.model.User; +import com.podzilla.auth.repository.RoleRepository; import com.podzilla.auth.repository.UserRepository; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.CourierRegisteredEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,13 +32,22 @@ public class AdminService { private final UserRepository userRepository; private final UserService userService; private final CacheService cacheService; + private final PasswordEncoder passwordEncoder; + private final RoleRepository roleRepository; + private final EventPublisher eventPublisher; public AdminService(final UserRepository userRepository, final UserService userService, - final CacheService cacheService) { + final CacheService cacheService, + final PasswordEncoder passwordEncoder, + final RoleRepository roleRepository, + final EventPublisher eventPublisher) { this.userRepository = userRepository; this.userService = userService; this.cacheService = cacheService; + this.passwordEncoder = passwordEncoder; + this.roleRepository = roleRepository; + this.eventPublisher = eventPublisher; } public List getUsers() { @@ -61,6 +77,24 @@ public void deleteUser(final UUID userId) { LOGGER.debug("User deleted successfully with userId={}", userId); } + @Transactional + public void addCourier(final AddCourierRequest request) { + User user = new User(); + user.setName(request.getName()); + user.setEmail(request.getEmail()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setMobileNumber(request.getMobileNumber()); + user.setEnabled(true); + user.setRoles(Set.of(roleRepository.findByErole(ERole.ROLE_COURIER) + .orElseThrow(() -> new RuntimeException("Role not found")))); + + userRepository.save(user); + + eventPublisher.publishEvent(EventsConstants.COURIER_REGISTERED, + new CourierRegisteredEvent(user.getId().toString(), + user.getName(), user.getMobileNumber())); + } + public static UserDetails getUserDetails(final User user) { Set authorities = user .getRoles() @@ -71,6 +105,7 @@ public static UserDetails getUserDetails(final User user) { return CustomUserDetails.builder() .username(user.getEmail()) + .id(user.getId()) .password(user.getPassword()) .enabled(user.getEnabled()) .authorities(authorities) diff --git a/src/main/java/com/podzilla/auth/service/AuthenticationService.java b/src/main/java/com/podzilla/auth/service/AuthenticationService.java index f31fca6..f48c5a9 100644 --- a/src/main/java/com/podzilla/auth/service/AuthenticationService.java +++ b/src/main/java/com/podzilla/auth/service/AuthenticationService.java @@ -1,14 +1,19 @@ package com.podzilla.auth.service; +import com.podzilla.auth.dto.CustomUserDetails; import com.podzilla.auth.dto.LoginRequest; import com.podzilla.auth.dto.SignupRequest; import com.podzilla.auth.exception.InvalidActionException; import com.podzilla.auth.exception.ValidationException; +import com.podzilla.auth.model.Address; import com.podzilla.auth.model.ERole; import com.podzilla.auth.model.Role; import com.podzilla.auth.model.User; import com.podzilla.auth.repository.RoleRepository; import com.podzilla.auth.repository.UserRepository; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.CustomerRegisteredEvent; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; @@ -31,18 +36,21 @@ public class AuthenticationService { private final UserRepository userRepository; private final TokenService tokenService; private final RoleRepository roleRepository; + private final EventPublisher eventPublisher; public AuthenticationService( final AuthenticationManager authenticationManager, final PasswordEncoder passwordEncoder, final UserRepository userRepository, final TokenService tokenService, - final RoleRepository roleRepository) { + final RoleRepository roleRepository, + final EventPublisher eventPublisher) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; this.userRepository = userRepository; this.tokenService = tokenService; this.roleRepository = roleRepository; + this.eventPublisher = eventPublisher; } public String login(final LoginRequest loginRequest, @@ -71,19 +79,23 @@ public String login(final LoginRequest loginRequest, public void registerAccount(final SignupRequest signupRequest) { checkUserLoggedIn("User cannot register while logged in."); - checkNotNullValidationException(signupRequest, - "Signup request cannot be null."); - checkNotNullValidationException(signupRequest.getEmail(), - "Email cannot be null."); - checkNotNullValidationException(signupRequest.getPassword(), - "Password cannot be null."); - checkNotNullValidationException(signupRequest.getName(), - "Name cannot be null."); - if (userRepository.existsByEmail(signupRequest.getEmail())) { throw new ValidationException("Email already in use."); } + if (userRepository.existsByMobileNumber( + signupRequest.getMobileNumber())) { + throw new ValidationException("Mobile number already in use."); + } + + Address address = Address.builder() + .street(signupRequest.getAddress().getStreet()) + .city(signupRequest.getAddress().getCity()) + .state(signupRequest.getAddress().getState()) + .country(signupRequest.getAddress().getCountry()) + .postalCode(signupRequest.getAddress().getPostalCode()) + .build(); + User account = User.builder() .name(signupRequest.getName()) @@ -91,13 +103,21 @@ public void registerAccount(final SignupRequest signupRequest) { .password( passwordEncoder.encode( signupRequest.getPassword())) + .mobileNumber(signupRequest.getMobileNumber()) + .address(address) .build(); + address.setUser(account); + Role role = roleRepository.findByErole(ERole.ROLE_USER).orElse(null); checkNotNullValidationException(role, "Role_USER not found."); account.setRoles(Collections.singleton(role)); - userRepository.save(account); + account = userRepository.save(account); + + eventPublisher.publishEvent(EventsConstants.CUSTOMER_REGISTERED, + new CustomerRegisteredEvent(account.getId().toString(), + account.getName())); } public void logoutUser( @@ -124,21 +144,26 @@ public String refreshToken(final HttpServletRequest request, public void addUserDetailsInHeader( final HttpServletResponse response) { + + CustomUserDetails userDetails = getCurrentUserDetails(); + String email = userDetails.getUsername(); + StringBuilder roles = new StringBuilder(); + userDetails.getAuthorities().forEach((authority) -> { + if (!roles.isEmpty()) { + roles.append(", "); + } + roles.append(authority.getAuthority()); + }); + setRoleAndEmailInHeader(response, email, roles.toString(), + userDetails.getId().toString()); + } + + public static CustomUserDetails getCurrentUserDetails() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object principal = authentication.getPrincipal(); - if (principal instanceof UserDetails) { - UserDetails userDetails = (UserDetails) principal; - String email = userDetails.getUsername(); - StringBuilder roles = new StringBuilder(); - userDetails.getAuthorities().forEach((authority) -> { - if (!roles.isEmpty()) { - roles.append(", "); - } - roles.append(authority.getAuthority()); - }); - setRoleAndEmailInHeader(response, email, roles.toString()); + if (principal instanceof CustomUserDetails) { + return (CustomUserDetails) principal; } else { throw new InvalidActionException( "User details not saved correctly."); @@ -148,16 +173,11 @@ public void addUserDetailsInHeader( private void setRoleAndEmailInHeader( final HttpServletResponse response, final String email, - final String roles) { + final String roles, + final String id) { response.setHeader("X-User-Email", email); response.setHeader("X-User-Roles", roles); - } - - private void checkNotNullValidationException(final String value, - final String message) { - if (value == null || value.isEmpty()) { - throw new ValidationException(message); - } + response.setHeader("X-User-Id", id); } private void checkNotNullValidationException(final Object value, diff --git a/src/main/java/com/podzilla/auth/service/UserService.java b/src/main/java/com/podzilla/auth/service/UserService.java index 1b24454..95616eb 100644 --- a/src/main/java/com/podzilla/auth/service/UserService.java +++ b/src/main/java/com/podzilla/auth/service/UserService.java @@ -1,8 +1,15 @@ package com.podzilla.auth.service; +import com.podzilla.auth.dto.CustomUserDetails; +import com.podzilla.auth.dto.UpdateRequest; +import com.podzilla.auth.dto.UserDetailsRequest; import com.podzilla.auth.exception.NotFoundException; +import com.podzilla.auth.exception.ValidationException; +import com.podzilla.auth.model.Address; import com.podzilla.auth.model.User; +import com.podzilla.auth.repository.AddressRepository; import com.podzilla.auth.repository.UserRepository; +import com.podzilla.mq.events.DeliveryAddress; import jakarta.transaction.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,21 +24,98 @@ public class UserService { LoggerFactory.getLogger(UserService.class); private final UserRepository userRepository; + private final AddressRepository addressRepository; - public UserService(final UserRepository userRepository) { + public UserService(final UserRepository userRepository, + final AddressRepository addressRepository) { this.userRepository = userRepository; + this.addressRepository = addressRepository; } @Transactional - public void updateUserProfile(final UUID userId, final String name) { - User user = getUserOrThrow(userId); - LOGGER.debug("Updating name for userId={}", userId); - user.setName(name); - userRepository.save(user); - LOGGER.debug("User profile updated successfully for userId={}", userId); + public void updateUserProfile(final UpdateRequest updateRequest) { + LOGGER.debug("Updating user profile"); + CustomUserDetails customUserDetails = + AuthenticationService.getCurrentUserDetails(); + if (updateRequest.getName() != null + && !updateRequest.getName().isBlank()) { + User user = getUserOrThrow(customUserDetails.getId()); + LOGGER.debug("Updating user with id={}", user.getId()); + user.setName(updateRequest.getName()); + userRepository.save(user); + } + if (updateRequest.getMobileNumber() != null + && !updateRequest.getMobileNumber().isBlank() + && mobileNumberIsUnique(updateRequest.getMobileNumber())) { + User user = getUserOrThrow(customUserDetails.getId()); + LOGGER.debug("Updating mobile number for user with id={}", + user.getId()); + user.setMobileNumber(updateRequest.getMobileNumber()); + userRepository.save(user); + } + if (updateRequest.getAddress() != null + && isValidAddress(updateRequest.getAddress())) { + Address address = getAddressOrThrow( + customUserDetails.getId()); + LOGGER.debug("Updating address for user with id={}", + address.getUser().getId()); + address.setStreet(updateRequest.getAddress().getStreet()); + address.setCity(updateRequest.getAddress().getCity()); + address.setState(updateRequest.getAddress().getState()); + address.setCountry(updateRequest.getAddress().getCountry()); + address.setPostalCode(updateRequest.getAddress() + .getPostalCode()); + addressRepository.save(address); + } } + public UserDetailsRequest getUserDetails() { + CustomUserDetails customUserDetails = + AuthenticationService.getCurrentUserDetails(); + LOGGER.debug("Fetching user details for user with id={}", + customUserDetails.getId()); + User user = getUserOrThrow(customUserDetails.getId()); + DeliveryAddress address = new DeliveryAddress(); + address.setStreet(user.getAddress().getStreet()); + address.setCity(user.getAddress().getCity()); + address.setState(user.getAddress().getState()); + address.setCountry(user.getAddress().getCountry()); + address.setPostalCode(user.getAddress().getPostalCode()); + return UserDetailsRequest.builder() + .name(user.getName()) + .email(user.getEmail()) + .mobileNumber(user.getMobileNumber()) + .address(address) + .build(); + } + + private boolean isValidAddress(final DeliveryAddress address) { + if (address.getStreet() == null || address.getStreet().isBlank()) { + throw new ValidationException("Street is required"); + } + if (address.getCity() == null || address.getCity().isBlank()) { + throw new ValidationException("City is required"); + } + if (address.getState() == null || address.getState().isBlank()) { + throw new ValidationException("State is required"); + } + if (address.getCountry() == null || address.getCountry().isBlank()) { + throw new ValidationException("Country is required"); + } + if (address.getPostalCode() == null + || address.getPostalCode().isBlank()) { + throw new ValidationException("Postal code is required"); + } + return true; + } + + private boolean mobileNumberIsUnique(final String mobileNumber) { + if (userRepository.existsByMobileNumber(mobileNumber)) { + throw new ValidationException("Mobile number already exists"); + } + return true; + } public User getUserOrThrow(final UUID userId) { LOGGER.debug("Fetching user with id={}", userId); @@ -42,4 +126,15 @@ public User getUserOrThrow(final UUID userId) { + userId + " does not exist."); }); } + + public Address getAddressOrThrow(final UUID userId) { + LOGGER.debug("Fetching address for user with id={}", userId); + return addressRepository.findByUserId(userId) + .orElseThrow(() -> { + LOGGER.warn("Address not found for user with id={}", + userId); + return new NotFoundException("Address for user with id " + + userId + " does not exist."); + }); + } } diff --git a/src/test/java/com/podzilla/auth/controller/AuthenticationControllerTest.java b/src/test/java/com/podzilla/auth/controller/AuthenticationControllerTest.java index cc35333..31553e1 100644 --- a/src/test/java/com/podzilla/auth/controller/AuthenticationControllerTest.java +++ b/src/test/java/com/podzilla/auth/controller/AuthenticationControllerTest.java @@ -3,12 +3,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.podzilla.auth.dto.LoginRequest; import com.podzilla.auth.dto.SignupRequest; +import com.podzilla.auth.model.Address; import com.podzilla.auth.model.ERole; import com.podzilla.auth.model.Role; import com.podzilla.auth.model.User; +import com.podzilla.auth.repository.AddressRepository; import com.podzilla.auth.repository.RoleRepository; import com.podzilla.auth.repository.UserRepository; import com.podzilla.auth.service.TokenService; // Assuming you have a JwtService +import com.podzilla.mq.events.DeliveryAddress; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -66,11 +69,21 @@ void setUp() { userRole.setErole(ERole.ROLE_USER); roleRepository.save(userRole); + Address address = new Address(); + address.setStreet("123 Test St"); + address.setCity("Test City"); + address.setState("Test State"); + address.setCountry("Test Country"); + address.setPostalCode("12345"); + // Create a pre-existing user for login tests User user = new User(); user.setEmail(testUserEmail); user.setPassword(passwordEncoder.encode(testUserPassword)); user.setName("Test User"); // Assuming name is required or desired + user.setMobileNumber("1234567890"); + user.setAddress(address); + address.setUser(user); user.getRoles().add(userRole); userRepository.save(user); } @@ -87,6 +100,9 @@ void registerUser_shouldCreateNewUser_whenEmailIsNotTaken() throws Exception { signupRequest.setEmail("newuser@example.com"); signupRequest.setPassword("newpassword"); signupRequest.setName("New User"); + signupRequest.setMobileNumber("1234562137890"); + signupRequest.setAddress(new DeliveryAddress("456 New St", "New City", + "New State", "New Country", "54321")); mockMvc.perform(post("/auth/register") .contentType(MediaType.APPLICATION_JSON) diff --git a/src/test/java/com/podzilla/auth/service/AuthenticationServiceTest.java b/src/test/java/com/podzilla/auth/service/AuthenticationServiceTest.java index 0a1dd7b..b111cf2 100644 --- a/src/test/java/com/podzilla/auth/service/AuthenticationServiceTest.java +++ b/src/test/java/com/podzilla/auth/service/AuthenticationServiceTest.java @@ -4,11 +4,17 @@ import com.podzilla.auth.dto.LoginRequest; import com.podzilla.auth.dto.SignupRequest; import com.podzilla.auth.exception.ValidationException; +import com.podzilla.auth.model.Address; import com.podzilla.auth.model.ERole; import com.podzilla.auth.model.Role; import com.podzilla.auth.model.User; import com.podzilla.auth.repository.RoleRepository; import com.podzilla.auth.repository.UserRepository; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.BaseEvent; +import com.podzilla.mq.events.CustomerRegisteredEvent; +import com.podzilla.mq.events.DeliveryAddress; import jakarta.servlet.http.HttpServletRequest; // Added import import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; @@ -53,6 +59,8 @@ class AuthenticationServiceTest { private HttpServletResponse httpServletResponse; @Mock // Added mock for HttpServletRequest private HttpServletRequest httpServletRequest; + @Mock + private EventPublisher eventPublisher; @InjectMocks private AuthenticationService authenticationService; @@ -68,6 +76,14 @@ void setUp() { signupRequest.setName("Test User"); signupRequest.setEmail("test@example.com"); signupRequest.setPassword("password123"); + signupRequest.setMobileNumber("1234567890"); + DeliveryAddress deliveryAddress = new DeliveryAddress(); + deliveryAddress.setStreet("123 Test St"); + deliveryAddress.setCity("Test City"); + deliveryAddress.setState("Test State"); + deliveryAddress.setCountry("Test Country"); + deliveryAddress.setPostalCode("12345"); + signupRequest.setAddress(deliveryAddress); loginRequest = new LoginRequest(); loginRequest.setEmail("test@example.com"); @@ -94,6 +110,9 @@ void registerAccount_shouldSaveUser_whenEmailNotExistsAndPasswordNotEmpty() { when(passwordEncoder.encode(signupRequest.getPassword())).thenReturn("encodedPassword"); when(roleRepository.findByErole(ERole.ROLE_USER)).thenReturn(Optional.of(userRole)); when(userRepository.save(any(User.class))).thenReturn(user); // Return the saved user + // mock publish event in event publisher void method + doNothing().when(eventPublisher).publishEvent(any(EventsConstants.EventMetadata.class), + any(CustomerRegisteredEvent.class)); // Act authenticationService.registerAccount(signupRequest); @@ -132,24 +151,6 @@ void registerAccount_shouldThrowValidationException_whenEmailExists() { verify(userRepository, never()).save(any(User.class)); } - @Test - void registerAccount_shouldThrowValidationException_whenPasswordIsEmpty() { - // Arrange - signupRequest.setPassword(""); // Empty password - - // Act & Assert - ValidationException exception = assertThrows(ValidationException.class, () -> { - authenticationService.registerAccount(signupRequest); - }); - - assertEquals("Validation error: Password cannot be null.", - exception.getMessage()); - verify(userRepository, never()).existsByEmail(anyString()); - verify(passwordEncoder, never()).encode(anyString()); - verify(roleRepository, never()).findByErole(any()); - verify(userRepository, never()).save(any(User.class)); - } - @Test void registerAccount_shouldHandleRoleNotFoundGracefully() { // Arrange - Simulate role not found in DB diff --git a/src/test/java/com/podzilla/auth/service/UserServiceTest.java b/src/test/java/com/podzilla/auth/service/UserServiceTest.java deleted file mode 100644 index e6e5ba2..0000000 --- a/src/test/java/com/podzilla/auth/service/UserServiceTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.podzilla.auth.service; - -import com.podzilla.auth.exception.NotFoundException; -import com.podzilla.auth.model.User; -import com.podzilla.auth.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - - -import java.util.Optional; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class UserServiceTest { - - @Mock - private UserRepository userRepository; - - @InjectMocks - private UserService userService; - - private UUID userId; - private User existingUser; - - @BeforeEach - void setUp() { - userId = UUID.randomUUID(); - existingUser = User.builder() - .id(userId) - .name("Old Name") - .email("old@example.com") - .build(); - } - - @Test - void updateUserProfile_shouldUpdateName_whenUserExists() { - // Arrange - String newName = "New Name"; - when(userRepository.findById(userId)).thenReturn(Optional.of(existingUser)); - - // Act - userService.updateUserProfile(userId, newName); - - // Assert - verify(userRepository).findById(userId); - - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - verify(userRepository).save(userCaptor.capture()); - User savedUser = userCaptor.getValue(); - - assertEquals(newName, savedUser.getName()); - assertEquals(userId, savedUser.getId()); - } - - @Test - void updateUserProfile_shouldThrowNotFoundException_whenUserDoesNotExist() { - // Arrange - when(userRepository.findById(userId)).thenReturn(Optional.empty()); - - // Act & Assert - NotFoundException exception = assertThrows(NotFoundException.class, () -> { - userService.updateUserProfile(userId, "New Name"); - }); - - assertEquals("Not Found: User with id " + userId + " does not exist.", exception.getMessage()); - verify(userRepository).findById(userId); - verify(userRepository, never()).save(any(User.class)); - } - - -} \ No newline at end of file