Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/workflows/gradle-build-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ jobs:
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=cloud,google,gcp" \
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
--set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \
--set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \
--platform "managed" \
--max-instances 8 \
--allow-unauthenticated
2 changes: 2 additions & 0 deletions .github/workflows/gradle-deploy-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ jobs:
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
--set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \
--set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \
--platform "managed" \
--max-instances 2 \
--allow-unauthenticated
2 changes: 2 additions & 0 deletions .github/workflows/gradle-deploy-native-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ jobs:
--set-env-vars "^@^MICRONAUT_ENVIRONMENTS=dev,cloud,google,gcp" \
--set-env-vars "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL }}" \
--set-env-vars "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" \
--set-env-vars "SLACK_PULSE_SIGNING_SECRET=${{ secrets.SLACK_PULSE_SIGNING_SECRET }}" \
--set-env-vars "SLACK_PULSE_BOT_TOKEN=${{ secrets.SLACK_PULSE_BOT_TOKEN }}" \
--platform "managed" \
--max-instances 2 \
--allow-unauthenticated
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public static class ApplicationConfig {
@NotNull
private NotificationsConfig notifications;

@NotNull
private PulseResponseConfig pulseResponse;

@Getter
@Setter
@ConfigurationProperties("feedback")
Expand Down Expand Up @@ -89,5 +92,25 @@ public static class SlackConfig {
private String botToken;
}
}

