feat(notifications): add email to user on api signup
Add a new notification that is sent to the user who signs up to an API. This is sent in addition to
the approval notifications to all plan admins.
Closes:
ADD
distro/conf/src/main/resources/notifications/email/tpl/en/apiman.client.contract.request.user.html
+<!DOCTYPE html>
+<html lang="en">
+{#include header.include.html title='API Signup Request' /}
+
+{#include standard-body.include.html notificationHeader='API Signup Request' name=notification.recipient.fullName}
+{#notificationBody}
+<p>your API signup request is currently pending approval.</p>
+<p>You will receive further emails on status changes.</p>
+
+
+<h5>API Information</h5>
+<p>The API you want to subscribe to:</p>
+<dl class="row">
+ <dt class="col-sm-3">Organization</dt>
+ <dd class="col-sm-9">
+ {event.apiOrgId}
+ </dd>
+ <dt class="col-sm-3">Name</dt>
+ <dd class="col-sm-9">
+ {event.apiId}
+ </dd>
+ <dt class="col-sm-3">Version</dt>
+ <dd class="col-sm-9">
+ {event.apiVersion}
+ </dd>
+ <dt class="col-sm-3">Plan</dt>
+ <dd class="col-sm-9">
+ {event.planId}
+ </dd>
+ <dt class="col-sm-3">Plan Version</dt>
+ <dd class="col-sm-9">
+ {event.planVersion}
+ </dd>
+</dl>
+
+<h5>Client Information</h5>
+<p>The client you used for the subscription:</p>
+<dl class="row">
+ <dt class="col-sm-3">Organization</dt>
+ <dd class="col-sm-9">
+ {event.clientOrgId}
+ </dd>
+ <dt class="col-sm-3">Name</dt>
+ <dd class="col-sm-9">
+ {event.clientId}
+ </dd>
+ <dt class="col-sm-3">Version</dt>
+ <dd class="col-sm-9">
+ {event.clientVersion}
+ </dd>
+</dl>
+
+<div class="container mt-5">
+ {#include footer.en.include.html /}
+</div>
+{/notificationBody}
+{/include}
+</html>

ADD
distro/conf/src/main/resources/notifications/email/tpl/en/apiman.client.contract.request.user.txt
+Hi {notification.recipient.fullName},
+
+your API signup request is currently pending approval.
+You will receive further emails on status changes.
+
+## API Information
+
+The API you want to subscribe to:
+
+Organization: {event.apiOrgId}
+Name: {event.apiId}
+Version: {event.apiVersion}
+Plan: {event.planId}
+Plan Version: {event.planVersion}
+
+## Client Information
+
+The client you used for the subscription:
+
+Organization: {event.clientOrgId}
+Name: {event.clientId}
+Version: {event.clientVersion}
+
+{#include footer.en.include.txt /}

MODIFY
distro/conf/src/test/java/DefaultTemplatesTest.java
distro/conf/src/test/java/DefaultTemplatesTest.java
@@ -265,4 +265,121 @@ public class DefaultTemplatesTest {
EmailNotificationDispatcher.createDefaultTemplateMap(notificationDto, CONFIG)
);
}
+
+ @Test
+ public void contract_request_user() throws IOException {
+ UserDto appDeveloper = new UserDto()
+ .setFullName("John Smith Appdev")
+ .setUsername("JohnSmith123")
+ .setEmail("foo@apiman.io")
+ .setLocale(Locale.ENGLISH);
+
+ ContractCreatedEvent contractCreated = ContractCreatedEvent
+ .builder()
+ .setHeaders(ApimanEventHeaders.builder()
+ .setId("Event123")
+ .setSource(URI.create("https://example.org"))
+ .setSubject("Hello")
+ .setEventVersion(1L)
+ .setTime(OffsetDateTime.now())
+ .setType("X")
+ .build())
+ .setApiOrgId("ApiOrg")
+ .setApiId("CoolApi")
+ .setApiVersion("1.0")
+ .setClientOrgId("MobileKompany")
+ .setClientId("MobileApp")
+ .setClientVersion("2.0")
+ .setContractId("1234")
+ .setPlanId("Gold")
+ .setPlanVersion("1.3")
+ .setApprovalRequired(true)
+ .setUser(appDeveloper)
+ .build();
+
+ NotificationDto<IVersionedApimanEvent> notificationDto = new NotificationDto<>()
+ .setId(123L)
+ .setCategory(NotificationCategory.API_LIFECYCLE)
+ .setReason("whatever")
+ .setReasonMessage("hi")
+ .setStatus(NotificationStatus.OPEN)
+ .setCreatedOn(OffsetDateTime.now())
+ .setModifiedOn(OffsetDateTime.now())
+ .setRecipient(appDeveloper)
+ .setSource("adsadsaad")
+ .setPayload(contractCreated);
+
+ QuteTemplateEngine engine = new QuteTemplateEngine(CONFIG);
+ engine.applyTemplate(
+ Files.readString(CONFIG.getConfigDirectory().resolve("notifications/email/tpl/en/apiman.client.contract.request.user.txt")),
+ EmailNotificationDispatcher.createDefaultTemplateMap(notificationDto, CONFIG)
+ );
+
+ engine.applyTemplate(
+ Files.readString(CONFIG.getConfigDirectory().resolve("notifications/email/tpl/en/apiman.client.contract.request.user.html")),
+ EmailNotificationDispatcher.createDefaultTemplateMap(notificationDto, CONFIG)
+ );
+ }
+
+ @Test
+ public void contract_approval_response_rejected() throws IOException {
+ UserDto recipient = new UserDto()
+ .setFullName("John Smith Appdev")
+ .setUsername("JohnSmith123")
+ .setEmail("foo@apiman.io")
+ .setLocale(Locale.ENGLISH);
+
+ UserDto approver = new UserDto()
+ .setFullName("David Approver")
+ .setUsername("ApproverPerson")
+ .setEmail("approver@apiman.io")
+ .setLocale(Locale.ENGLISH);
+
+ ContractApprovalEvent approvalEvent = ContractApprovalEvent
+ .builder()
+ .setHeaders(ApimanEventHeaders.builder()
+ .setId("Event123")
+ .setSource(URI.create("https://example.org"))
+ .setSubject("Hello")
+ .setEventVersion(1L)
+ .setTime(OffsetDateTime.now())
+ .setType("X")
+ .build())
+ .setApiOrgId("ApiOrg")
+ .setApiId("CoolApi")
+ .setApiVersion("1.0")
+ .setClientOrgId("MobileKompany")
+ .setClientId("MobileApp")
+ .setClientVersion("2.0")
+ .setContractId("1234")
+ .setPlanId("Gold")
+ .setPlanVersion("1.3")
+ .setApprover(approver)
+ .setApproved(false)
+ .setRejectionReason("You shall not PAS")
+ .build();
+
+ NotificationDto<IVersionedApimanEvent> notificationDto = new NotificationDto<>()
+ .setId(123L)
+ .setCategory(NotificationCategory.API_ADMINISTRATION)
+ .setReason("whatever")
+ .setReasonMessage("hi")
+ .setStatus(NotificationStatus.OPEN)
+ .setCreatedOn(OffsetDateTime.now())
+ .setModifiedOn(OffsetDateTime.now())
+ .setRecipient(recipient)
+ .setSource("adsadsaad")
+ .setPayload(approvalEvent);
+
+ QuteTemplateEngine engine = new QuteTemplateEngine(CONFIG);
+ engine.applyTemplate(
+ Files.readString(CONFIG.getConfigDirectory().resolve("notifications/email/tpl/en/apiman.client.contract.approval.rejected.txt")),
+ EmailNotificationDispatcher.createDefaultTemplateMap(notificationDto, CONFIG)
+ );
+
+ engine.applyTemplate(
+ Files.readString(CONFIG.getConfigDirectory().resolve("notifications/email/tpl/en/apiman.client.contract.approval.rejected.html")),
+ EmailNotificationDispatcher.createDefaultTemplateMap(notificationDto, CONFIG)
+ );
+ }
}

ADD
manager/api/rest-impl/src/main/java/io/apiman/manager/api/notifications/email/handlers/ContractRequestUserEmailNotification.java
+/*
+ * Copyright 2022 Scheer PAS Schweiz AG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package io.apiman.manager.api.notifications.email.handlers;
+
+import io.apiman.manager.api.beans.events.ContractCreatedEvent;
+import io.apiman.manager.api.beans.events.IVersionedApimanEvent;
+import io.apiman.manager.api.beans.idm.UserDto;
+import io.apiman.manager.api.beans.notifications.EmailNotificationTemplate;
+import io.apiman.manager.api.beans.notifications.dto.NotificationDto;
+import io.apiman.manager.api.notifications.email.SimpleEmail;
+import io.apiman.manager.api.notifications.email.SimpleMailNotificationService;
+import io.apiman.manager.api.notifications.producers.ContractRequestUserNotificationProducer;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.Initialized;
+import javax.enterprise.event.Observes;
+import javax.inject.Inject;
+import java.util.Map;
+
+/**
+ * @author Florian Volk {@literal <florian.volk@scheer-group.com>}
+ */
+@ApplicationScoped
+public class ContractRequestUserEmailNotification implements INotificationHandler<ContractCreatedEvent> {
+
+ private final SimpleMailNotificationService mailNotificationService;
+
+ @Inject
+ public ContractRequestUserEmailNotification(SimpleMailNotificationService mailNotificationService) {
+ this.mailNotificationService = mailNotificationService;
+ }
+
+ public void init(@Observes @Initialized(ApplicationScoped.class) Object init) {
+ // no-op to force eager initialization
+ }
+
+ @Override
+ public void handle(NotificationDto<ContractCreatedEvent> notification, Map<String, Object> defaultTemplateMap) {
+ ContractCreatedEvent event = notification.getPayload();
+
+ if (event.isApprovalRequired()) {
+ UserDto recipient = notification.getRecipient();
+
+ mailNotificationService
+ .findTemplateFor(notification.getReason(), recipient.getLocale())
+ .ifPresentOrElse(
+ template -> send(recipient, template, defaultTemplateMap),
+ () -> warnOnce(recipient, notification)
+ );
+ }
+ }
+
+ private void send(UserDto recipient, EmailNotificationTemplate template, Map<String, Object> defaultTemplateMap) {
+ var mail = SimpleEmail
+ .builder()
+ .setRecipient(recipient)
+ .setTemplate(template)
+ .setTemplateVariables(defaultTemplateMap)
+ .build();
+
+ mailNotificationService.send(mail);
+ }
+
+ @Override
+ public boolean wants(NotificationDto<? extends IVersionedApimanEvent> notification) {
+ return notification.getReason().equals(ContractRequestUserNotificationProducer.APIMAN_CLIENT_CONTRACT_REQUEST_REASON);
+ }
+}

ADD
manager/api/rest-impl/src/main/java/io/apiman/manager/api/notifications/producers/ContractRequestUserNotificationProducer.java
+/*
+ * Copyright 2022 Scheer PAS Schweiz AG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * imitations under the License.
+ */
+
+package io.apiman.manager.api.notifications.producers;
+
+import io.apiman.common.logging.ApimanLoggerFactory;
+import io.apiman.common.logging.IApimanLogger;
+import io.apiman.manager.api.beans.events.ContractCreatedEvent;
+import io.apiman.manager.api.beans.notifications.NotificationCategory;
+import io.apiman.manager.api.beans.notifications.dto.CreateNotificationDto;
+import io.apiman.manager.api.beans.notifications.dto.RecipientDto;
+import io.apiman.manager.api.beans.notifications.dto.RecipientType;
+import io.apiman.manager.api.notifications.INotificationProducer;
+import io.apiman.manager.api.service.NotificationService;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.event.Observes;
+import javax.inject.Inject;
+import java.util.List;
+
+/**
+ * Accept a {@link ContractCreatedEvent} and produce a {@link #APIMAN_CLIENT_CONTRACT_REQUEST_REASON} notification.
+ * This is a summary for the user who requested an API to inform him, that is request is pending approval.
+ *
+ * @author Florian Volk {@literal <florian.volk@scheer-group.com>}
+ */
+@ApplicationScoped
+public class ContractRequestUserNotificationProducer implements INotificationProducer {
+
+ public static final String APIMAN_CLIENT_CONTRACT_REQUEST_REASON = "apiman.client.contract.request.user";
+
+ private final IApimanLogger LOGGER = ApimanLoggerFactory.getLogger(ContractRequestUserNotificationProducer.class);
+ private final NotificationService notificationService;
+
+ @Inject
+ public ContractRequestUserNotificationProducer(NotificationService notificationService) {
+ this.notificationService = notificationService;
+ }
+
+ public void processEvent(@Observes ContractCreatedEvent signupEvent) {
+ LOGGER.debug("Processing signup event {0}", signupEvent);
+ if (signupEvent.isApprovalRequired()) {
+ CreateNotificationDto newNotification = new CreateNotificationDto();
+ String orgId = signupEvent.getApiOrgId();
+
+ RecipientDto requester = new RecipientDto()
+ .setRecipient(signupEvent.getUser().getUsername())
+ .setOrgId(orgId)
+ .setRecipientType(RecipientType.INDIVIDUAL);
+
+ newNotification.setRecipient(List.of(requester))
+ .setReason(APIMAN_CLIENT_CONTRACT_REQUEST_REASON)
+ .setReasonMessage("Signup for API")
+ .setCategory(NotificationCategory.API_LIFECYCLE)
+ .setSource("/apiman/notifications/contracts/approvals")
+ .setPayload(signupEvent);
+
+ LOGGER.debug("Sending notification for user requested contract {0} from client {1} version {2}",
+ signupEvent.getContractId(), signupEvent.getClientId(), signupEvent.getClientVersion());
+
+ notificationService.sendNotification(newNotification);
+ }
+ }
+}

MODIFY
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/002_bwayne-notifications.resttest
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/002_bwayne-notifications.resttest
@@ -42,7 +42,6 @@ Content-Type: application/json
"apiOrgId": "Organization1",
"apiId": "API1",
"apiVersion": "1.0",
- "contractId": "56",
"planId": "Plan1",
"planVersion": "1.0",
"approved": false,
@@ -143,6 +142,109 @@ Content-Type: application/json
"approved": true,
"rejectionReason": null
}
+ }, {
+ "category": "API_LIFECYCLE",
+ "reason": "apiman.client.contract.request.user",
+ "reasonMessage": "Signup for API",
+ "status": "OPEN",
+ "recipient": {
+ "username": "bwayne",
+ "fullName": "Bruce Wayne",
+ "email": "bwayne@wayne-enterprises.com"
+ },
+ "source": "/apiman/notifications/contracts/approvals",
+ "payload": {
+ "headers": {
+ "source": "/apiman/events/contracts/created",
+ "type": "io.apiman.manager.api.beans.events.ContractCreatedEvent",
+ "subject": "approval-required",
+ "eventVersion": 1,
+ "otherProperties": {}
+ },
+ "user": {
+ "username": "bwayne",
+ "fullName": "Bruce Wayne",
+ "email": "bwayne@wayne-enterprises.com"
+ },
+ "clientOrgId": "Organization1",
+ "clientId": "Client2",
+ "clientVersion": "1.0",
+ "apiOrgId": "Organization1",
+ "apiId": "API1",
+ "apiVersion": "1.0",
+ "planId": "Plan1",
+ "planVersion": "1.0",
+ "approvalRequired": true
+ }
+ }, {
+ "category": "API_LIFECYCLE",
+ "reason": "apiman.client.contract.request.user",
+ "reasonMessage": "Signup for API",
+ "status": "OPEN",
+ "recipient": {
+ "username": "bwayne",
+ "fullName": "Bruce Wayne",
+ "email": "bwayne@wayne-enterprises.com"
+ },
+ "source": "/apiman/notifications/contracts/approvals",
+ "payload": {
+ "headers": {
+ "source": "/apiman/events/contracts/created",
+ "type": "io.apiman.manager.api.beans.events.ContractCreatedEvent",
+ "subject": "approval-required",
+ "eventVersion": 1,
+ "otherProperties": {}
+ },
+ "user": {
+ "username": "bwayne",
+ "fullName": "Bruce Wayne",
+ "email": "bwayne@wayne-enterprises.com"
+ },
+ "clientOrgId": "Organization1",
+ "clientId": "Client1",
+ "clientVersion": "1.0",
+ "apiOrgId": "Organization2",
+ "apiId": "API2",
+ "apiVersion": "1.5",
+ "planId": "PlanA",
+ "planVersion": "2.0",
+ "approvalRequired": true
+ }
+ }, {
+ "category": "API_LIFECYCLE",
+ "reason": "apiman.client.contract.request.user",
+ "reasonMessage": "Signup for API",
+ "status": "OPEN",
+ "recipient": {
+ "username": "bwayne",
+ "fullName": "Bruce Wayne",
+ "email": "bwayne@wayne-enterprises.com"
+ },
+ "source": "/apiman/notifications/contracts/approvals",
+ "payload": {
+ "headers": {
+ "source": "/apiman/events/contracts/created",
+ "type": "io.apiman.manager.api.beans.events.ContractCreatedEvent",
+ "subject": "approval-required",
+ "eventVersion": 1,
+ "otherProperties": {}
+ },
+ "user": {
+ "username": "bwayne",
+ "fullName": "Bruce Wayne",
+ "email": "bwayne@wayne-enterprises.com"
+ },
+ "clientOrgId": "Organization1",
+ "clientId": "Client1",
+ "clientVersion": "1.0",
+ "apiOrgId": "Organization1",
+ "apiId": "API1",
+ "apiVersion": "1.0",
+ "planId": "Plan1",
+ "planVersion": "1.0",
+ "approvalRequired": true
+ }
}],
- "totalSize": 4
+ "totalSize": 7
}
+

