diff --git a/builder-api/pom.xml b/builder-api/pom.xml index d0cf3dd8..6e7daaed 100644 --- a/builder-api/pom.xml +++ b/builder-api/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 org.acme builder-api @@ -50,6 +52,10 @@ io.quarkus quarkus-arc + + io.quarkus + quarkus-hibernate-validator + io.quarkiverse.googlecloudservices quarkus-google-cloud-firebase-admin @@ -168,7 +174,8 @@ - ${project.build.directory}/${project.build.finalName}-runner + + ${project.build.directory}/${project.build.finalName}-runner org.jboss.logmanager.LogManager ${maven.home} @@ -191,4 +198,4 @@ - + \ No newline at end of file diff --git a/builder-api/src/main/java/org/acme/api/error/ApiError.java b/builder-api/src/main/java/org/acme/api/error/ApiError.java new file mode 100644 index 00000000..7a6fba48 --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/error/ApiError.java @@ -0,0 +1,7 @@ +package org.acme.api.error; + +public record ApiError(boolean error, String message) { + public static ApiError of(String message) { + return new ApiError(true, message); + } +} diff --git a/builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java b/builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java new file mode 100644 index 00000000..85fe6cdc --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/error/JsonServerExceptionMappers.java @@ -0,0 +1,60 @@ +package org.acme.api.error; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +public class JsonServerExceptionMappers { + + // @ServerExceptionMapper + // public Response map(UnrecognizedPropertyException e) { + // return Response.status(Response.Status.BAD_REQUEST) + // .type(MediaType.APPLICATION_JSON) + // .entity(Map.of("error", true, "message", "Unknown fields " + e.getPropertyName())) + // .build(); + // } + + @ServerExceptionMapper + public Response map(MismatchedInputException e) { + // e.g. screenerName is object but DTO expects String + String field = + e.getPath() != null && !e.getPath().isEmpty() + ? e.getPath().get(e.getPath().size() - 1).getFieldName() + : "request body"; + + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of("Invalid type for field '" + field + "'.")) + .build(); + } + + @ServerExceptionMapper + public Response map(JsonParseException e) { + // malformed JSON like { "schema": } + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of("Malformed JSON.")) + .build(); + } + + @ServerExceptionMapper + public Response map(WebApplicationException e) { + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of("Malformed JSON.")) + .build(); + } + + @ServerExceptionMapper + public Response map(JsonMappingException e) { + // other mapping errors + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of("Invalid request body.")) + .build(); + } +} diff --git a/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java b/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java new file mode 100644 index 00000000..877d5f51 --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/error/UnknownFieldExceptionMapper.java @@ -0,0 +1,21 @@ +package org.acme.api.error; + +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +// Global error handler for providing extra fields in the request body + +@Provider +public class UnknownFieldExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(UnrecognizedPropertyException e) { + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of("Unknown field '" + e.getPropertyName() + "'")) + .build(); + } +} diff --git a/builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java b/builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java new file mode 100644 index 00000000..f9631fc8 --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/error/ValidationExceptionMapper.java @@ -0,0 +1,45 @@ +package org.acme.api.error; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +@Provider +public class ValidationExceptionMapper implements ExceptionMapper { + + private static final Logger LOG = Logger.getLogger(ValidationExceptionMapper.class); + + @Override + public Response toResponse(ConstraintViolationException e) { + // Log all violations (since endpoint method won't run) + String detail = + e.getConstraintViolations().stream() + .map( + v -> + v.getPropertyPath() + + ": " + + v.getMessage() + + " (invalid=" + + String.valueOf(v.getInvalidValue()) + + ")") + .collect(Collectors.joining("; ")); + + LOG.warn("Validation failed: " + detail); + + String msg = + e.getConstraintViolations().stream() + .findFirst() + .map(ConstraintViolation::getMessage) + .orElse("Validation failed."); + + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .entity(ApiError.of(msg)) + .build(); + } +} diff --git a/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java new file mode 100644 index 00000000..a5d3c76c --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvided.java @@ -0,0 +1,18 @@ +package org.acme.api.validation; + +import jakarta.validation.*; +import java.lang.annotation.*; +import java.lang.annotation.Retention; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = AtLeastOneProvidedValidator.class) +public @interface AtLeastOneProvided { + String message() default "At least one field must be provided"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String[] fields(); // names of fields to check +} diff --git a/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java new file mode 100644 index 00000000..8d807f14 --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/validation/AtLeastOneProvidedValidator.java @@ -0,0 +1,42 @@ +package org.acme.api.validation; + +import jakarta.validation.*; +import java.lang.reflect.Method; +import java.util.Map; + +public class AtLeastOneProvidedValidator + implements ConstraintValidator { + + private String[] fields; + + @Override + public void initialize(AtLeastOneProvided ann) { + this.fields = ann.fields(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext ctx) { + if (value == null) return true; + + Class cls = value.getClass(); + + for (String fieldName : fields) { + try { + // Works for records + POJOs + Method accessor = cls.getMethod(fieldName); + Object fieldValue = accessor.invoke(value); + + if (fieldValue instanceof String s && !s.isBlank()) return true; + if (fieldValue instanceof Map m && !m.isEmpty()) return true; + if (fieldValue != null) return true; + + } catch (NoSuchMethodException e) { + throw new IllegalStateException("No accessor '" + fieldName + "()' on " + cls.getName(), e); + } catch (Exception e) { + return false; + } + } + + return false; + } +} diff --git a/builder-api/src/main/java/org/acme/api/validation/HasSchema.java b/builder-api/src/main/java/org/acme/api/validation/HasSchema.java new file mode 100644 index 00000000..7ce91126 --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/validation/HasSchema.java @@ -0,0 +1,7 @@ +package org.acme.api.validation; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface HasSchema { + JsonNode schema(); +} diff --git a/builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java b/builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java new file mode 100644 index 00000000..2ef37651 --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/validation/SchemaValidator.java @@ -0,0 +1,45 @@ +package org.acme.api.validation; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class SchemaValidator implements ConstraintValidator { + + private boolean required; + private boolean mustBeObject; + + @Override + public void initialize(ValidSchema ann) { + this.required = ann.required(); + this.mustBeObject = ann.mustBeObject(); + } + + @Override + public boolean isValid(HasSchema value, ConstraintValidatorContext ctx) { + if (value == null) return true; + + JsonNode schema = value.schema(); + + // Priority 1: required check (null or JSON null) + if (required && (schema == null || schema.isNull())) { + return fail(ctx, "schema cannot be null", "schema"); + } + + // If not required and absent, it's valid + if (schema == null || schema.isNull()) return true; + + // Priority 2: type check + if (mustBeObject && !schema.isObject()) { + return fail(ctx, "schema must be a JSON object", "schema"); + } + + return true; + } + + private static boolean fail(ConstraintValidatorContext ctx, String msg, String node) { + ctx.disableDefaultConstraintViolation(); + ctx.buildConstraintViolationWithTemplate(msg).addPropertyNode(node).addConstraintViolation(); + return false; + } +} diff --git a/builder-api/src/main/java/org/acme/api/validation/ValidSchema.java b/builder-api/src/main/java/org/acme/api/validation/ValidSchema.java new file mode 100644 index 00000000..c34c9dee --- /dev/null +++ b/builder-api/src/main/java/org/acme/api/validation/ValidSchema.java @@ -0,0 +1,21 @@ +package org.acme.api.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = SchemaValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidSchema { + String message() default "Invalid schema"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + boolean required() default true; // reject null/NullNode + + boolean mustBeObject() default true; // reject non-object +} diff --git a/builder-api/src/main/java/org/acme/controller/ScreenerResource.java b/builder-api/src/main/java/org/acme/controller/ScreenerResource.java index 9e42abb8..022e15bc 100644 --- a/builder-api/src/main/java/org/acme/controller/ScreenerResource.java +++ b/builder-api/src/main/java/org/acme/controller/ScreenerResource.java @@ -1,437 +1,491 @@ package org.acme.controller; -import com.fasterxml.jackson.databind.JsonNode; import io.quarkus.logging.Log; import io.quarkus.security.identity.SecurityIdentity; import jakarta.annotation.security.PermitAll; import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotBlank; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import org.acme.auth.AuthUtils; import org.acme.model.domain.*; import org.acme.model.dto.PublishScreenerRequest; import org.acme.model.dto.SaveSchemaRequest; +import org.acme.model.dto.Screener.CreateScreenerRequest; +import org.acme.model.dto.Screener.EditScreenerRequest; import org.acme.persistence.EligibilityCheckRepository; -import org.acme.persistence.ScreenerRepository; import org.acme.persistence.PublishedScreenerRepository; +import org.acme.persistence.ScreenerRepository; import org.acme.persistence.StorageService; import org.acme.service.DmnService; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - @Path("/api") public class ScreenerResource { - @Inject - ScreenerRepository screenerRepository; + @Inject Validator validator; - @Inject - PublishedScreenerRepository publishedScreenerRepository; + @Inject ScreenerRepository screenerRepository; - @Inject - EligibilityCheckRepository eligibilityCheckRepository; + @Inject PublishedScreenerRepository publishedScreenerRepository; - @Inject - StorageService storageService; + @Inject EligibilityCheckRepository eligibilityCheckRepository; - @Inject - DmnService dmnService; + @Inject StorageService storageService; - @GET - @Path("/screeners") - public Response getScreeners(@Context SecurityIdentity identity) { - String userId = AuthUtils.getUserId(identity); - if (userId == null){ - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - Log.info("Fetching screeners for user: " + userId); - List screeners = screenerRepository.getWorkingScreeners(userId); + @Inject DmnService dmnService; - return Response.ok(screeners, MediaType.APPLICATION_JSON).build(); + @GET + @Path("/screeners") + public Response getScreeners(@Context SecurityIdentity identity) { + String userId = AuthUtils.getUserId(identity); + if (userId == null) { + return Response.status(Response.Status.UNAUTHORIZED).build(); } + Log.info("Fetching screeners for user: " + userId); + List screeners = screenerRepository.getWorkingScreeners(userId); - @GET - @Path("/screener/{screenerId}") - public Response getScreener(@Context SecurityIdentity identity, @PathParam("screenerId") String screenerId) { - String userId = AuthUtils.getUserId(identity); - Log.info("Fetching screener " + screenerId + " for user " + userId); + return Response.ok(screeners, MediaType.APPLICATION_JSON).build(); + } - Optional screenerOptional = screenerRepository.getWorkingScreener(screenerId); + @GET + @Path("/screener/{screenerId}") + public Response getScreener( + @Context SecurityIdentity identity, @PathParam("screenerId") String screenerId) { + String userId = AuthUtils.getUserId(identity); + Log.info("Fetching screener " + screenerId + " for user " + userId); - if (screenerOptional.isEmpty()){ - throw new NotFoundException(); - } + Optional screenerOptional = screenerRepository.getWorkingScreener(screenerId); - Screener screener = screenerOptional.get(); - if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { - return Response.status(Response.Status.UNAUTHORIZED).build(); - } + if (screenerOptional.isEmpty()) { + throw new NotFoundException(); + } - return Response.ok(screener, MediaType.APPLICATION_JSON).build(); + Screener screener = screenerOptional.get(); + if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); } - @GET - @Path("/published/screener/{screenerId}") - @PermitAll // This endpoint is accessible without authentication - public Response getPublishedScreener(@PathParam("screenerId") String screenerId) { - Optional screenerOptional = publishedScreenerRepository.getScreener(screenerId); - if (screenerOptional.isEmpty()){ - throw new NotFoundException(); - } + return Response.ok(screener, MediaType.APPLICATION_JSON).build(); + } - return Response.ok(screenerOptional.get(), MediaType.APPLICATION_JSON).build(); + @GET + @Path("/published/screener/{screenerId}") + @PermitAll // This endpoint is accessible without authentication + public Response getPublishedScreener(@PathParam("screenerId") String screenerId) { + Optional screenerOptional = publishedScreenerRepository.getScreener(screenerId); + if (screenerOptional.isEmpty()) { + throw new NotFoundException(); } - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Path("/screener") - public Response postScreener(@Context SecurityIdentity identity, Screener newScreener){ - String userId = AuthUtils.getUserId(identity); - - newScreener.setOwnerId(userId); - try { - String screenerId = screenerRepository.saveNewWorkingScreener(newScreener); - newScreener.setId(screenerId); - return Response.ok(newScreener, MediaType.APPLICATION_JSON).build(); - } catch (Exception e){ - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not save Screener")) - .build(); - } + return Response.ok(screenerOptional.get(), MediaType.APPLICATION_JSON).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/screener") + public Response postScreener( + @Context SecurityIdentity identity, @Valid CreateScreenerRequest request) { + String userId = AuthUtils.getUserId(identity); + + Screener newScreener = Screener.create(userId, request.screenerName(), request.description()); + + try { + String screenerId = screenerRepository.saveNewWorkingScreener(newScreener); + newScreener.setId(screenerId); + return Response.ok(newScreener, MediaType.APPLICATION_JSON).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not save Screener")) + .build(); + } + } + + @PATCH + @Consumes(MediaType.APPLICATION_JSON) + @Path("/screener/{screenerId}") + public Response updateScreener( + @Context SecurityIdentity identity, + @PathParam("screenerId") String screenerId, + @Valid EditScreenerRequest request) { + String userId = AuthUtils.getUserId(identity); + + // Fetch Screener record and confirm user is authorized + Optional maybeScreener = screenerRepository.getWorkingScreener(screenerId); + if (maybeScreener.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); } - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Path("/screener") - public Response updateScreener(@Context SecurityIdentity identity, Screener screener){ - String userId = AuthUtils.getUserId(identity); - if (!isUserAuthorizedToAccessScreener(userId, screener.getId())) return Response.status(Response.Status.UNAUTHORIZED).build(); - - //add user info to the update data - screener.setOwnerId(userId); - - try { - screenerRepository.updateWorkingScreener(screener); - - return Response.ok().build(); - } catch (Exception e){ - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not update Screener")) - .build(); - } + Screener screener = maybeScreener.get(); + if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); } - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Path("/save-form-schema") - public Response saveFormSchema(@Context SecurityIdentity identity, SaveSchemaRequest saveSchemaRequest){ - - String screenerId = saveSchemaRequest.screenerId; - if (screenerId == null || screenerId.isBlank()){ - return Response.status(Response.Status.BAD_REQUEST) - .entity("Error: Missing required required data in request body: screenerId") - .build(); - } - - String userId = AuthUtils.getUserId(identity); - if (!isUserAuthorizedToAccessScreener(userId, saveSchemaRequest.screenerId)) return Response.status(Response.Status.UNAUTHORIZED).build(); - - JsonNode schema = saveSchemaRequest.schema; - if (schema == null){ - return Response.status(Response.Status.BAD_REQUEST) - .entity("Error: Missing required required data in request body: screenerId") - .build(); - } - try { - String filePath = storageService.getScreenerWorkingFormSchemaPath(screenerId); - storageService.writeJsonToStorage(filePath, schema); - Log.info("Saved form schema of screener " + screenerId + " to storage"); - return Response.ok().build(); - } catch (Exception e){ - Log.info(("Failed to save form for screener " + screenerId)); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } + Log.info(request.toString()); + + // Update Screener fields from request + if (request.screenerName() != null) { + screener.setScreenerName(request.screenerName()); } - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Path("/publish") - public Response publishScreener(@Context SecurityIdentity identity, PublishScreenerRequest publishScreenerRequest){ - - String screenerId = publishScreenerRequest.screenerId; - if (screenerId == null || screenerId.isBlank()){ - return Response.status(Response.Status.BAD_REQUEST) - .entity("Error: Missing required query parameter: screenerId") - .build(); - } - - String userId = AuthUtils.getUserId(identity); - if (!isUserAuthorizedToAccessScreener(userId, screenerId)) return Response.status(Response.Status.UNAUTHORIZED).build(); - - try { - Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); - if (screenerOpt.isEmpty()) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - Screener screener = screenerOpt.get(); - screenerRepository.publishScreener(screener); - return Response.ok().build(); - } catch (Exception e) { - Log.error("Error: Error updating screener to published. Screener: " + screenerId); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } + try { + screenerRepository.updateWorkingScreener(screener); + return Response.ok(screener, MediaType.APPLICATION_JSON).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not update Screener")) + .build(); + } + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/save-form-schema") + public Response saveFormSchema( + @Context SecurityIdentity identity, + @QueryParam("screenerId") @NotBlank(message = "Must provide screenerId") String screenerId, + @Valid SaveSchemaRequest request) { + + Log.info( + "schema node = " + + (request == null + ? "request=null" + : request.schema() == null + ? "schema=null" + : request.schema().getNodeType() + " : " + request.schema().toString())); + + var violations = validator.validate(request); + if (!violations.isEmpty()) { + return Response.status(400).entity(violations.toString()).build(); } - @DELETE - @Path("/screener/delete") - public Response deleteScreener(@Context SecurityIdentity identity, @QueryParam("screenerId") String screenerId){ - if (screenerId == null || screenerId.isBlank()){ - return Response.status(Response.Status.BAD_REQUEST) - .entity("Error: Missing required query parameter: screenerId") - .build(); - } - - String userId = AuthUtils.getUserId(identity); - if (!isUserAuthorizedToAccessScreener(userId, screenerId)) return Response.status(Response.Status.UNAUTHORIZED).build(); - - try { - screenerRepository.deleteWorkingScreener(screenerId); - return Response.ok().build(); - } catch (Exception e){ - Log.error("Error: error deleting screener " + screenerId); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } + // // Make sure request.schema is not null + // if (request.schema() == null || request.schema().isNull()) { + // return Response.status(Response.Status.BAD_REQUEST) + // .entity(ApiError.of("schema cannot be null.")) + // .build(); + // } + + String userId = AuthUtils.getUserId(identity); + + // Fetch Screener record and confirm user is authorized + Optional maybeScreener = screenerRepository.getWorkingScreener(screenerId); + if (maybeScreener.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", true, "message", "Screener " + screenerId + " cannot be found.")) + .build(); } - private boolean isUserAuthorizedToAccessScreener(String userId, String screenerId) { - Optional screenerOptional = screenerRepository.getWorkingScreenerMetaDataOnly(screenerId); - if (screenerOptional.isEmpty()){ - return false; - } - Screener screener = screenerOptional.get(); - return isUserAuthorizedToAccessScreenerByScreener(userId, screener); + Screener screener = maybeScreener.get(); + if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED) + .entity(Map.of("error", true, "message", "Unauthorized access to the screener.")) + .build(); } - private boolean isUserAuthorizedToAccessScreenerByScreener(String userId, Screener screener) { - String ownerId = screener.getOwnerId(); - if (userId.equals(ownerId)){ - return true; - } - return false; + try { + String filePath = storageService.getScreenerWorkingFormSchemaPath(screenerId); + storageService.writeJsonToStorage(filePath, request.schema()); + return Response.ok().build(); + } catch (Exception e) { + Log.info(("Failed to save form for screener " + screenerId)); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/publish") + public Response publishScreener( + @Context SecurityIdentity identity, PublishScreenerRequest publishScreenerRequest) { + + String screenerId = publishScreenerRequest.screenerId; + if (screenerId == null || screenerId.isBlank()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Error: Missing required query parameter: screenerId") + .build(); + } + + String userId = AuthUtils.getUserId(identity); + if (!isUserAuthorizedToAccessScreener(userId, screenerId)) + return Response.status(Response.Status.UNAUTHORIZED).build(); + + try { + Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); + if (screenerOpt.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + Screener screener = screenerOpt.get(); + screenerRepository.publishScreener(screener); + return Response.ok().build(); + } catch (Exception e) { + Log.error("Error: Error updating screener to published. Screener: " + screenerId); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + @DELETE + @Path("/screener/delete") + public Response deleteScreener( + @Context SecurityIdentity identity, @QueryParam("screenerId") String screenerId) { + if (screenerId == null || screenerId.isBlank()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Error: Missing required query parameter: screenerId") + .build(); + } + + String userId = AuthUtils.getUserId(identity); + if (!isUserAuthorizedToAccessScreener(userId, screenerId)) + return Response.status(Response.Status.UNAUTHORIZED).build(); + + try { + screenerRepository.deleteWorkingScreener(screenerId); + return Response.ok().build(); + } catch (Exception e) { + Log.error("Error: error deleting screener " + screenerId); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } - @GET - @Path("/screener/{screenerId}/benefit") - public Response getScreenerBenefits(@Context SecurityIdentity identity, - @PathParam("screenerId") String screenerId){ - String userId = AuthUtils.getUserId(identity); - - Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); - if (screenerOpt.isEmpty()){ - throw new NotFoundException(); - } - Screener screener = screenerOpt.get(); - - if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)){ - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - - try{ - List benefits = screenerRepository.getBenefitsInScreener(screener); - return Response.ok().entity(benefits).build(); - } catch (Exception e){ - Log.error(e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not fetch benefits")) - .build(); - } + private boolean isUserAuthorizedToAccessScreener(String userId, String screenerId) { + Optional screenerOptional = + screenerRepository.getWorkingScreenerMetaDataOnly(screenerId); + if (screenerOptional.isEmpty()) { + return false; } + Screener screener = screenerOptional.get(); + return isUserAuthorizedToAccessScreenerByScreener(userId, screener); + } + + private boolean isUserAuthorizedToAccessScreenerByScreener(String userId, Screener screener) { + return userId.equals(screener.getOwnerId()); + } + + @GET + @Path("/screener/{screenerId}/benefit") + public Response getScreenerBenefits( + @Context SecurityIdentity identity, @PathParam("screenerId") String screenerId) { + String userId = AuthUtils.getUserId(identity); + + Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); + if (screenerOpt.isEmpty()) { + throw new NotFoundException(); + } + Screener screener = screenerOpt.get(); - @GET - @Path("/screener/{screenerId}/benefit/{benefitId}") - public Response getScreenerBenefit(@Context SecurityIdentity identity, - @PathParam("screenerId") String screenerId, - @PathParam("benefitId") String benefitId){ - String userId = AuthUtils.getUserId(identity); - if (!isUserAuthorizedToAccessScreener(userId, screenerId)){ - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - - try{ - Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId); - if (benefitOpt.isEmpty()){ - return Response.status(Response.Status.NOT_FOUND).build(); - } - return Response.ok().entity(benefitOpt.get()).build(); - } catch (Exception e){ - Log.error(e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not fetch benefit")) - .build(); - } + if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); } - @GET - @Path("/screener/{screenerId}/benefit/{benefitId}/check") - public Response getScreenerCustomBenefitChecks(@Context SecurityIdentity identity, - @PathParam("screenerId") String screenerId, - @PathParam("benefitId") String benefitId){ - try { - String userId = AuthUtils.getUserId(identity); - - Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); - if (screenerOpt.isEmpty()){ - throw new NotFoundException(); - } - Screener screener = screenerOpt.get(); - - if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)){ - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - - Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId); - if (benefitOpt.isEmpty()) { - throw new NotFoundException(); - } - - List checks = eligibilityCheckRepository.getChecksInBenefit(benefitOpt.get()); - return Response.ok().entity(checks).build(); - } catch (Exception e){ - Log.error(e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not fetch checks")) - .build(); - } + try { + List benefits = screenerRepository.getBenefitsInScreener(screener); + return Response.ok().entity(benefits).build(); + } catch (Exception e) { + Log.error(e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not fetch benefits")) + .build(); + } + } + + @GET + @Path("/screener/{screenerId}/benefit/{benefitId}") + public Response getScreenerBenefit( + @Context SecurityIdentity identity, + @PathParam("screenerId") String screenerId, + @PathParam("benefitId") String benefitId) { + String userId = AuthUtils.getUserId(identity); + if (!isUserAuthorizedToAccessScreener(userId, screenerId)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); } - @POST - @Path("/screener/{screenerId}/benefit") - public Response addCustomBenefit(@Context SecurityIdentity identity, - @PathParam("screenerId") String screenerId, - Benefit newBenefit) { - String userId = AuthUtils.getUserId(identity); - - newBenefit.setOwnerId(userId); - newBenefit.setChecks(Collections.emptyList()); - - BenefitDetail benefitDetail = new BenefitDetail(); - benefitDetail.setId(newBenefit.getId()); - benefitDetail.setName(newBenefit.getName()); - benefitDetail.setDescription(newBenefit.getDescription()); - benefitDetail.setPublic(newBenefit.getPublic()); - try { - // Check to make sure not introducing duplicates - Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); - if (screenerOpt.isEmpty()){ - Log.error("Screener not found. Screener ID:" + screenerId); - throw new NotFoundException(); - } - - Screener screener = screenerOpt.get(); - - // Authorise action - if (userId != null && !isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - - List benefits = screenerOpt.get().getBenefits(); - if (benefits == null) { - benefits = Collections.emptyList(); - } - Boolean benefitIdExists = !benefits.stream().filter(benefit -> benefit.getId().equals(benefitDetail.getId())).toList().isEmpty(); - - if (benefitIdExists){ - return Response.status( - Response.Status.CONFLICT.getStatusCode(), - "Benefit with provided ID already exists on screener." - ).build(); - } - - String benefitId = screenerRepository.saveNewCustomBenefit(screenerId, newBenefit); - screenerRepository.addBenefitDetailToWorkingScreener(screenerId, benefitDetail); - newBenefit.setId(benefitId); - return Response.ok(newBenefit, MediaType.APPLICATION_JSON).build(); - } catch (Exception e) { - Log.error(e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not save benefit")) - .build(); - } + try { + Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId); + if (benefitOpt.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok().entity(benefitOpt.get()).build(); + } catch (Exception e) { + Log.error(e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not fetch benefit")) + .build(); + } + } + + @GET + @Path("/screener/{screenerId}/benefit/{benefitId}/check") + public Response getScreenerCustomBenefitChecks( + @Context SecurityIdentity identity, + @PathParam("screenerId") String screenerId, + @PathParam("benefitId") String benefitId) { + try { + String userId = AuthUtils.getUserId(identity); + + Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); + if (screenerOpt.isEmpty()) { + throw new NotFoundException(); + } + Screener screener = screenerOpt.get(); + + if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId); + if (benefitOpt.isEmpty()) { + throw new NotFoundException(); + } + + List checks = + eligibilityCheckRepository.getChecksInBenefit(benefitOpt.get()); + return Response.ok().entity(checks).build(); + } catch (Exception e) { + Log.error(e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not fetch checks")) + .build(); + } + } + + @POST + @Path("/screener/{screenerId}/benefit") + public Response addCustomBenefit( + @Context SecurityIdentity identity, + @PathParam("screenerId") String screenerId, + Benefit newBenefit) { + String userId = AuthUtils.getUserId(identity); + + newBenefit.setOwnerId(userId); + newBenefit.setChecks(Collections.emptyList()); + + BenefitDetail benefitDetail = new BenefitDetail(); + benefitDetail.setId(newBenefit.getId()); + benefitDetail.setName(newBenefit.getName()); + benefitDetail.setDescription(newBenefit.getDescription()); + benefitDetail.setPublic(newBenefit.getPublic()); + try { + // Check to make sure not introducing duplicates + Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); + if (screenerOpt.isEmpty()) { + Log.error("Screener not found. Screener ID:" + screenerId); + throw new NotFoundException(); + } + + Screener screener = screenerOpt.get(); + + // Authorise action + if (userId != null && !isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + List benefits = screenerOpt.get().getBenefits(); + if (benefits == null) { + benefits = Collections.emptyList(); + } + Boolean benefitIdExists = + !benefits.stream() + .filter(benefit -> benefit.getId().equals(benefitDetail.getId())) + .toList() + .isEmpty(); + + if (benefitIdExists) { + return Response.status( + Response.Status.CONFLICT.getStatusCode(), + "Benefit with provided ID already exists on screener.") + .build(); + } + + String benefitId = screenerRepository.saveNewCustomBenefit(screenerId, newBenefit); + screenerRepository.addBenefitDetailToWorkingScreener(screenerId, benefitDetail); + newBenefit.setId(benefitId); + return Response.ok(newBenefit, MediaType.APPLICATION_JSON).build(); + } catch (Exception e) { + Log.error(e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not save benefit")) + .build(); } + } - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Path("/screener/{screenerId}/benefit") - public Response updateCustomBenefit(@Context SecurityIdentity identity, - @PathParam("screenerId") String screenerId, - Benefit updatedBenefit) { - String userId = AuthUtils.getUserId(identity); - - // TODO: Add validations for user provided data - - if (!isUserAuthorizedToAccessScreener(userId, screenerId)) { - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - - try { - Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, updatedBenefit.getId()); - if (benefitOpt.isEmpty()) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - screenerRepository.updateCustomBenefit(screenerId, updatedBenefit); - return Response.ok(updatedBenefit, MediaType.APPLICATION_JSON).build(); - } catch (Exception e) { - Log.error(e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not update custom benefit")) - .build(); - } + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Path("/screener/{screenerId}/benefit") + public Response updateCustomBenefit( + @Context SecurityIdentity identity, + @PathParam("screenerId") String screenerId, + Benefit updatedBenefit) { + String userId = AuthUtils.getUserId(identity); + + // TODO: Add validations for user provided data + + if (!isUserAuthorizedToAccessScreener(userId, screenerId)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); } - @DELETE - @Path("/screener/{screenerId}/benefit/{benefitId}") - public Response deleteCustomBenefit(@Context SecurityIdentity identity, - @PathParam("screenerId") String screenerId, - @PathParam("benefitId") String benefitId) { - try { - // Check if Screener and Benefit exist - Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); - Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId); - if (screenerOpt.isEmpty()){ - throw new NotFoundException(); - } - if (benefitOpt.isEmpty()) { - throw new NotFoundException(); - } - - // Confirm user is authorized to make the change - String userId = AuthUtils.getUserId(identity); - Screener screener = screenerOpt.get(); - if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)){ - return Response.status(Response.Status.UNAUTHORIZED).build(); - } - - // Delete the benefit and remove the benefitDetail from the screener - screenerRepository.deleteCustomBenefit(screenerId, benefitId); - List updatedBenefits = screener.getBenefits() - .stream() - .filter(benefitDetail -> !benefitDetail.getId().equals(benefitId)) - .toList(); - screener.setBenefits(updatedBenefits); - screenerRepository.updateWorkingScreener(screener); - - return Response.ok().build(); - } catch (Exception e) { - Log.error(e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(Map.of("error", "Could not delete custom benefit")) - .build(); - } + try { + Optional benefitOpt = + screenerRepository.getCustomBenefit(screenerId, updatedBenefit.getId()); + if (benefitOpt.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + screenerRepository.updateCustomBenefit(screenerId, updatedBenefit); + return Response.ok(updatedBenefit, MediaType.APPLICATION_JSON).build(); + } catch (Exception e) { + Log.error(e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not update custom benefit")) + .build(); + } + } + + @DELETE + @Path("/screener/{screenerId}/benefit/{benefitId}") + public Response deleteCustomBenefit( + @Context SecurityIdentity identity, + @PathParam("screenerId") String screenerId, + @PathParam("benefitId") String benefitId) { + try { + // Check if Screener and Benefit exist + Optional screenerOpt = screenerRepository.getWorkingScreener(screenerId); + Optional benefitOpt = screenerRepository.getCustomBenefit(screenerId, benefitId); + if (screenerOpt.isEmpty()) { + throw new NotFoundException(); + } + if (benefitOpt.isEmpty()) { + throw new NotFoundException(); + } + + // Confirm user is authorized to make the change + String userId = AuthUtils.getUserId(identity); + Screener screener = screenerOpt.get(); + if (!isUserAuthorizedToAccessScreenerByScreener(userId, screener)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + // Delete the benefit and remove the benefitDetail from the screener + screenerRepository.deleteCustomBenefit(screenerId, benefitId); + List updatedBenefits = + screener.getBenefits().stream() + .filter(benefitDetail -> !benefitDetail.getId().equals(benefitId)) + .toList(); + screener.setBenefits(updatedBenefits); + screenerRepository.updateWorkingScreener(screener); + + return Response.ok().build(); + } catch (Exception e) { + Log.error(e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Could not delete custom benefit")) + .build(); } + } } diff --git a/builder-api/src/main/java/org/acme/model/domain/Screener.java b/builder-api/src/main/java/org/acme/model/domain/Screener.java index b6439548..9ee2fed3 100644 --- a/builder-api/src/main/java/org/acme/model/domain/Screener.java +++ b/builder-api/src/main/java/org/acme/model/domain/Screener.java @@ -1,107 +1,96 @@ package org.acme.model.domain; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import java.util.List; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) public class Screener { - /* Screener metadata */ - private String id; - private String ownerId; - private String screenerName; - private String organizationName; - - /* Screener data */ - private Map formSchema; - private List resultsSchema; - private List benefits; - - /* Publishing properties */ - private String publishedScreenerId; - private String lastPublishDate; - - public Screener(Map model) { - this.formSchema = model; - } - - public Screener(){ - } - - public Map getFormSchema() { - return formSchema; - } - - public void setFormSchema(Map formSchema) { - this.formSchema = formSchema; - } - - public void setOwnerId(String ownerId){ - this.ownerId = ownerId; - } - - public String getOwnerId(){ - return this.ownerId; - } - - public void setScreenerName(String screenerName){ - this.screenerName = screenerName; - } - - public String getScreenerName(){ - return this.screenerName; - } - - public void setLastPublishDate(String lastPublishDate){ - this.lastPublishDate = lastPublishDate; - } - - public void setId(String id){ - this.id = id; - } - - public String getId(){ - return this.id; - } - - public String getOrganizationName(){ - return this.organizationName; - } - - public void setOrganizationName(String organizationName){ - this.organizationName = organizationName; - } - - public void setPublishedScreenerId(String publishedScreenerId){ - this.publishedScreenerId = publishedScreenerId; - } - - public String getPublishedScreenerId(){ - return this.publishedScreenerId; - } - - public void setLastPublishedDate(String lastPublishDate){ - this.lastPublishDate = lastPublishDate; - } - - public String getLastPublishDate(){ - return this.lastPublishDate; - } - - public List getResultsSchema() { - return resultsSchema; - } - - public void setResultsSchema(List resultsSchema) { - this.resultsSchema = resultsSchema; - } - - public List getBenefits() { - return benefits; - } - - public void setBenefits(List benefits) { - this.benefits = benefits; - } + /* Screener metadata */ + private String id; + private String ownerId; + private String screenerName; + + /* Screener data */ + private Map formSchema; + private List benefits; + + /* Publishing properties */ + private String publishedScreenerId; + private String lastPublishDate; + + public Screener(Map model) { + this.formSchema = model; + } + + public Screener() {} + + /* Domain creation for POST */ + public static Screener create(String ownerId, String screenerName, String description) { + Screener s = new Screener(); + s.ownerId = ownerId; + s.screenerName = screenerName; + + return s; + } + + public Map getFormSchema() { + return formSchema; + } + + public void setFormSchema(Map formSchema) { + this.formSchema = formSchema; + } + + public void setOwnerId(String ownerId) { + this.ownerId = ownerId; + } + + public String getOwnerId() { + return this.ownerId; + } + + public void setScreenerName(String screenerName) { + this.screenerName = screenerName; + } + + public String getScreenerName() { + return this.screenerName; + } + + public void setLastPublishDate(String lastPublishDate) { + this.lastPublishDate = lastPublishDate; + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public void setPublishedScreenerId(String publishedScreenerId) { + this.publishedScreenerId = publishedScreenerId; + } + + public String getPublishedScreenerId() { + return this.publishedScreenerId; + } + + public void setLastPublishedDate(String lastPublishDate) { + this.lastPublishDate = lastPublishDate; + } + + public String getLastPublishDate() { + return this.lastPublishDate; + } + + public List getBenefits() { + return benefits; + } + + public void setBenefits(List benefits) { + this.benefits = benefits; + } } diff --git a/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java b/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java index 4724484e..8eacbb95 100644 --- a/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java +++ b/builder-api/src/main/java/org/acme/model/dto/SaveSchemaRequest.java @@ -1,8 +1,8 @@ package org.acme.model.dto; import com.fasterxml.jackson.databind.JsonNode; +import org.acme.api.validation.HasSchema; +import org.acme.api.validation.ValidSchema; -public class SaveSchemaRequest { - public String screenerId; - public JsonNode schema; -} +@ValidSchema(required = true, mustBeObject = true) +public record SaveSchemaRequest(JsonNode schema) implements HasSchema {} diff --git a/builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java b/builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java new file mode 100644 index 00000000..b0ec9127 --- /dev/null +++ b/builder-api/src/main/java/org/acme/model/dto/Screener/CreateScreenerRequest.java @@ -0,0 +1,8 @@ +package org.acme.model.dto.Screener; + +import jakarta.validation.constraints.NotBlank; + +public record CreateScreenerRequest( + @NotBlank(message = "screenerName must be provided.") String screenerName, + String description) { +} \ No newline at end of file diff --git a/builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java b/builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java new file mode 100644 index 00000000..915b1385 --- /dev/null +++ b/builder-api/src/main/java/org/acme/model/dto/Screener/EditScreenerRequest.java @@ -0,0 +1,6 @@ +package org.acme.model.dto.Screener; + +import org.acme.api.validation.AtLeastOneProvided; + +@AtLeastOneProvided(fields = {"screenerName"}) +public record EditScreenerRequest(String screenerName) {} diff --git a/builder-api/src/main/resources/application.properties b/builder-api/src/main/resources/application.properties index 5126db2a..37212871 100644 --- a/builder-api/src/main/resources/application.properties +++ b/builder-api/src/main/resources/application.properties @@ -1,6 +1,6 @@ quarkus.http.cors.enabled=true quarkus.http.cors.origins=* -quarkus.http.cors.methods=GET,POST,PUT,DELETE +quarkus.http.cors.methods=GET,POST,PUT,PATCH,DELETE quarkus.http.cors.headers=Authorization,Content-Type quarkus.datasource.db-kind=sqlite @@ -14,3 +14,6 @@ quarkus.http.auth.permission.secured.policy=authenticated quarkus.http.auth.permission.public.paths=/api/published/* quarkus.http.auth.permission.public.policy=permit + +# Reject requests if extra properties are sent +quarkus.jackson.fail-on-unknown-properties=true \ No newline at end of file diff --git a/builder-frontend/src/api/screener.ts b/builder-frontend/src/api/screener.ts index 8c3fb8f4..26f3906e 100644 --- a/builder-frontend/src/api/screener.ts +++ b/builder-frontend/src/api/screener.ts @@ -46,7 +46,10 @@ export const fetchProject = async (screenerId) => { } }; -export const createNewScreener = async (screenerData) => { +export const createNewScreener = async (request: { + screenerName: string; + description?: string; +}) => { const url = apiUrl + "/screener"; try { const response = await authFetch(url, { @@ -55,7 +58,7 @@ export const createNewScreener = async (screenerData) => { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(screenerData), + body: JSON.stringify(request), }); if (!response.ok) { @@ -69,20 +72,25 @@ export const createNewScreener = async (screenerData) => { } }; -export const updateScreener = async (screenerData) => { - const url = apiUrl + "/screener"; +export const updateScreener = async ( + screenerId: string, + request: { screenerName: string }, +) => { + const url = new URL(`${apiUrl}/screener/${screenerId}`); + try { - const response = await authFetch(url, { - method: "PUT", + const response = await authFetch(url.toString(), { + method: "PATCH", headers: { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(screenerData), + body: JSON.stringify(request), }); if (!response.ok) { - throw new Error(`Update failed with status: ${response.status}`); + const err = await response.json(); + throw new Error(err); } } catch (error) { console.error("Error updating project:", error); @@ -110,13 +118,14 @@ export const deleteScreener = async (screenerData) => { } }; -export const saveFormSchema = async (screenerId, schema) => { +export const saveFormSchema = async (screenerId: string, schema) => { const requestData: any = {}; - requestData.screenerId = screenerId; requestData.schema = schema; - const url = apiUrl + "/save-form-schema"; + const url = new URL(`${apiUrl}/save-form-schema`); + url.searchParams.append("screenerId", screenerId); + try { - const response = await authFetch(url, { + const response = await authFetch(url.toString(), { method: "POST", headers: { "Content-Type": "application/json", @@ -155,7 +164,10 @@ export const publishScreener = async (screenerId: string): Promise => { } }; -export const addCustomBenefit = async (screenerId: string, benefit: BenefitDetail) => { +export const addCustomBenefit = async ( + screenerId: string, + benefit: BenefitDetail, +) => { const url = apiUrl + "/screener/" + screenerId + "/benefit"; try { const response = await authFetch(url, { @@ -176,7 +188,10 @@ export const addCustomBenefit = async (screenerId: string, benefit: BenefitDetai } }; -export const removeCustomBenefit = async (screenerId: string, benefitId: string) => { +export const removeCustomBenefit = async ( + screenerId: string, + benefitId: string, +) => { const url = apiUrl + "/screener/" + screenerId + "/benefit/" + benefitId; try { const response = await authFetch(url, { @@ -188,7 +203,9 @@ export const removeCustomBenefit = async (screenerId: string, benefitId: string) }); if (!response.ok) { - throw new Error(`Delete of benefit failed with status: ${response.status}`); + throw new Error( + `Delete of benefit failed with status: ${response.status}`, + ); } } catch (error) { console.error("Error deleting custom benefit:", error); @@ -196,7 +213,10 @@ export const removeCustomBenefit = async (screenerId: string, benefitId: string) } }; -export const evaluateScreener = async (screenerId: string, inputData: any): Promise => { +export const evaluateScreener = async ( + screenerId: string, + inputData: any, +): Promise => { const url = apiUrl + "/decision/v2?screenerId=" + screenerId; try { const response = await authFetch(url, { @@ -211,7 +231,7 @@ export const evaluateScreener = async (screenerId: string, inputData: any): Prom if (!response.ok) { throw new Error(`Evaluation failed with status: ${response.status}`); } - + const data = await response.json(); return data; } catch (error) { diff --git a/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx b/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx index 71256ca3..0aa18ca5 100644 --- a/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx +++ b/builder-frontend/src/components/homeScreen/EditScreenerForm.jsx @@ -29,9 +29,8 @@ export default function EditScreenerForm({ setIsLoading(true); const data = { screenerName: screenerName(), - id: screenerData.id, }; - await handleEditScreener(data); + await handleEditScreener(screenerData.id, data); if (isActive) setIsLoading(false); } catch (e) { if (setIsLoading()) { diff --git a/builder-frontend/src/components/homeScreen/ProjectsList.tsx b/builder-frontend/src/components/homeScreen/ProjectsList.tsx index 9d038e9a..a8ed1e7d 100644 --- a/builder-frontend/src/components/homeScreen/ProjectsList.tsx +++ b/builder-frontend/src/components/homeScreen/ProjectsList.tsx @@ -33,7 +33,10 @@ export default function ProjectsList() { navigate("/project/" + project.id); }; - const handleCreateNewScreener = async (screenerData) => { + const handleCreateNewScreener = async (screenerData: { + screenerName: string; + description?: string | undefined; + }) => { try { const newScreener = await createNewScreener(screenerData); navigate(`/project/${newScreener.id}`); @@ -48,9 +51,12 @@ export default function ProjectsList() { setIsEditgModalVisible(true); }; - const handleUpdateScreener = async (screenerData) => { + const handleUpdateScreener = async ( + screenerId: string, + screenerData: { screenerName: string }, + ) => { try { - await updateScreener(screenerData); + await updateScreener(screenerId, screenerData); refetchProjectList(); setIsEditgModalVisible(false); } catch (e) {