All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.connectors.concur.HubConcurController Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2019 VMware, Inc. All Rights Reserved.
 * SPDX-License-Identifier: BSD-2-Clause
 */

package com.vmware.connectors.concur;

import com.nimbusds.jose.util.StandardCharset;
import com.vmware.connectors.common.json.JsonDocument;
import com.vmware.connectors.common.payloads.response.*;
import com.vmware.connectors.common.utils.AuthUtil;
import com.vmware.connectors.common.utils.CardTextAccessor;
import com.vmware.connectors.concur.domain.*;
import com.vmware.connectors.concur.exception.AttachmentURLNotFoundException;
import com.vmware.connectors.concur.exception.InvalidServiceAccountCredentialException;
import com.vmware.connectors.concur.exception.UserNotFoundException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.validation.Valid;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

import static com.vmware.connectors.common.utils.CommonUtils.BACKEND_STATUS;
import static org.springframework.http.HttpHeaders.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.*;

@RestController
@SuppressWarnings("PMD.CouplingBetweenObjects")
public class HubConcurController {

    private static final Logger logger = LoggerFactory.getLogger(HubConcurController.class);

    private static final String X_BASE_URL_HEADER = "X-Connector-Base-Url";

    private static final String COMMENT_KEY = "comment";
    private static final String REASON_KEY = "reason";

    private static final String APPROVE = "APPROVE";
    private static final String REJECT = "Send Back to Employee";

    private static final String CONNECTOR_AUTH = "X-Connector-Authorization";

    private static final String CLIENT_ID = "client_id";
    private static final String CLIENT_SECRET = "client_secret";
    private static final String USERNAME = "username";
    private static final String PASSWORD = "password";
    private static final String GRANT_TYPE = "grant_type";
    private static final String BEARER = "Bearer ";
    private static final String CONTENT_DISPOSITION_FORMAT = "Content-Disposition: inline; filename=\"%s.pdf\"";
    private static final int AUTH_VALUES_COUNT = 4;

    private final WebClient rest;
    private final CardTextAccessor cardTextAccessor;
    private final Resource concurRequestTemplate;
    private final String serviceAccountAuthHeader;
    private final String oauthTokenUrl;

    @Autowired
    public HubConcurController(
            WebClient rest,
            CardTextAccessor cardTextAccessor,
            @Value("classpath:static/templates/concur-request-template.xml") Resource concurRequestTemplate,
            @Value("${concur.service-account-auth-header:}") String serviceAccountAuthHeader,
            @Value("${concur.oauth-instance-url}") String oauthTokenUrl
    ) {
        this.rest = rest;
        this.cardTextAccessor = cardTextAccessor;
        this.concurRequestTemplate = concurRequestTemplate;
        this.serviceAccountAuthHeader = serviceAccountAuthHeader;
        this.oauthTokenUrl = oauthTokenUrl;
    }

    @PostMapping(
            path = "/cards/requests",
            produces = APPLICATION_JSON_VALUE,
            consumes = APPLICATION_JSON_VALUE
    )
    public Mono> getCards(
            @RequestHeader(AUTHORIZATION) String authorization,
            @RequestHeader(X_BASE_URL_HEADER) String baseUrl,
            @RequestHeader("X-Routing-Prefix") String routingPrefix,
            @RequestHeader(name = CONNECTOR_AUTH, required = false) String connectorAuth,
            Locale locale
    ) {
        String userEmail = AuthUtil.extractUserEmail(authorization);
        logger.debug("getCards called: baseUrl={}, routingPrefix={}, userEmail={}", baseUrl, routingPrefix, userEmail);

        if (isServiceAccountCredentialEmpty(connectorAuth)) {
            return Mono.just(ResponseEntity.badRequest().build());
        }

        return getAuthHeader(connectorAuth)
                .flatMap(authHeader -> fetchCards(baseUrl, locale, routingPrefix, userEmail, authHeader)
                        .map(ResponseEntity::ok));
    }