MODIFY
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/004_bwayne-list-notifications-all-read.resttest
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/004_bwayne-list-notifications-all-read.resttest
@@ -10,8 +10,8 @@ Content-Type: application/json
----
200
Content-Type: application/json
-X-Total-Count: 4
-Total-Count: 4
+X-Total-Count: 7
+Total-Count: 7
{
"beans": [{
@@ -34,6 +34,21 @@ Total-Count: 4
"reason": "apiman.client.contract.approval.granted",
"reasonMessage": "Signup was approved!",
"status": "USER_DISMISSED"
+ }, {
+ "category": "API_LIFECYCLE",
+ "reason": "apiman.client.contract.request.user",
+ "reasonMessage": "Signup for API",
+ "status": "USER_DISMISSED"
+ }, {
+ "category": "API_LIFECYCLE",
+ "reason": "apiman.client.contract.request.user",
+ "reasonMessage": "Signup for API",
+ "status": "USER_DISMISSED"
+ }, {
+ "category": "API_LIFECYCLE",
+ "reason": "apiman.client.contract.request.user",
+ "reasonMessage": "Signup for API",
+ "status": "USER_DISMISSED"
}],
- "totalSize": 4
+ "totalSize": 7
}

MODIFY
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/006_bwayne-count-all-notifications.resttest
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/006_bwayne-count-all-notifications.resttest
@@ -3,6 +3,6 @@ Content-Type: application/json
----
204
-X-Total-Count: 4
-Total-Count: 4
+X-Total-Count: 7
+Total-Count: 7

