Commit 2e8b90ad authored by Martin Lowe's avatar Martin Lowe 🇨🇦 Committed by Martin Lowe
Browse files

Progress for upgrading Mailer call to send mail to author and team

Includes Qute templates, Mailer extension settings, and logic to update
form once finished.
parent 1cec49b7
......@@ -7,4 +7,12 @@ quarkus.oidc.client-id=sample
quarkus.oidc.credentials.client-secret.value=sample
security.token.salt=somesaltvalue
eclipse.app.base-url=https://www.rem.docker/
\ No newline at end of file
eclipse.app.base-url=https://www.rem.docker/
## Used to send mail through the EclipseFdn smtp connection
quarkus.mailer.password=YOURGENERATEDAPPLICATIONPASSWORD
quarkus.mailer.username=YOUREMAIL@gmail.com
## Used to retrieve user profile information from CMS
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
\ No newline at end of file
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipsefoundation</groupId>
<artifactId>react-container</artifactId>
......@@ -8,10 +9,11 @@
<eclipse-api-version>0.2-SNAPSHOT</eclipse-api-version>
<surefire-plugin.version>2.22.1</surefire-plugin.version>
<maven.compiler.target>11</maven.compiler.target>
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>1.13.7.Final</quarkus.platform.version>
<maven.compiler.source>11</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.parameters>true</maven.compiler.parameters>
<quarkus-plugin.version>1.13.7.Final</quarkus-plugin.version>
......@@ -63,6 +65,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-undertow</artifactId>
......@@ -87,6 +97,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
......@@ -116,9 +134,10 @@
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus-plugin.version}</version>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
......@@ -132,14 +151,14 @@
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>${maven.compiler.parameters}</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<includes>
<include>**/*Test.java</include>
</includes>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
......
/*******************************************************************************
* Copyright (C) 2020 Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
******************************************************************************/
package org.eclipsefoundation.react.api;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipsefoundation.react.api.model.EclipseUser;
import io.quarkus.oidc.client.filter.OidcClientFilter;
/**
* Binding interface for the Eclipse Foundation user account API. Runtime implementations are automatically generated by
* Quarkus at compile time. As the API deals with sensitive information, authentication is required to access this
* endpoint.
*
* @author Martin Lowe
*
*/
@Path("/account")
//@OidcClientFilter
@RegisterRestClient(configKey = "fdn-accounts")
public interface AccountsAPI {
/**
* Retrieves all user objects that match the given query parameters.
*
* @param id user ID of the Eclipse account to retrieve
* @param name the given name to match against for Eclipse accounts
* @param mail the email address to match against for Eclipse accounts
* @return all matching eclipse accounts
*/
@GET
@Path("/profile")
@Produces("application/json")
List<EclipseUser> getUsers(@QueryParam("uid") String id, @QueryParam("name") String name,
@QueryParam("mail") String mail);
}
/**
* Copyright (C) 2020 Eclipse Foundation
*
* <p>This program and the accompanying materials are made available under the terms of the Eclipse
* Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/
*
* <p>SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.react.api.model;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
/**
* Represents a users Eclipse Foundation account
*
* @author Martin Lowe
*/
public class EclipseUser {
private int uid;
private String name;
private String mail;
private ECA eca;
private boolean isCommitter;
/** @return the id */
public int getId() {
return uid;
}
/** @param id the id to set */
public void setId(int uid) {
this.uid = uid;
}
/** @return the name */
public String getName() {
return name;
}
/** @param name the name to set */
public void setName(String name) {
this.name = name;
}
/** @return the mail */
public String getMail() {
return mail;
}
/** @param mail the mail to set */
public void setMail(String mail) {
this.mail = mail;
}
/** @return the eca */
public ECA getEca() {
return eca;
}
/** @param eca the eca to set */
public void setEca(ECA eca) {
this.eca = eca;
}
/** @return the isCommitter */
public boolean isCommitter() {
return isCommitter;
}
/** @param isCommitter the isCommitter to set */
public void setCommitter(boolean isCommitter) {
this.isCommitter = isCommitter;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("EclipseUser [uid=");
builder.append(uid);
builder.append(", name=");
builder.append(name);
builder.append(", mail=");
builder.append(mail);
builder.append(", eca=");
builder.append(eca);
builder.append(", isCommitter=");
builder.append(isCommitter);
builder.append("]");
return builder.toString();
}
/**
* ECA for Eclipse accounts, representing whether users have signed the Eclipse Committer Agreement to enable
* contribution.
*/
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class ECA {
private boolean signed;
private boolean canContributeSpecProject;
public ECA() {
this(false, false);
}
public ECA(boolean signed, boolean canContributeSpecProject) {
this.signed = signed;
this.canContributeSpecProject = canContributeSpecProject;
}
/** @return the signed */
public boolean isSigned() {
return signed;
}
/** @param signed the signed to set */
public void setSigned(boolean signed) {
this.signed = signed;
}
/** @return the canContributeSpecProject */
public boolean isCanContributeSpecProject() {
return canContributeSpecProject;
}
/** @param canContributeSpecProject the canContributeSpecProject to set */
public void setCanContributeSpecProject(boolean canContributeSpecProject) {
this.canContributeSpecProject = canContributeSpecProject;
}
}
}
......@@ -127,7 +127,10 @@ public class DataLoader {
contacts.add(c);
}
// randomly create WG entries
while (Math.random() > 0.5) {
while (true) {
if (Math.random() > 0.5) {
break;
}
FormWorkingGroup wg = new FormWorkingGroup();
wg.setWorkingGroupID(config.getWorkingGroups().get(r.nextInt(config.getWorkingGroups().size())));
wg.setParticipationLevel(
......
......@@ -59,7 +59,7 @@ public class FormWorkingGroup extends BareNode implements TargetedClone<FormWork
// form entity
@OneToOne(targetEntity = MembershipForm.class)
@JoinColumn(name = "form_id", unique = true)
@JoinColumn(name = "form_id")
private MembershipForm form;
@OneToOne(cascade = CascadeType.ALL)
......
......@@ -35,6 +35,7 @@ import org.eclipsefoundation.persistence.model.DtoTable;
import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
import org.eclipsefoundation.persistence.model.SortableField;
import org.eclipsefoundation.react.namespace.FormState;
import org.eclipsefoundation.react.namespace.MembershipFormAPIParameterNames;
import org.hibernate.annotations.GenericGenerator;
......@@ -55,6 +56,7 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
private String registrationCountry;
@SortableField
private Long dateCreated;
private FormState state;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "form_id")
......@@ -141,6 +143,14 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
this.dateCreated = dateCreated;
}
public FormState getState() {
return this.state;
}
public void setState(FormState state) {
this.state = state;
}
/**
* @return the contacts
*/
......@@ -173,6 +183,7 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
target.setPurchaseOrderRequired(getPurchaseOrderRequired());
target.setRegistrationCountry(getRegistrationCountry());
target.setVatNumber(getVatNumber());
target.setState(getState());
if (getDateCreated() != null) {
target.setDateCreated(getDateCreated());
}
......@@ -184,7 +195,7 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(id, membershipLevel, signingAuthority, userID, vatNumber,
registrationCountry, purchaseOrderRequired, dateCreated);
registrationCountry, purchaseOrderRequired, dateCreated, state);
return result;
}
......@@ -201,7 +212,8 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
&& signingAuthority == other.signingAuthority && Objects.equals(userID, other.userID)
&& Objects.equals(vatNumber, other.vatNumber) && Objects.equals(dateCreated, other.dateCreated)
&& Objects.equals(registrationCountry, other.registrationCountry)
&& Objects.equals(purchaseOrderRequired, other.purchaseOrderRequired);
&& Objects.equals(purchaseOrderRequired, other.purchaseOrderRequired)
&& Objects.equals(state, other.state);
}
@Override
......@@ -223,6 +235,8 @@ public class MembershipForm extends BareNode implements TargetedClone<Membership
builder.append(vatNumber);
builder.append(", dateCreated=");
builder.append(dateCreated);
builder.append(", state=");
builder.append(state);
builder.append("]");
return builder.toString();
}
......
package org.eclipsefoundation.react.namespace;
/**
* Defined membership form states
*
* @author Martin Lowe
*
*/
public enum FormState {
SUBMITTED, INPROGRESS, COMPLETE;
}
......@@ -14,6 +14,7 @@ package org.eclipsefoundation.react.request;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
......@@ -23,6 +24,7 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
......@@ -30,9 +32,16 @@ import javax.ws.rs.core.Response;
import org.eclipsefoundation.core.helper.CSRFHelper;
import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
import org.eclipsefoundation.persistence.model.RDBMSQuery;
import org.eclipsefoundation.react.model.Contact;
import org.eclipsefoundation.react.model.FormOrganization;
import org.eclipsefoundation.react.model.FormWorkingGroup;
import org.eclipsefoundation.react.model.MembershipForm;
import org.eclipsefoundation.react.namespace.FormState;
import org.eclipsefoundation.react.namespace.MembershipFormAPIParameterNames;
import org.eclipsefoundation.react.service.MailerService;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.quarkus.security.Authenticated;
......@@ -46,6 +55,10 @@ import io.quarkus.security.Authenticated;
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MembershipFormResource extends AbstractRESTResource {
public static final Logger LOGGER = LoggerFactory.getLogger(MembershipFormResource.class);
@Inject
MailerService mailer;
@GET
public Response getAll(@HeaderParam(value = CSRFHelper.CSRF_HEADER_NAME) String csrf) {
......@@ -85,6 +98,7 @@ public class MembershipFormResource extends AbstractRESTResource {
} else if (results.isEmpty()) {
return Response.status(404).build();
}
LOGGER.error("First: {}", results);
// return the results as a response
return Response.ok(results.get(0)).build();
}
......@@ -108,6 +122,7 @@ public class MembershipFormResource extends AbstractRESTResource {
if (r != null) {
return r;
}
LOGGER.error("First: {}", mem);
mem.setUserID(ident.getPrincipal().getName());
// need to fetch ref to use attached entity
MembershipForm ref = mem.cloneTo(dao.getReference(formID, MembershipForm.class));
......@@ -132,7 +147,7 @@ public class MembershipFormResource extends AbstractRESTResource {
@POST
@Path("{id}/complete")
public Response completeForm(@PathParam("id") String formID) {
public Response completeForm(@PathParam("id") String formID, @QueryParam("force") boolean force) {
// check if user is allowed to modify these resources
Response r = checkAccess(formID);
if (r != null) {
......@@ -148,10 +163,28 @@ public class MembershipFormResource extends AbstractRESTResource {
return Response.serverError().build();
} else if (results.isEmpty()) {
return Response.status(404).build();
} else if (!force && (FormState.SUBMITTED.equals(results.get(0).getState())
|| FormState.COMPLETE.equals(results.get(0).getState()))) {
// dont send email if force param is not true and form already submitted
return Response.status(204).build();
}
// TODO actual action here
return Response.ok().build();
// send the form to the mailing service
MembershipForm mf = results.get(0);
mailer.sendToFormAuthor(mf);
// retrieve all of the info needed to post the form email
MultivaluedMap<String, String> extraparams = new MultivaluedMapImpl<>();
extraparams.add(MembershipFormAPIParameterNames.FORM_ID.getName(), formID);
List<FormOrganization> org = dao.get(new RDBMSQuery<>(wrap, filters.get(FormOrganization.class), extraparams));
List<FormWorkingGroup> wgs = dao.get(new RDBMSQuery<>(wrap, filters.get(FormWorkingGroup.class), extraparams));
List<Contact> contacts = dao.get(new RDBMSQuery<>(wrap, filters.get(Contact.class), extraparams));
// send the membership team email message
mailer.sendToMembershipTeam(mf, org.get(0), wgs, contacts);
// update the state and push the update
mf.setState(FormState.SUBMITTED);
return Response.ok(dao.add(new RDBMSQuery<>(wrap, filters.get(MembershipForm.class)), Arrays.asList(mf)))
.build();
}
}
......@@ -13,7 +13,6 @@ package org.eclipsefoundation.react.request;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.ws.rs.GET;
......
......@@ -11,8 +11,6 @@
*/
package org.eclipsefoundation.react.request;
import java.util.Set;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
......
package org.eclipsefoundation.react.service;
import java.util.List;
import org.eclipsefoundation.react.model.Contact;
import org.eclipsefoundation.react.model.FormOrganization;
import org.eclipsefoundation.react.model.FormWorkingGroup;
import org.eclipsefoundation.react.model.MembershipForm;
/**
* Interface defining emails that need to be generated and sent as part of the submission of the membership forms to the
* membership team. This interface includes emails providing feedback to the user and to the membership team.
*
* @author Martin Lowe
*
*/
public interface MailerService {
/**
* Sends an EMail message to the author of the form thanking them for their submission and interest. This request is
* asynchronous and has no return.
*
* @param form the form that is being submitted
*/
void sendToFormAuthor(MembershipForm form);
/**
* Sends an email message to the membership team regarding the submitted form. This should list all of the
* information about the form and the submission for the membership team.
*
* @param form the membership form that was submitted
* @param org the organization associated with the membership form
* @param wgs the working groups associated with the membership form
* @param contacts contacts associated with the organization and working groups for the membership form
*/
void sendToMembershipTeam(MembershipForm form, FormOrganization org, List<FormWorkingGroup> wgs,
List<Contact> contacts);
}
package org.eclipsefoundation.react.service.impl;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.react.api.AccountsAPI;
import org.eclipsefoundation.react.api.model.EclipseUser;
import org.eclipsefoundation.react.model.Contact;
import org.eclipsefoundation.react.model.FormOrganization;
import org.eclipsefoundation.react.model.FormWorkingGroup;
import org.eclipsefoundation.react.model.MembershipForm;
import org.eclipsefoundation.react.service.MailerService;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import io.quarkus.qute.Location;
import io.quarkus.qute.Template;
/**
* Default implementation of the mailer service using the Qute templating engine and the mailer extensions built into
* Quarkus.
*
* @author Martin Lowe
*
*/
@ApplicationScoped
public class DefaultMailerService implements MailerService {
@ConfigProperty(name = "eclipse.mailer.membership.inbox")
String membershipMailbox;
@Inject
Mailer mailer;
// Qute templates, generates email bodies
@Location("emails/form_author_email_web_template")
Template authorTemplateWeb;
@Location("emails/form_author_email_template")
Template authorTemplate;
@Location("emails/form_membership_email_web_template")
Template membershipTemplateWeb;
@Location("emails/form_membership_email_template")
Template membershipTemplate;
@Inject
@RestClient
AccountsAPI accounts;
@Override
public void sendToFormAuthor(MembershipForm form) {
if (form == null) {
throw new IllegalStateException("A form is required to submit for mailing");