@Getter
@Setter
@ConfigurationProperties("pulse-response")
public static class PulseResponseConfig {

@NotNull
private SlackConfig slack;

@Getter
@Setter
@ConfigurationProperties("slack")
public static class SlackConfig {
@NotBlank
private String signingSecret;

@NotBlank
private String botToken;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import java.time.LocalDate;

public interface PulseServices {
public void sendPendingEmail(LocalDate now);
public void notifyUsers(LocalDate now);
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public PulseServicesImpl(
this.automatedEmailRepository = automatedEmailRepository;
}

public void sendPendingEmail(LocalDate check) {
public void notifyUsers(LocalDate check) {
if (check.getDayOfWeek() == emailDay) {
LOG.info("Checking for pending Pulse email");
// Start from the first of the year and move forward to ensure that we
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.objectcomputing.checkins.services.pulseresponse;

import com.objectcomputing.checkins.exceptions.NotFoundException;
import com.objectcomputing.checkins.util.form.FormUrlEncodedDecoder;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileServices;

import io.micronaut.http.MediaType;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.format.Format;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
Expand All @@ -14,6 +20,7 @@
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;

import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
Expand All @@ -22,17 +29,27 @@
import java.time.LocalDate;
import java.util.Set;
import java.util.UUID;
import java.util.Map;
import java.nio.charset.StandardCharsets;

@Controller("/services/pulse-responses")
@ExecuteOn(TaskExecutors.BLOCKING)
@Secured(SecurityRule.IS_AUTHENTICATED)
@Tag(name = "pulse-responses")
public class PulseResponseController {

private final PulseResponseService pulseResponseServices;
private final MemberProfileServices memberProfileServices;
private final SlackSignatureVerifier slackSignatureVerifier;
private final PulseSlackCommand pulseSlackCommand;

public PulseResponseController(PulseResponseService pulseResponseServices) {
public PulseResponseController(PulseResponseService pulseResponseServices,
MemberProfileServices memberProfileServices,
SlackSignatureVerifier slackSignatureVerifier,
PulseSlackCommand pulseSlackCommand) {
this.pulseResponseServices = pulseResponseServices;
this.memberProfileServices = memberProfileServices;
this.slackSignatureVerifier = slackSignatureVerifier;
this.pulseSlackCommand = pulseSlackCommand;
}

/**
Expand All @@ -43,6 +60,7 @@ public PulseResponseController(PulseResponseService pulseResponseServices) {
* @param dateTo
* @return
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Get("/{?teamMemberId,dateFrom,dateTo}")
public Set<PulseResponse> findPulseResponses(@Nullable @Format("yyyy-MM-dd") LocalDate dateFrom,
@Nullable @Format("yyyy-MM-dd") LocalDate dateTo,
Expand All @@ -56,6 +74,7 @@ public Set<PulseResponse> findPulseResponses(@Nullable @Format("yyyy-MM-dd") Loc
* @param pulseResponse, {@link PulseResponseCreateDTO}
* @return {@link HttpResponse<PulseResponse>}
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Post
public HttpResponse<PulseResponse> createPulseResponse(@Body @Valid PulseResponseCreateDTO pulseResponse,
HttpRequest<?> request) {
Expand All @@ -70,6 +89,7 @@ public HttpResponse<PulseResponse> createPulseResponse(@Body @Valid PulseRespons
* @param pulseResponse, {@link PulseResponse}
* @return {@link HttpResponse<PulseResponse>}
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Put
public HttpResponse<PulseResponse> update(@Body @Valid @NotNull PulseResponse pulseResponse,
HttpRequest<?> request) {
Expand All @@ -82,6 +102,7 @@ public HttpResponse<PulseResponse> update(@Body @Valid @NotNull PulseResponse pu
* @param id
* @return
*/
@Secured(SecurityRule.IS_AUTHENTICATED)
@Get("/{id}")
public PulseResponse readRole(@NotNull UUID id) {
PulseResponse result = pulseResponseServices.read(id);
Expand All @@ -90,4 +111,72 @@ public PulseResponse readRole(@NotNull UUID id) {
}
return result;
}
}

@Secured(SecurityRule.IS_ANONYMOUS)
@Post(uri = "/command", consumes = MediaType.APPLICATION_FORM_URLENCODED)
public HttpResponse commandPulseResponse(
@Header("X-Slack-Signature") String signature,
@Header("X-Slack-Request-Timestamp") String timestamp,
@Body String requestBody) {
// Validate the request
if (slackSignatureVerifier.verifyRequest(signature,
timestamp, requestBody)) {
// Convert the request body to a map of values.
FormUrlEncodedDecoder formUrlEncodedDecoder = new FormUrlEncodedDecoder();
Map<String, Object> body =
formUrlEncodedDecoder.decode(requestBody,
StandardCharsets.UTF_8);

// Respond to the slack command.
String triggerId = (String)body.get("trigger_id");
if (pulseSlackCommand.send(triggerId)) {
return HttpResponse.ok();
} else {
return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
}
} else {
return HttpResponse.unauthorized();
}
}

@Secured(SecurityRule.IS_ANONYMOUS)
@Post("/external")
public HttpResponse<PulseResponse> externalPulseResponse(
@Header("X-Slack-Signature") String signature,
@Header("X-Slack-Request-Timestamp") String timestamp,
@Body String requestBody,
HttpRequest<?> request) {
// Validate the request
if (slackSignatureVerifier.verifyRequest(signature,
timestamp, requestBody)) {
PulseResponseCreateDTO pulseResponseDTO =
SlackPulseResponseConverter.get(memberProfileServices,
requestBody);

// Create the pulse response
PulseResponse pulseResponse = pulseResponseServices.unsecureSave(
new PulseResponse(
pulseResponseDTO.getInternalScore(),
pulseResponseDTO.getExternalScore(),
pulseResponseDTO.getSubmissionDate(),
pulseResponseDTO.getTeamMemberId(),
pulseResponseDTO.getInternalFeelings(),
pulseResponseDTO.getExternalFeelings()
)
);

if (pulseResponse == null) {
return HttpResponse.status(HttpStatus.CONFLICT,
"Already submitted today");
} else {
return HttpResponse.created(pulseResponse)
.headers(headers -> headers.location(
URI.create(String.format("%s/%s",
request.getPath(),
pulseResponse.getId()))));
}
} else {
return HttpResponse.unauthorized();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.constraints.NotNull;

import java.time.LocalDate;
import java.util.Optional;
import java.util.List;
import java.util.UUID;

Expand All @@ -14,4 +15,5 @@ public interface PulseResponseRepository extends CrudRepository<PulseResponse, U

List<PulseResponse> findByTeamMemberId(@NotNull UUID teamMemberId);
List<PulseResponse> findBySubmissionDateBetween(@NotNull LocalDate dateFrom, @NotNull LocalDate dateTo);
}
Optional<PulseResponse> getByTeamMemberIdAndSubmissionDate(@NotNull UUID teamMemberId, @NotNull LocalDate submissionDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ public interface PulseResponseService {
PulseResponse read(UUID id);

PulseResponse save(PulseResponse pulseResponse);
PulseResponse unsecureSave(PulseResponse pulseResponse);

PulseResponse update(PulseResponse pulseResponse);

Set<PulseResponse> findByFields(UUID teamMemberId, LocalDate dateFrom, LocalDate dateTo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,61 @@ public PulseResponseServicesImpl(

@Override
public PulseResponse save(PulseResponse pulseResponse) {
UUID currentUserId = currentUserServices.getCurrentUser().getId();
if (pulseResponse != null) {
verifyPulseData(pulseResponse);
final UUID memberId = pulseResponse.getTeamMemberId();
UUID currentUserId = currentUserServices.getCurrentUser().getId();
if (memberId != null &&
!currentUserId.equals(memberId) &&
!isSubordinateTo(memberId, currentUserId)) {
throw new BadArgException(
String.format("User %s does not have permission to create pulse response for user %s",
currentUserId, memberId));
}
return saveCommon(pulseResponse);
} else {
return null;
}
}

@Override
public PulseResponse unsecureSave(PulseResponse pulseResponse) {
PulseResponse pulseResponseRet = null;
if (pulseResponse != null) {
// External users could submit a pulse resonse multiple times. We
// need to check to see if this user has already submitted one
// today.
boolean submitted = false;
final UUID memberId = pulseResponse.getTeamMemberId();
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
if (pulseResponse.getId() != null) {
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
} else if (memberId != null &&
memberRepo.findById(memberId).isEmpty()) {
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
} else if (memberId != null &&
!currentUserId.equals(memberId) &&
!isSubordinateTo(memberId, currentUserId)) {
throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId));
if (memberId != null) {
Optional<PulseResponse> existing =
pulseResponseRepo.getByTeamMemberIdAndSubmissionDate(
memberId, pulseResponse.getSubmissionDate());
submitted = existing.isPresent();
}
if (!submitted) {
verifyPulseData(pulseResponse);
return saveCommon(pulseResponse);
}
pulseResponseRet = pulseResponseRepo.save(pulseResponse);
}
return null;
}

private void verifyPulseData(PulseResponse pulseResponse) {
final UUID memberId = pulseResponse.getTeamMemberId();
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
if (pulseResponse.getId() != null) {
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
} else if (memberId != null &&
memberRepo.findById(memberId).isEmpty()) {
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
}
}

private PulseResponse saveCommon(PulseResponse pulseResponse) {
PulseResponse pulseResponseRet = pulseResponseRepo.save(pulseResponse);

// Send low pulse survey score if appropriate
sendPulseLowScoreEmail(pulseResponseRet);
Expand Down
Loading
Loading