    private Mono getAuthHeader(final String auth) {
        final String connectorAuth = getServiceAccountCredential(auth);
        final String[] authValues = connectorAuth.split(":");

        // Service account credential format - username:password:client-id:client-secret
        if (authValues.length != AUTH_VALUES_COUNT) {
            throw new InvalidServiceAccountCredentialException("Service account credential is invalid.");
        }

        final String username = authValues[0];
        final String password = authValues[1];
        final String clientId = authValues[2];
        final String clientSecret = authValues[3];

        final MultiValueMap body = getBody(username, password, clientId, clientSecret);

        return rest.post()
                .uri(UriComponentsBuilder.fromUriString(oauthTokenUrl).path("/oauth2/v0/token").toUriString())
                .header(ACCEPT, APPLICATION_JSON_VALUE)
                .body(BodyInserters.fromFormData(body))
                .retrieve()
                .bodyToMono(JsonDocument.class)
                .map(jsonDocument -> BEARER + jsonDocument.read("$.access_token"))
                .onErrorMap(WebClientResponseException.class, e -> handleForbiddenException(e));
    }

    public Throwable handleForbiddenException(WebClientResponseException e) {
        // We have to convert the exception to return UNAUTHORIZED(401), since concur returns FORBIDDEN(403) for invalid credentials.
        if (HttpStatus.FORBIDDEN.equals(e.getStatusCode())) {
            return new WebClientResponseException(
                    e.getMessage(),
                    HttpStatus.UNAUTHORIZED.value(),
                    e.getStatusText(),
                    e.getHeaders(),
                    e.getResponseBodyAsByteArray(),
                    StandardCharset.UTF_8);
        }
        return e;
    }

    private MultiValueMap getBody(final String username,
                                                  final String password,
                                                  final String clientId,
                                                  final String clientSecret) {
        final MultiValueMap body = new LinkedMultiValueMap<>();

        body.put(CLIENT_ID, List.of(clientId));
        body.put(CLIENT_SECRET, List.of(clientSecret));
        body.put(USERNAME, List.of(username));
        body.put(PASSWORD, List.of(password));
        body.put(GRANT_TYPE, List.of(PASSWORD));

        return body;
    }

    private boolean isServiceAccountCredentialEmpty(final String connectorAuth) {
        if (StringUtils.isBlank(this.serviceAccountAuthHeader) && StringUtils.isBlank(connectorAuth)) {
            logger.debug("X-Connector-Authorization should not be empty if service credentials are not present in the config file");
            return true;
        } else {
            return false;
        }
    }

    private String getServiceAccountCredential(final String connectorAuth) {
        if (StringUtils.isBlank(this.serviceAccountAuthHeader)) {
            return connectorAuth;
        } else {
            return this.serviceAccountAuthHeader;
        }
    }

    private Mono fetchCards(
            String baseUrl,
            Locale locale,
            String routingPrefix,
            String userEmail,
            String connectorAuth
    ) {
        logger.debug("fetchCards called: baseUrl={}, routingPrefix={}, userEmail={}", baseUrl, routingPrefix, userEmail);

        return fetchLoginIdFromUserEmail(userEmail, baseUrl, connectorAuth)
                .switchIfEmpty(Mono.error(new UserNotFoundException("User with email id " + userEmail + " is not found.")))
                .flatMapMany(loginId -> fetchAllApprovals(baseUrl, loginId, connectorAuth))
                .flatMap(expense -> fetchRequestData(baseUrl, expense.getId(), connectorAuth))
                .map(report -> makeCards(baseUrl, routingPrefix, locale, report))
                .reduce(new Cards(), this::addCard);
    }

    private Mono fetchLoginIdFromUserEmail(
            String userEmail,
            String baseUrl,
            String connectorAuth
    ) {
        return rest.get()
                .uri(baseUrl + "/api/v3.0/common/users?primaryEmail={userEmail}", userEmail)
                .header(AUTHORIZATION, connectorAuth)
                .accept(APPLICATION_JSON)
                .retrieve()
                .bodyToMono(UserDetailsResponse.class)
                .flatMapMany(userDetails -> Flux.fromIterable(userDetails.getItems()))
                .next()
                .map(UserDetailsVO::getLoginId);
    }

