Skip to content

Commit

Permalink
feat: create approve/reject endorsement (#116)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Atifsid authored May 16, 2024
1 parent 88fe526 commit 67524a7
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 24 deletions.
81 changes: 69 additions & 12 deletions api-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -256,8 +257,7 @@ paths:
application/json:
schema:
type: object
$ref: '#/components/schemas/endorsementResponse'

$ref: '#/components/schemas/UpdateEndorsementResponse'

'400':
description: Invalid endorsement Id value
Expand All @@ -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:
Expand Down Expand Up @@ -580,4 +604,37 @@ components:
ApiResponseForSkillName:
type: array
items:
$ref: '#/components/schemas/skillUsersResponse'
$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
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,10 @@ public ResponseEntity<GenericResponse<EndorsementDTO>> postEndorsement(
new GenericResponse<EndorsementDTO>(null, "Failed to create endorsement"),
HttpStatus.BAD_REQUEST);
}

@PatchMapping(value = "/{id}")
public ResponseEntity<GenericResponse<Void>> updateEndorsementStatus(
@PathVariable(value = "id") UUID id, @RequestParam String status) {
return ResponseEntity.ok().body(endorsementService.updateEndorsementStatus(id, status));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,4 +16,6 @@ Page<EndorsementModelFromJSON> getEndorsementsFromDummyData(
PageRequest pageRequest, String skillID, String userID) throws IOException;

EndorsementModel createEndorsement(EndorsementDRO endorsementDRO);

GenericResponse<Void> updateEndorsementStatus(UUID id, String status);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -128,4 +134,36 @@ public EndorsementModel createEndorsement(EndorsementDRO endorsementDRO) {
throw new NoEntityException("Skill with id:" + skillId + " not found");
}
}

@Override
public GenericResponse<Void> 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<EndorsementModel> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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<String, EndorsementStatus> 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;
}
}
14 changes: 14 additions & 0 deletions skill-tree/src/main/java/com/RDS/skilltree/utils/CommonUtils.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,29 +12,21 @@
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(
HttpServletRequest request, HttpServletResponse response, Object handler) {
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();
}
}

0 comments on commit 67524a7

Please sign in to comment.