Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DT-1143: Passthrough APIs for support requests and content uploads #2448

Merged
merged 17 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.broadinstitute.consent.http.resources.SchemaResource;
import org.broadinstitute.consent.http.resources.StatusResource;
import org.broadinstitute.consent.http.resources.StudyResource;
import org.broadinstitute.consent.http.resources.SupportResource;
import org.broadinstitute.consent.http.resources.SwaggerResource;
import org.broadinstitute.consent.http.resources.TDRResource;
import org.broadinstitute.consent.http.resources.TosResource;
Expand Down Expand Up @@ -232,6 +233,7 @@ public void run(ConsentConfiguration config, Environment env) {
env.jersey().register(new SchemaResource());
env.jersey().register(new SwaggerResource(config.getGoogleAuthentication()));
env.jersey().register(new StatusResource(env.healthChecks()));
env.jersey().register(injector.getInstance(SupportResource.class));
env.jersey().register(
new UserResource(samService, userService, datasetService, acknowledgementService));
env.jersey().register(new TosResource(samService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@

public enum SupportRequestType {

QUESTION("question"), INCIDENT("incident"), PROBLEM("problem"), TASK("task");
QUESTION, BUG, FEATURE_REQUEST, TASK

private final String value;

SupportRequestType(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,15 @@ public Request parseRequestResponse(String response) {
/**
* Constructs a DuosTicket with the proper structure to request support via Zendesk
*
* @param name The name of the user requesting support
* @param type The type of request ("question", "incident", "problem", "task")
* @param email The email of the user requesting support
* @param subject Subject line of the request
* @param description Description of the task or question
* @param url The origin url of this request
* @param uploads Optional list of attachment tokens
* @param ticketFields TicketFields
*/
public DuosTicket createTicket(String name, SupportRequestType type, String email, String subject,
String description, String url, List<String> uploads) {
if (name == null || email == null) {
throw new IllegalArgumentException("Name and email of user requesting support is required");
}
if (subject == null) {
throw new IllegalArgumentException("Support ticket subject is required");
}
if (description == null) {
throw new IllegalArgumentException("Support ticket description is required");
}
if (type == null) {
throw new IllegalArgumentException("Support ticket type is required");
}
if (url == null) {
throw new IllegalArgumentException("Support ticket url is required");
}

public DuosTicket createTicket(TicketFields ticketFields) {
ticketFields.validate();
Ticket ticket = new Ticket(
new Ticket.Requester(name, email),
subject,
createComment(description, url, uploads));
ticket.setCustomFields(createCustomFields(name, type, email, description));
new Ticket.Requester(ticketFields.name(), ticketFields.email()),
ticketFields.subject(),
createComment(ticketFields.description(), ticketFields.url(), ticketFields.uploads()));
rushtong marked this conversation as resolved.
Show resolved Hide resolved
ticket.setCustomFields(createCustomFields(ticketFields.name(), ticketFields.type(), ticketFields.email(), ticketFields.description()));
rushtong marked this conversation as resolved.
Show resolved Hide resolved
// This value specifies tickets as belonging to the DUOS group defined in Zendesk
ticket.setTicketFormId(360000669472L);
return new DuosTicket(ticket);
Expand All @@ -85,7 +63,7 @@ static private Comment createComment(String description, String url, List<String
*/
static private List<CustomFieldValue> createCustomFields(String name, SupportRequestType type,
String email, String description) {
return List.of(new CustomFieldValue(360012744452L, new String[]{type.getValue()}),
return List.of(new CustomFieldValue(360012744452L, new String[]{type.name()}),
new CustomFieldValue(360007369412L, new String[]{description}),
new CustomFieldValue(360012744292L, new String[]{name}),
new CustomFieldValue(360012782111L, new String[]{email}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.broadinstitute.consent.http.models.support;

import java.util.List;
import org.broadinstitute.consent.http.enumeration.SupportRequestType;

public record TicketFields(String name, SupportRequestType type, String email, String subject,
String description, String url, List<String> uploads) {

public void validate() {
if (name() == null || email() == null) {
throw new IllegalArgumentException("Name and email of user requesting support is required");
}
if (subject() == null) {
throw new IllegalArgumentException("Subject is required");
}
if (description() == null) {
throw new IllegalArgumentException("Description is required");
}
if (type() == null) {
throw new IllegalArgumentException("Type is required");
}
if (url() == null) {
throw new IllegalArgumentException("URL is required");
}
rushtong marked this conversation as resolved.
Show resolved Hide resolved
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.broadinstitute.consent.http.resources;

import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.broadinstitute.consent.http.models.support.DuosTicket;
import org.broadinstitute.consent.http.models.support.TicketFactory;
import org.broadinstitute.consent.http.models.support.TicketFields;
import org.broadinstitute.consent.http.service.SupportRequestService;
import org.broadinstitute.consent.http.util.gson.GsonUtil;
import org.owasp.fileio.FileValidator;
import org.zendesk.client.v2.model.Request;

@Path("support")
public class SupportResource extends Resource {

private final SupportRequestService supportRequestService;
private final static TicketFactory ticketFactory = new TicketFactory();
rushtong marked this conversation as resolved.
Show resolved Hide resolved
private final static Gson gson = GsonUtil.getInstance();
rushtong marked this conversation as resolved.
Show resolved Hide resolved
private final static FileValidator validator = new FileValidator();
rushtong marked this conversation as resolved.
Show resolved Hide resolved

@Inject
public SupportResource(SupportRequestService supportRequestService) {
this.supportRequestService = supportRequestService;
}

@POST
@Path("request")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response postRequest(String body) {
try {
TicketFields ticketFields = gson.fromJson(body, TicketFields.class);
DuosTicket ticket = ticketFactory.createTicket(ticketFields);
logInfo("Support Request Ticket: " + ticket.toString());
rushtong marked this conversation as resolved.
Show resolved Hide resolved
Request request = supportRequestService.postTicketToSupport(ticket);
return Response.status(HttpStatusCodes.STATUS_CODE_CREATED).entity(request).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

@POST
@Path("upload")
@Consumes("application/binary")
@Produces(MediaType.APPLICATION_JSON)
public Response postUpload(byte[] content) {
try {
if (content.length > validator.getMaxFileUploadSize()) {
rushtong marked this conversation as resolved.
Show resolved Hide resolved
return Response.status(Response.Status.BAD_REQUEST).build();
}
JsonObject token = supportRequestService.postAttachmentToSupport(content);
logInfo("Support Request Content Upload: " + content.length + " bytes");
return Response.status(HttpStatusCodes.STATUS_CODE_CREATED).entity(token).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ServerErrorException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -35,10 +36,10 @@ public SupportRequestService(ServicesConfiguration configuration) {
* subsequent ticket submission.
*
* @param content Binary attachment content
* @return Token string for use as a DuosTicket.Ticket attachment
* @return JsonObject with a "token" key containing the file upload token
* @throws Exception The exception
*/
public String postAttachmentToSupport(byte[] content) throws Exception {
public JsonObject postAttachmentToSupport(byte[] content) throws Exception {
if (configuration.isActivateSupportNotifications()) {
GenericUrl genericUrl = new GenericUrl(configuration.postSupportUploadUrl());
ByteArrayContent byteContent = new ByteArrayContent("application/binary", content);
Expand All @@ -57,13 +58,12 @@ public String postAttachmentToSupport(byte[] content) throws Exception {
if (obj != null && obj.get("upload") != null) {
JsonObject uploadObj = obj.get("upload").getAsJsonObject();
if (uploadObj != null && uploadObj.get("token") != null) {
return uploadObj.get("token").getAsString();
return uploadObj.get("token").getAsJsonObject();
}
}
} else {
logDebug("Not configured to send support attachments");
}
return null;
logDebug("Not configured to send support attachments");
rushtong marked this conversation as resolved.
Show resolved Hide resolved
throw new BadRequestException("Not configured to send support attachments");
}

/**
Expand All @@ -90,10 +90,9 @@ public Request postTicketToSupport(DuosTicket ticket) throws Exception {
}
return new TicketFactory().parseRequestResponse(
IOUtils.toString(response.getContent(), Charset.defaultCharset()));
} else {
logDebug("Not configured to send support requests");
}
return null;
logDebug("Not configured to send support requests");
throw new BadRequestException("Not configured to send support requests");
}

}
4 changes: 4 additions & 0 deletions src/main/resources/assets/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,10 @@ paths:
description: All systems are OK
500:
description: Some number of subsystems are not OK.
/support/request:
$ref: './paths/supportRequest.yaml'
/support/upload:
$ref: './paths/supportUpload.yaml'
/tos/text:
$ref: './paths/tosText.yaml'
/tos/text/duos:
Expand Down
22 changes: 22 additions & 0 deletions src/main/resources/assets/paths/supportRequest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
post:
summary: Create a support request
description: Create a support request
tags:
- Support
requestBody:
content:
application/json:
schema:
$ref: '../schemas/TicketFields.yaml'
responses:
201:
description: The created support request
content:
application/json:
schema:
$ref: '../schemas/SupportRequestResponse.yaml'
400:
description: |
Bad Request: not configured for support requests
500:
description: Internal Server Error
27 changes: 27 additions & 0 deletions src/main/resources/assets/paths/supportUpload.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
post:
summary: Create a file upload for a support request
description: Create a file upload for a support request
tags:
- Support
requestBody:
content:
application/binary:
schema:
type: string
format: binary
responses:
201:
description: A token that represents the uploaded file
content:
application/json:
schema:
type: object
properties:
token:
type: string
description: The upload token that represents this upload request
400:
description: |
Bad Request: max file upload size is 5 MB or server not configured for uploads
500:
description: Internal Server Error
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
type: object
required:
- name
- type
- email
- subject
- description
- url
properties:
name:
description: The name of the user requesting support
type: string
description: The name of the user requesting support
type:
description: The type of request ("question", "incident", "problem", "task")
type: string
enum:
- question
- incident
- problem
- task
enum: ["QUESTION", "BUG", "FEATURE_REQUEST", "TASK"]
email:
description: The email of the user requesting support
type: string
description: The email of the user requesting support
subject:
description: Subject line of the request
type: string
description: Subject line of the request
description:
description: Description of the task or question
type: string
description: Description of the support request
url:
description: The origin url of this request
type: string
description: The origin url of this request
uploads:
description: Optional list of attachment tokens
type: array
description: Optional list of attachment tokens
items:
type: string
Loading
Loading