    private Flux fetchAllApprovals(
            String baseUrl,
            String userEmail,
            String connectorAuth
    ) {
        int limit = 50;
        String userFilter = "all";
        return rest.get()
                .uri(baseUrl + "/api/v3.0/expense/reportdigests?approverLoginID={userEmail}&limit={limit}&user={userFilter}",
                        userEmail, limit, userFilter)
                .header(AUTHORIZATION, connectorAuth)
                .accept(APPLICATION_JSON)
                .retrieve()
                .bodyToMono(PendingApprovalResponse.class)
                .flatMapMany(expenses -> Flux.fromIterable(expenses.getPendingApprovals()));
    }

    private Mono fetchRequestData(
            String baseUrl,
            String reportId,
            String connectorAuth
    ) {
        logger.trace("fetchRequestData called: baseUrl={}, reportId={}", baseUrl, reportId);

        return rest.get()
                .uri(baseUrl + "/api/expense/expensereport/v2.0/report/{reportId}", reportId)
                .header(AUTHORIZATION, connectorAuth)
                .accept(APPLICATION_JSON)
                .retrieve()
                .bodyToMono(ExpenseReportResponse.class);
    }

    private Card makeCards(
            String baseUrl,
            String routingPrefix,
            Locale locale,
            ExpenseReportResponse report
    ) {
        String reportId = report.getReportID();
        String reportName = report.getReportName();

        logger.trace("makeCard called: routingPrefix={}, reportId={}, reportName={}", routingPrefix, reportId, reportName);

        Card.Builder builder = new Card.Builder()
                .setName("Concur")
                .setHeader(
                        new CardHeader(
                                cardTextAccessor.getMessage("hub.concur.header", locale, reportName),
                                null,
                                new CardHeaderLinks(
                                        UriComponentsBuilder
                                                .fromUriString(baseUrl)
                                                .path("/approvalsportal.asp")
                                                .toUriString(),
                                        null
                                )
                        )
                )
                .setBody(buildCard(locale, report, routingPrefix))
                .setBackendId(report.getReportID())
                .addAction(makeAction(routingPrefix, locale, reportId,
                        true, "hub.concur.approve", COMMENT_KEY, "hub.concur.approve.comment.label", "/approve"))
                .addAction(makeAction(routingPrefix, locale, reportId,
                        false, "hub.concur.decline", REASON_KEY, "hub.concur.decline.reason.label", "/decline"));

        builder.setImageUrl("https://s3.amazonaws.com/vmw-mf-assets/connector-images/hub-concur.png");

        return builder.build();
    }

    private CardBody buildCard(final Locale locale,
                               final ExpenseReportResponse report,
                               final String routingPrefix) {
        final CardBody.Builder cardBodyBuilder =  new CardBody.Builder()
                .addField(makeGeneralField(locale, "hub.concur.report.name", report.getReportName()))
                .addField(makeGeneralField(locale, "hub.concur.requester", report.getEmployeeName()))
                .addField(makeGeneralField(locale, "hub.concur.expenseAmount",
                        formatCurrency(report.getReportTotal(), locale, report.getCurrencyCode())));

        if (!CollectionUtils.isEmpty(report.getExpenseEntriesList())) {
            buildExpenseItems(report, locale).forEach(cardBodyBuilder::addField);

            // Add expense report attachment URL.
            if (StringUtils.isNotBlank(report.getReportImageURL())) {
                cardBodyBuilder.addField(buildAttachmentURL(routingPrefix, report.getReportID(), locale));
            }
        }
        return cardBodyBuilder.build();
    }

    private List buildExpenseItems(final ExpenseReportResponse report, final Locale locale) {
        return report.getExpenseEntriesList()
                .stream()
                .map(entry -> new CardBodyField.Builder()
                        .setType(CardBodyFieldType.SECTION)
                        .setTitle(cardTextAccessor.getMessage("hub.concur.business.purpose", locale, entry.getBusinessPurpose()))
                        .addItems(buildItems(locale, entry))
                        .build()
                )
                .collect(Collectors.toList());
    }

