From 67524a7dee7c285126947f3ce887d4124c54200d Mon Sep 17 00:00:00 2001 From: Atif Siddiqui <51026543+Atifsid@users.noreply.github.com> Date: Thu, 16 May 2024 23:21:34 +0530 Subject: [PATCH] feat: create approve/reject endorsement (#116) * feat: Create updateEndorsementStatus API * feat: Implement role restrictions in updateEndorsementStatus API * feat: improve logic for updateEndorsementStatus API * fix: switch to InsufficientAuthenticationException (401, Unauthorized) if user in not superuser * chore: update return message for updateEndorsementStatus * temp: validate endorsementId for updateEndorsementStatus UUID.fromString does not throw exception in case of "1-1-1-1-1". Hence temporarily validating it until #109 is merged. * chore: update switch to string.equals() to compare endorsement status * chore: update exceptions for update-endorsement-status * fix: switch to AccessDeniedException to handle Unauthorized * chore: move isValidUUID out of UUIDValidationInterceptor * fix: remove unnecessary RuntimeException * chore: update api-contracts * fix: match endorsementId with the DB * chore: handle endorsement status already updated case and handle invalid status * fix: remove unnecessary validations * fix: simply endorsement status check AS we are checking for invalid status via `EndorsementStatus.fromString()` we can simply condition to check for `PENDING` status * fix: api-contracts --- api-contracts.yml | 81 ++++++++++++++++--- .../Endorsement/EndorsementController.java | 6 ++ .../Endorsement/EndorsementService.java | 3 + .../Endorsement/EndorsementServiceImpl.java | 38 +++++++++ .../Endorsement/EndorsementStatus.java | 20 ++++- .../com/RDS/skilltree/utils/CommonUtils.java | 14 ++++ .../utils/UUIDValidationInterceptor.java | 13 +-- 7 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java diff --git a/api-contracts.yml b/api-contracts.yml index c30cd1b6..cf87436a 100644 --- a/api-contracts.yml +++ b/api-contracts.yml @@ -227,24 +227,25 @@ paths: - /endorsements/{endorsementId}: + /endorsements/{id}?status={EndorsementStatus}: patch: tags: - endorsements parameters: - - name: endorsementId + - name: id schema: type: string + format: UUID in: path description: EndorsementId which has to be modified required: true - requestBody: - required: true - content: - application/json: - schema: - type: object - $ref: '#/components/schemas/endorsementUpdate' + - name: status + schema: + type: string, + format: EndorsementStatus, + in: query + description: New endorsement status which has to be updated + required: true summary: Update endorsement status given endorsementId description: Update endorsement status given endorsement id, **this can be only used by Super User for now** operationId: updateEndorsementStatusGivenId @@ -256,8 +257,7 @@ paths: application/json: schema: type: object - $ref: '#/components/schemas/endorsementResponse' - + $ref: '#/components/schemas/UpdateEndorsementResponse' '400': description: Invalid endorsement Id value @@ -267,6 +267,30 @@ paths: type: object $ref: '#/components/schemas/ApiResponseFailureInvalidParameter' + '400': + description: Invalid endorsement status + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ApiResponseFailureInvalidParameter' + + '409': + description: Updating the status of an endorsement that has been accepted/rejected + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ApiResponseFailureEndorsementUpdateResponse' + + '403': + description: Unauthorized access, user is not a super user + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ApiResponseFailureUnauthorized' + '503': description: Service Unavailable content: @@ -580,4 +604,37 @@ components: ApiResponseForSkillName: type: array items: - $ref: '#/components/schemas/skillUsersResponse' \ No newline at end of file + $ref: '#/components/schemas/skillUsersResponse' + + UpdateEndorsementResponse: + type: object + properties: + code: + type: integer + format: int32 + example: 200 + message: + type: string + example: Successfully updated endorsement status + + ApiResponseFailureUnauthorized: + type: object + properties: + code: + type: integer + format: int32 + example: 403 + message: + type: string + example: Unauthorized, Access is only available to super users + + ApiResponseFailureEndorsementUpdateResponse: + type: object + properties: + code: + type: integer + format: int32 + example: 409 + message: + type: string + example: Endorsement is already updated, Cannot modify status \ No newline at end of file diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java index 06e054f9..7b154118 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementController.java @@ -79,4 +79,10 @@ public ResponseEntity> postEndorsement( new GenericResponse(null, "Failed to create endorsement"), HttpStatus.BAD_REQUEST); } + + @PatchMapping(value = "/{id}") + public ResponseEntity> updateEndorsementStatus( + @PathVariable(value = "id") UUID id, @RequestParam String status) { + return ResponseEntity.ok().body(endorsementService.updateEndorsementStatus(id, status)); + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java index e599ceaa..a7254a48 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementService.java @@ -1,5 +1,6 @@ package com.RDS.skilltree.Endorsement; +import com.RDS.skilltree.Common.Response.GenericResponse; import java.io.IOException; import java.util.UUID; import org.springframework.data.domain.Page; @@ -15,4 +16,6 @@ Page getEndorsementsFromDummyData( PageRequest pageRequest, String skillID, String userID) throws IOException; EndorsementModel createEndorsement(EndorsementDRO endorsementDRO); + + GenericResponse updateEndorsementStatus(UUID id, String status); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java index 740fa8f3..dc380c51 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementServiceImpl.java @@ -1,10 +1,14 @@ package com.RDS.skilltree.Endorsement; +import com.RDS.skilltree.Common.Response.GenericResponse; +import com.RDS.skilltree.Exceptions.EntityAlreadyExistsException; +import com.RDS.skilltree.Exceptions.InvalidParameterException; import com.RDS.skilltree.Exceptions.NoEntityException; import com.RDS.skilltree.Skill.SkillModel; import com.RDS.skilltree.Skill.SkillRepository; import com.RDS.skilltree.User.UserModel; import com.RDS.skilltree.User.UserRepository; +import com.RDS.skilltree.User.UserRole; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; @@ -21,6 +25,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @Service @@ -128,4 +134,36 @@ public EndorsementModel createEndorsement(EndorsementDRO endorsementDRO) { throw new NoEntityException("Skill with id:" + skillId + " not found"); } } + + @Override + public GenericResponse updateEndorsementStatus(UUID id, String status) { + UserModel user = + (UserModel) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (!user.getRole().equals(UserRole.SUPERUSER)) { + throw new AccessDeniedException("Unauthorized, Access is only available to super users"); + } + + EndorsementStatus endorsementStatus = EndorsementStatus.fromString(status); + if (endorsementStatus.equals(EndorsementStatus.PENDING)) { + throw new InvalidParameterException("endorsement status", status); + } + Optional optionalEndorsementModel = endorsementRepository.findById(id); + if (optionalEndorsementModel.isPresent()) { + if (optionalEndorsementModel.get().getStatus() != EndorsementStatus.PENDING) { + throw new EntityAlreadyExistsException( + "Endorsement is already updated. Cannot modify status"); + } + EndorsementModel updatedEndorsementModel = + EndorsementModel.builder() + .id(optionalEndorsementModel.get().getId()) + .user(optionalEndorsementModel.get().getUser()) + .skill(optionalEndorsementModel.get().getSkill()) + .endorsersList(optionalEndorsementModel.get().getEndorsersList()) + .status(EndorsementStatus.valueOf(status)) + .build(); + endorsementRepository.save(updatedEndorsementModel); + return new GenericResponse<>(null, "Successfully updated endorsement status"); + } + throw new NoEntityException("No endorsement with id " + id + " was found"); + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java index 06407ca2..9300b618 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/Endorsement/EndorsementStatus.java @@ -1,7 +1,25 @@ package com.RDS.skilltree.Endorsement; +import com.RDS.skilltree.Exceptions.InvalidParameterException; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + public enum EndorsementStatus { APPROVED, PENDING, - REJECTED + REJECTED; + + private static final Map endorsementStatusMap = + Stream.of(values()) + .collect(Collectors.toMap(EndorsementStatus::toString, Function.identity())); + + public static EndorsementStatus fromString(final String name) { + EndorsementStatus endorsementStatus = endorsementStatusMap.get(name); + if (endorsementStatus == null) { + throw new InvalidParameterException("endorsement status", name); + } + return endorsementStatus; + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java new file mode 100644 index 00000000..ac810b76 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java @@ -0,0 +1,14 @@ +package com.RDS.skilltree.utils; + +import java.util.regex.Pattern; + +public class CommonUtils { + private static final Pattern UUID_REGEX = + Pattern.compile( + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + + public static boolean isValidUUID(String uuidString) { + + return UUID_REGEX.matcher(uuidString).matches(); + } +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java index d5a4aa19..cccc1d5b 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/UUIDValidationInterceptor.java @@ -3,7 +3,6 @@ import com.RDS.skilltree.Exceptions.InvalidParameterException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -13,9 +12,6 @@ public class UUIDValidationInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(UUIDValidationInterceptor.class); - private static final Pattern UUID_REGEX = - Pattern.compile( - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); @Override public boolean preHandle( @@ -23,19 +19,14 @@ public boolean preHandle( String skillID = request.getParameter("skillID"); String userID = request.getParameter("userID"); - if (skillID != null && !skillID.isEmpty() && !isValidUUID(skillID)) { + if (skillID != null && !skillID.isEmpty() && !CommonUtils.isValidUUID(skillID)) { throw new InvalidParameterException("skillID", "Invalid UUID format"); } - if (userID != null && !userID.isEmpty() && !isValidUUID(userID)) { + if (userID != null && !userID.isEmpty() && !CommonUtils.isValidUUID(userID)) { throw new InvalidParameterException("userID", "Invalid UUID format"); } return true; } - - private boolean isValidUUID(String uuidString) { - - return UUID_REGEX.matcher(uuidString).matches(); - } }