MODIFY
manager/ui/war/plugins/api-manager/ts/notification/notificationmapper.service.ts
manager/ui/war/plugins/api-manager/ts/notification/notificationmapper.service.ts
@@ -84,6 +84,23 @@ class NotificationMapper {
}
},
],
+ [
+ "apiman.client.contract.request.user",
+ {
+ icon: "fa-info",
+ reason: "Your API signup",
+ messageResolver: (notification: ApimanNotification<any>): string => {
+ const notificationWithEvent: ApimanNotification<ContractApprovalEvent> = notification;
+ const event: ContractApprovalEvent = notificationWithEvent.payload;
+ return `Your request to sign up to API ${event.apiId} is pending approval`;
+ },
+ linkResolver: (notification: ApimanNotification<any>): string => {
+ const notificationWithEvent: ApimanNotification<ContractApprovalEvent> = notification;
+ const event: ContractApprovalEvent = notificationWithEvent.payload;
+ return `${ApimanGlobals.pluginName}/orgs/${event.clientOrgId}/clients/${event.clientId}/${event.clientVersion}/contracts`;
+ }
+ },
+ ],
[
"apiman.client.status_change",
{

feat(notifications): add email to user on api signup

Add a new notification that is sent to the user who signs up to an API. This is sent in addition to
the approval notifications to all plan admins.

Closes: #1852