    private List buildItems(final Locale locale, final ExpenseEntriesVO expenseEntry) {
        final List items = new ArrayList<>();

        addItem("hub.concur.expense.type.name", expenseEntry.getExpenseTypeName(), locale, items);
        addItem("hub.concur.transaction.date", expenseEntry.getTransactionDate(), locale, items);
        addItem("hub.concur.vendor.name", expenseEntry.getVendorDescription(), locale, items);
        addItem("hub.concur.city.of.purchase", expenseEntry.getLocationName(), locale, items);
        addItem("hub.concur.payment.type", expenseEntry.getPaymentTypeCode(), locale, items);
        addItem("hub.concur.amount", formatCurrency(expenseEntry.getPostedAmount(),
                locale, expenseEntry.getTransactionCurrencyName()), locale, items);

        final List attendeesList = expenseEntry.getAttendeesList();
        if (!CollectionUtils.isEmpty(attendeesList)) {
            items.add(makeCardBodyFieldItem(cardTextAccessor.getMessage("hub.concur.attendees", locale), attendeesList.toString()));
        }

        return items;
    }

    private void addItem(final String title,
                         final String description,
                         final Locale locale,
                         final List items) {
        if (StringUtils.isBlank(description)) {
            return;
        }

        items.add(makeCardBodyFieldItem(cardTextAccessor.getMessage(title, locale), description));
    }

    private CardBodyFieldItem makeCardBodyFieldItem(final String title, final String description) {
        return new CardBodyFieldItem.Builder()
                .setType(CardBodyFieldType.GENERAL)
                .setTitle(title)
                .setDescription(description)
                .build();
    }

    private CardBodyField makeGeneralField(
            Locale locale,
            String labelKey,
            String value
    ) {
        if (StringUtils.isBlank(value)) {
            return null;
        }

        return new CardBodyField.Builder()
                .setType(CardBodyFieldType.GENERAL)
                .setTitle(cardTextAccessor.getMessage(labelKey, locale))
                .setDescription(value)
                .build();
    }

    private String formatCurrency(
            String amount,
            Locale locale,
            String currencyCode
    ) {
        return String.format(
                "%s %s",
                currencyCode,
                NumberFormat.getNumberInstance(locale ==  null? Locale.getDefault() : locale).format(Double.parseDouble(amount))
        );
    }

    private CardAction makeAction(
            String routingPrefix,
            Locale locale,
            String reportId,
            boolean primary,
            String buttonLabelKey,
            String textFieldId,
            String textFieldLabelKey,
            String apiPath
    ) {
        return new CardAction.Builder()
                .setActionKey(CardActionKey.USER_INPUT)
                .setLabel(cardTextAccessor.getActionLabel(buttonLabelKey, locale))
                .setCompletedLabel(cardTextAccessor.getActionCompletedLabel(buttonLabelKey, locale))
                .setPrimary(primary)
                .setMutuallyExclusiveSetId("approval-actions")
                .setType(HttpMethod.POST)
                .setUrl(routingPrefix + "api/expense/" + reportId + apiPath)
                .addUserInputField(
                        new CardActionInputField.Builder().setFormat("textarea")
                                .setId(textFieldId)
                                .setLabel(cardTextAccessor.getMessage(textFieldLabelKey, locale))
                                .build()
                )
                .build();
    }

    private CardBodyField buildAttachmentURL(final String routingPrefix,
                                             final String reportID,
                                             final Locale locale) {
        CardBodyField.Builder builder = new CardBodyField.Builder()
                .setTitle(cardTextAccessor.getMessage("hub.concur.attachment", locale))
                .setType(CardBodyFieldType.SECTION)
                .addItem(new CardBodyFieldItem.Builder()
                        .setAttachmentName(reportID)
                        .setTitle(cardTextAccessor.getMessage("hub.concur.report.image.url", locale))
                        .setAttachmentMethod(HttpMethod.GET)

                        .setAttachmentUrl(getAttachmentUrl(routingPrefix, reportID))
                        .setType(CardBodyFieldType.ATTACHMENT_URL)
                        .setAttachmentContentType(APPLICATION_PDF_VALUE) // Concur always returns a PDF file. It consolidates all the attachments into a single PDF file.
                        .build());

        return builder.build();
    }

    private String getAttachmentUrl(String routingPrefix, String reportID) {
        return UriComponentsBuilder.fromUriString(routingPrefix).path("/api/expense/report/{report_id}/attachment")
                .buildAndExpand(
                        Map.of(
                                "report_id", reportID
                        )
                ).toUriString();
    }

