UserController.java
package no.ntnu.idi.stud.savingsapp.controller.user;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import lombok.extern.slf4j.Slf4j;
import no.ntnu.idi.stud.savingsapp.dto.user.*;
import no.ntnu.idi.stud.savingsapp.exception.user.PermissionDeniedException;
import no.ntnu.idi.stud.savingsapp.model.configuration.ChallengeType;
import no.ntnu.idi.stud.savingsapp.model.configuration.Commitment;
import no.ntnu.idi.stud.savingsapp.model.configuration.Experience;
import no.ntnu.idi.stud.savingsapp.model.user.Feedback;
import no.ntnu.idi.stud.savingsapp.model.user.SearchFilter;
import no.ntnu.idi.stud.savingsapp.model.user.SubscriptionLevel;
import no.ntnu.idi.stud.savingsapp.model.user.User;
import no.ntnu.idi.stud.savingsapp.security.AuthIdentity;
import no.ntnu.idi.stud.savingsapp.service.UserService;
import no.ntnu.idi.stud.savingsapp.validation.Enumerator;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.ArrayList;
/**
* Controller handling user related requests.
*/
@CrossOrigin
@RestController
@Validated
@RequestMapping("/api/users")
@EnableAutoConfiguration
@Tag(name = "User")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private ModelMapper modelMapper;
/**
* Retrieves the authenticated user's data.
* @param identity The security context of the authenticated user.
* @return ResponseEntity containing the UserDTO of the authenticated user.
* @apiNote This endpoint is used to fetch all user information for the authenticated
* user. It uses the user's ID stored in the authentication principal to fetch the
* data.
*/
@Operation(summary = "Get the authenticated user",
description = "Get all user information for " + "the authenticated user")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully got user") })
@GetMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDTO> getUser(@AuthenticationPrincipal AuthIdentity identity) {
User user = userService.findById(identity.getId());
UserDTO userDTO = modelMapper.map(user, UserDTO.class);
log.info("[UserController:getUser] user: {}", userDTO.getId());
return ResponseEntity.ok(userDTO);
}
/**
* Retrieves the profile of a specific user by their unique identifier.
* @param userId The unique identifier of the user whose profile is to be retrieved.
* @return ResponseEntity containing the ProfileDTO of the requested user.
* @apiNote This endpoint fetches the profile of any user given their user ID. It is
* intended for public access where any authenticated user can view others' profiles.
*/
@Operation(summary = "Get a profile", description = "Get the profile of a user")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully got profile") })
@GetMapping(value = "/{userId}/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ProfileDTO> getProfile(@PathVariable long userId) {
User user = userService.findById(userId);
ProfileDTO profileDTO = modelMapper.map(user, ProfileDTO.class);
log.info("[UserController:getProfile] profile: {}", profileDTO.getId());
return ResponseEntity.ok(profileDTO);
}
/**
* Updates the profile of the authenticated user based on the provided data.
* @param identity The security context of the authenticated user.
* @param updateDTO The data transfer object containing the fields that need to be
* updated.
* @return ResponseEntity containing the updated UserDTO.
* @apiNote This endpoint allows the authenticated user to update their own profile.
* It only updates fields that are provided in the request body.
*/
@Operation(summary = "Update a profile", description = "Update the profile of the authenticated user")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully updated profile") })
@PatchMapping(produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDTO> update(@AuthenticationPrincipal AuthIdentity identity,
@RequestBody @Valid UserUpdateDTO updateDTO) {
User user = userService.findById(identity.getId());
if (updateDTO.getFirstName() != null) {
user.setFirstName(updateDTO.getFirstName());
}
if (updateDTO.getLastName() != null) {
user.setLastName(updateDTO.getLastName());
}
if (updateDTO.getEmail() != null) {
user.setEmail(updateDTO.getEmail());
}
if (updateDTO.getEmail() != null) {
user.setEmail(updateDTO.getEmail());
}
if (updateDTO.getProfileImage() != null) {
user.setProfileImage(updateDTO.getProfileImage());
}
if (updateDTO.getBannerImage() != null) {
user.setBannerImage(updateDTO.getBannerImage());
}
if (updateDTO.getCheckingAccountBBAN() != null) {
user.setCheckingAccountBBAN(updateDTO.getCheckingAccountBBAN());
}
if (updateDTO.getSavingsAccountBBAN() != null) {
user.setSavingsAccountBBAN(updateDTO.getSavingsAccountBBAN());
}
if (updateDTO.getConfiguration() != null) {
if (updateDTO.getConfiguration().getCommitment() != null) {
user.getConfiguration().setCommitment(Commitment.valueOf(updateDTO.getConfiguration().getCommitment()));
}
if (updateDTO.getConfiguration().getExperience() != null) {
user.getConfiguration().setExperience(Experience.valueOf(updateDTO.getConfiguration().getExperience()));
}
if (updateDTO.getConfiguration().getChallengeTypes() != null) {
for (String challengeType : updateDTO.getConfiguration().getChallengeTypes()) {
user.getConfiguration().getChallengeTypes().add(ChallengeType.valueOf(challengeType));
}
}
}
User updatedUser = userService.update(user);
UserDTO userDTO = modelMapper.map(updatedUser, UserDTO.class);
log.info("[UserController:update] updated user id: {}", identity.getId());
return ResponseEntity.ok(userDTO);
}
@Operation(summary = "Delete the authenticated user", description = "Delete the authenticated user")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully deleted user") })
@DeleteMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> deleteUser(@AuthenticationPrincipal AuthIdentity identity) {
userService.delete(identity.getId());
log.info("[UserController:deleteUser] user: {}", identity.getId());
return ResponseEntity.ok().build();
}
@Operation(summary = "Update a password", description = "Update the password of the authenticated user")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully updated password") })
@PatchMapping(value = "/password", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserDTO> updatePassword(@AuthenticationPrincipal AuthIdentity identity,
@RequestBody @Valid PasswordUpdateDTO updateDTO) {
User user = userService.updatePassword(identity.getId(), updateDTO.getOldPassword(),
updateDTO.getNewPassword());
UserDTO userDTO = modelMapper.map(user, UserDTO.class);
log.info("[UserController:updatedPassword] user id: {}", identity.getId());
return ResponseEntity.ok(userDTO);
}
/**
* Initiates a password reset process by sending a reset email to the user with the
* provided email. This endpoint is called when a user requests a password reset. It
* triggers an email with reset instructions.
* @param email The email address of the user requesting a password reset, which must
* be a valid email format.
* @throws IllegalArgumentException if the email address does not meet the email
* format validation.
*/
@Operation(summary = "Initiate a password reset",
description = "Send a password reset mail " + "to the user with the specified email")
@ApiResponses(
value = { @ApiResponse(responseCode = "202", description = "Successfully initiated a password reset") })
@SecurityRequirements
@PostMapping(value = "/reset-password", consumes = MediaType.TEXT_PLAIN_VALUE)
@ResponseStatus(value = HttpStatus.ACCEPTED)
public void resetPassword(@RequestBody @Email(message = "Invalid email") String email) {
userService.initiatePasswordReset(email);
log.info("[UserController:resetPassword] initiated password reset, email: {}", email);
}
/**
* Confirms a password reset using a provided token and a new password. This endpoint
* is called to finalize the password reset process. It uses the token sent to the
* user's email and a new password specified by the user to complete the password
* reset.
* @param resetDTO The PasswordResetDTO containing the reset token and the new
* password which must be valid.
* @throws jakarta.validation.ValidationException if the resetDTO does not pass
* validation checks.
*/
@Operation(summary = "Confirm a password reset",
description = "Confirms a password reset " + "using a token and a new password")
@ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Password was reset successfully"),
@ApiResponse(responseCode = "403", description = "Invalid token") })
@SecurityRequirements
@PostMapping(value = "/confirm-password")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void confirmPasswordReset(@RequestBody @Valid PasswordResetDTO resetDTO) {
userService.confirmPasswordReset(resetDTO.getToken(), resetDTO.getPassword());
log.info("[UserController:confirmPasswordReset] initiated password reset, token: {}", resetDTO.getToken());
}
@Operation(summary = "Search for users by name and filter",
description = "Returns a list of users whose names contain the specified search term and match the filter.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "Successfully retrieved list of users") })
@GetMapping("/search/{searchTerm}/{filter}")
public ResponseEntity<List<UserDTO>> getUsersByNameAndFilter(@AuthenticationPrincipal AuthIdentity identity,
@PathVariable String searchTerm,
@PathVariable @Enumerator(value = SearchFilter.class, message = "Invalid filter") String filter) {
log.info("[UserController:getUsersByNameAndFilter] searchTerm: {}, filter: {}", searchTerm, filter);
List<User> users = userService.getUsersByNameAndFilter(identity.getId(), searchTerm,
SearchFilter.valueOf(filter));
List<UserDTO> userDTOs = new ArrayList<>();
for (User user : users) {
UserDTO userDTO = modelMapper.map(user, UserDTO.class);
userDTOs.add(userDTO);
log.info("[UserController:getUsersByNameAndFilter] user: {}", userDTO.getId());
}
return ResponseEntity.ok(userDTOs);
}
/**
* Sends feedback from an email.
*/
@Operation(summary = "Send feedback", description = "Send feedback from an email.")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Success") })
@PostMapping("/send-feedback")
public ResponseEntity<Void> sendFeedback(@Validated @RequestBody FeedbackRequestDTO feedbackRequestDTO) {
userService.sendFeedback(feedbackRequestDTO.getEmail(), feedbackRequestDTO.getMessage());
log.info("[UserController:sendFeedback] feedback: {}", feedbackRequestDTO.getMessage());
return ResponseEntity.ok().build();
}
/**
* Get all feedback.
* @param identity The authenticated user's identity.
* @return A list containing all feedback.
*/
@Operation(summary = "Send feedback", description = "Send feedback from a user.")
@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Success") })
@GetMapping("/get-feedback")
public ResponseEntity<List<FeedbackResponseDTO>> getFeedback(@AuthenticationPrincipal AuthIdentity identity) {
if (!identity.getRole().equalsIgnoreCase("ADMIN")) {
log.error("[UserController:getFeedback] Permission denied, user role: {}", identity.getRole());
throw new PermissionDeniedException();
}
List<Feedback> feedbacks = userService.getFeedback();
List<FeedbackResponseDTO> feedbackResponseDTOS = feedbacks.stream()
.map(quiz -> modelMapper.map(quiz, FeedbackResponseDTO.class))
.toList();
for (FeedbackResponseDTO feedbackResponseDTO : feedbackResponseDTOS) {
log.info("[UserController:getFeedback] feedback: {}", feedbackResponseDTO.getId());
}
return ResponseEntity.ok(feedbackResponseDTOS);
}
@Operation(summary = "Get X amount of random users",
description = "Get X amount of random users that fit the filter")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "Successfully retrieved list of users"), })
@GetMapping("/search/random/{amount}/{filter}")
public ResponseEntity<List<UserDTO>> getRandomUsers(@AuthenticationPrincipal AuthIdentity identity,
@PathVariable int amount,
@PathVariable @Enumerator(value = SearchFilter.class, message = "Invalid filter") String filter) {
List<User> users = userService.getRandomUsers(identity.getId(), amount, SearchFilter.valueOf(filter));
List<UserDTO> userDTOs = new ArrayList<>();
for (User user : users) {
UserDTO userDTO = modelMapper.map(user, UserDTO.class);
userDTOs.add(userDTO);
log.info("[UserController:getRandomUsers] user: {}", userDTO.getId());
}
return ResponseEntity.ok(userDTOs);
}
@Operation(summary = "Update User Subscription Level",
description = "Updates the subscription level of the current user")
@ApiResponses(
value = { @ApiResponse(responseCode = "200", description = "Subscription level updated successfully") })
@PutMapping("/subscription/{subscriptionLevel}")
public ResponseEntity<?> updateSubscriptionLevel(@AuthenticationPrincipal AuthIdentity identity,
@PathVariable @Enumerator(value = SubscriptionLevel.class,
message = "Invalid subscription level") String subscriptionLevel) {
userService.updateSubscriptionLevel(identity.getId(), SubscriptionLevel.valueOf(subscriptionLevel));
log.info("[UserController:updateSubscriptionLevel] subscription level: {}", subscriptionLevel);
return ResponseEntity.ok().build();
}
}