    private Cards addCard(
            Cards cards,
            Card card
    ) {
        cards.getCards().add(card);
        return cards;
    }

    @PostMapping(
            path = "/api/expense/{id}/approve",
            consumes = APPLICATION_FORM_URLENCODED_VALUE,
            produces = APPLICATION_JSON_VALUE
    )
    public Mono> approveRequest(
            @RequestHeader(AUTHORIZATION) String authorization,
            @RequestHeader(X_BASE_URL_HEADER) String baseUrl,
            @RequestHeader(name = CONNECTOR_AUTH, required = false) String connectorAuth,
            @PathVariable("id") String id,
            @Valid CommentForm form
    ) {
        logger.debug("approveRequest called: baseUrl={},  id={}, comment={}", baseUrl, id, form.getComment());

        if (isServiceAccountCredentialEmpty(connectorAuth)) {
            return Mono.just(ResponseEntity.badRequest().build());
        }

        String userEmail = AuthUtil.extractUserEmail(authorization);
        return getAuthHeader(connectorAuth)
                .flatMap(authHeader -> makeConcurRequest(form.getComment(), baseUrl, APPROVE, id, userEmail, authHeader)
                        .map(ResponseEntity::ok));
    }

    private Mono makeConcurRequest(
            String reason,
            String baseUrl,
            String action,
            String reportId,
            String userEmail,
            String connectorAuth
    ) {
        String concurRequestTemplate = getConcurRequestTemplate(reason, action);

        return fetchLoginIdFromUserEmail(userEmail, baseUrl, connectorAuth)
                .switchIfEmpty(Mono.error(new UserNotFoundException("User with email id " + userEmail + " is not found.")))
                .flatMapMany(loginId -> validateUser(baseUrl, reportId, loginId, connectorAuth))
                .flatMap(ignored -> fetchRequestData(baseUrl, reportId, connectorAuth))
                .map(ExpenseReportResponse::getWorkflowActionURL)
                .flatMap(
                        url ->
                                rest.post()
                                        .uri(url)
                                        .header(AUTHORIZATION, connectorAuth)
                                        .contentType(APPLICATION_XML)
                                        .accept(APPLICATION_JSON)
                                        .syncBody(concurRequestTemplate)
                                        .retrieve()
                                        .bodyToMono(String.class)
                )
                .next();
    }

    private String getConcurRequestTemplate(
            String reason,
            String concurAction
    ) {
        try {
            return IOUtils.toString(concurRequestTemplate.getInputStream(), StandardCharsets.UTF_8)
                    .replace("${action}", concurAction)
                    .replace("${comment}", HtmlUtils.htmlEscape(reason));
        } catch (IOException e) {
            throw new RuntimeException("Failed to read in concur request template!", e); // NOPMD
        }
    }

    private Mono validateUser(
            String baseUrl,
            String reportId,
            String loginID,
            String connectorAuth
    ) {
        return fetchAllApprovals(baseUrl, loginID, connectorAuth)
                .filter(expense -> expense.getId().equals(reportId))
                .filter(expense -> expense.getApproverLoginID().equals(loginID))
                .next()
                .switchIfEmpty(Mono.error(new UserNotFoundException("User with login id " + loginID + " is not found."))); // CustomException
    }

    @PostMapping(
            path = "/api/expense/{id}/decline",
            consumes = APPLICATION_FORM_URLENCODED_VALUE,
            produces = APPLICATION_JSON_VALUE
    )
    public Mono> declineRequest(
            @RequestHeader(AUTHORIZATION) String authorization,
            @RequestHeader(X_BASE_URL_HEADER) String baseUrl,
            @RequestHeader(name = CONNECTOR_AUTH, required = false) String connectorAuth,
            @PathVariable("id") String id,
            @Valid DeclineForm form
    ) {
        logger.debug("declineRequest called: baseUrl={}, id={}, reason={}", baseUrl, id, form.getReason());

        if (isServiceAccountCredentialEmpty(connectorAuth)) {
            return Mono.just(ResponseEntity.badRequest().build());
        }

        String userEmail = AuthUtil.extractUserEmail(authorization);
        return getAuthHeader(connectorAuth)
                .flatMap(authHeader -> makeConcurRequest(form.getReason(), baseUrl, REJECT, id, userEmail, authHeader)
                        .map(ResponseEntity::ok));
    }

    @GetMapping(
            path = "api/expense/report/{id}/attachment"
    )
    public Mono>> fetchAttachment(
            @RequestHeader(AUTHORIZATION) String authorization,
            @RequestHeader(X_BASE_URL_HEADER) String baseUrl,
            @RequestHeader(CONNECTOR_AUTH) String connectorAuth,
            @PathVariable("id") String reportId
    ) {
        final String userEmail = AuthUtil.extractUserEmail(authorization);
        logger.debug("fetchAttachment called: baseUrl={}, userEmail={}, reportId={}", baseUrl, userEmail, reportId);

        return getAuthHeader(connectorAuth)
                .flatMap(authHeader -> fetchLoginIdFromUserEmail(userEmail, baseUrl, authHeader)
                        .flatMap(loginID -> validateUser(baseUrl, reportId, loginID, authHeader))
                        .then(fetchRequestData(baseUrl, reportId, authHeader))
                        .flatMap(expenseReportResponse -> getAttachment(expenseReportResponse, authHeader))
                        .map(clientResponse -> handleClientResponse(clientResponse, reportId)));
    }

    private Mono getAttachment(ExpenseReportResponse report, String connectorAuth) {
        if (StringUtils.isBlank(report.getReportImageURL())) {
            throw new AttachmentURLNotFoundException("Concur expense report with ID " + report.getReportID() + " does not have any attachments.");
        }

        return this.rest.get()
                .uri(report.getReportImageURL())
                .header(AUTHORIZATION, connectorAuth)
                .exchange();
    }

    private ResponseEntity> handleClientResponse(final ClientResponse response, final String reportId) {
        if (response.statusCode().is2xxSuccessful()) {
            return ResponseEntity.ok()
                    .contentType(response.headers().contentType().orElse(APPLICATION_PDF))
                    .header(CONTENT_DISPOSITION, String.format(CONTENT_DISPOSITION_FORMAT, reportId))
                    .body(response.bodyToFlux(DataBuffer.class));

        }

        return handleErrorStatus(response);
    }

    private ResponseEntity> handleErrorStatus(final ClientResponse response) {
        final HttpStatus status = response.statusCode();
        final String backendStatus = Integer.toString(response.rawStatusCode());

        logger.error("Concur backend returned the status code [{}] and reason phrase [{}] ", status, status.getReasonPhrase());

        if (status == UNAUTHORIZED) {
            String body = "{\"error\" : \"invalid_connector_token\"}";
            final DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(body.getBytes(StandardCharset.UTF_8));
            return ResponseEntity.status(BAD_REQUEST)
                    .header(BACKEND_STATUS, backendStatus)
                    .contentType(APPLICATION_JSON)
                    .body(Flux.just(dataBuffer));
        } else {
            final ResponseEntity.BodyBuilder builder = ResponseEntity.status(INTERNAL_SERVER_ERROR).header(BACKEND_STATUS, backendStatus);
            response.headers().contentType().ifPresent(builder::contentType);
            return builder.body(response.bodyToFlux(DataBuffer.class));
        }
    }

    @ExceptionHandler(AttachmentURLNotFoundException.class)
    @ResponseStatus(NOT_FOUND)
    @ResponseBody
    public Map handleAttachmentURLNotFoundException(AttachmentURLNotFoundException e) {
        return Map.of("error", e.getMessage());
    }

    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(NOT_FOUND)
    @ResponseBody
    public Map handleUserNotFoundException(UserNotFoundException e) {
        return Map.of("error", e.getMessage());
    }

    @ExceptionHandler(InvalidServiceAccountCredentialException.class)
    @ResponseBody
    public ResponseEntity> handleInvalidServiceAccountCredentialException(InvalidServiceAccountCredentialException e) {
        return ResponseEntity.status(BAD_REQUEST)
                .header(BACKEND_STATUS, Integer.toString(UNAUTHORIZED.value()))
                .body(Map.of("error", e.getMessage()));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy