Merge release branch

This commit is contained in:
Arpit Mohan 2020-03-13 12:47:16 +05:30
commit a892ee90b5
37 changed files with 1262 additions and 128 deletions

View File

@ -9,7 +9,6 @@ import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.core.index.Indexed;
@ -48,14 +47,9 @@ public abstract class BaseDomain implements Persistable<String> {
protected Boolean deleted = false;
@JsonIgnore
@Version
protected Long documentVersion;
@JsonIgnore
protected Set<Policy> policies;
@JsonIgnore
@Override
public boolean isNew() {
return this.getId() == null;

View File

@ -18,6 +18,7 @@
<module>postgresPlugin</module>
<module>restApiPlugin</module>
<module>mongoPlugin</module>
<module>rapidApiPlugin</module>
</modules>
</project>

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.external.plugins</groupId>
<artifactId>rapidApiPlugin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>rapidApiPlugin</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<plugin.id>rapidapi-plugin</plugin.id>
<plugin.class>com.external.plugins.RapidApiPlugin</plugin.class>
<plugin.version>1.0-SNAPSHOT</plugin.version>
<plugin.provider>tech@appsmith.com</plugin.provider>
<plugin.dependencies/>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.appsmith</groupId>
<artifactId>interfaces</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestEntries>
<Plugin-Id>${plugin.id}</Plugin-Id>
<Plugin-Class>${plugin.class}</Plugin-Class>
<Plugin-Version>${plugin.version}</Plugin-Version>
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
<Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,266 @@
package com.external.plugins;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionResult;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Property;
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.bson.internal.Base64;
import org.json.JSONObject;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RapidApiPlugin extends BasePlugin {
private static int MAX_REDIRECTS = 5;
private static ObjectMapper objectMapper;
private static String rapidApiKeyName = "X-RapidAPI-Key";
private static String rapidApiKeyValue = "f2a61def63msh9d6582090d01286p157197jsnade6f31fcae8";
public RapidApiPlugin(PluginWrapper wrapper) {
super(wrapper);
this.objectMapper = new ObjectMapper();
}
@Slf4j
@Extension
public static class RapidApiPluginExecutor implements PluginExecutor {
@Override
public Mono<Object> execute(Object connection,
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration) {
String requestBody = (actionConfiguration.getBody() == null) ? "" : actionConfiguration.getBody();
String path = (actionConfiguration.getPath() == null) ? "" : actionConfiguration.getPath();
String url = datasourceConfiguration.getUrl() + path;
HttpMethod httpMethod = actionConfiguration.getHttpMethod();
if (httpMethod == null) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "HTTPMethod must be set."));
}
WebClient.Builder webClientBuilder = WebClient.builder();
if (datasourceConfiguration.getHeaders() != null) {
addHeadersToRequest(webClientBuilder, datasourceConfiguration.getHeaders());
}
if (actionConfiguration.getHeaders() != null) {
addHeadersToRequest(webClientBuilder, actionConfiguration.getHeaders());
}
// Add the rapid api headers
webClientBuilder.defaultHeader(rapidApiKeyName, rapidApiKeyValue);
URI uri = null;
try {
uri = createFinalUriWithQueryParams(url, actionConfiguration.getQueryParameters());
System.out.println("Final URL is : " + uri.toString());
} catch (URISyntaxException e) {
e.printStackTrace();
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
}
// Build the body of the request in case of bodyFormData is not null
if (actionConfiguration.getBodyFormData() != null) {
// First set the header to specify the content type
webClientBuilder.defaultHeader("Content-Type", "application/json");
Map<String, String> strStrMap = new HashMap<String, String>();
List<Property> bodyFormData = actionConfiguration.getBodyFormData();
bodyFormData
.stream()
.map(property -> strStrMap.put(property.getKey(), property.getValue()));
log.debug("str str map from body form data is : {}", strStrMap);
JSONObject bodyJson = new JSONObject(strStrMap);
String jsonString = bodyJson.toString();
log.debug("Json string from body form data is : {}", jsonString);
}
WebClient client = webClientBuilder.build();
return httpCall(client, httpMethod, uri, requestBody, 0)
.flatMap(clientResponse -> clientResponse.toEntity(byte[].class))
.map(stringResponseEntity -> {
HttpHeaders headers = stringResponseEntity.getHeaders();
// Find the media type of the response to parse the body as required.
MediaType contentType = headers.getContentType();
byte[] body = stringResponseEntity.getBody();
HttpStatus statusCode = stringResponseEntity.getStatusCode();
ActionExecutionResult result = new ActionExecutionResult();
result.setStatusCode(statusCode.toString());
// If the HTTP response is 200, only then cache the response.
// We shouldn't cache the response even for other 2xx statuses like 201, 204 etc.
if (statusCode.equals(HttpStatus.OK)) {
result.setShouldCacheResponse(true);
}
if (headers != null) {
// Convert the headers into json tree to store in the results
String headerInJsonString;
try {
headerInJsonString = objectMapper.writeValueAsString(headers);
} catch (JsonProcessingException e) {
e.printStackTrace();
return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)));
}
try {
// Set headers in the result now
result.setHeaders(objectMapper.readTree(headerInJsonString));
} catch (IOException e) {
e.printStackTrace();
return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)));
}
}
if (body != null) {
/**TODO
* Handle XML response. Currently we only handle JSON & Image responses. The other kind of responses
* are kept as is and returned as a string.
*/
if (MediaType.APPLICATION_JSON.equals(contentType) ||
MediaType.APPLICATION_JSON_UTF8.equals(contentType) ||
MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
try {
String jsonBody = new String(body);
result.setBody(objectMapper.readTree(jsonBody));
} catch (IOException e) {
e.printStackTrace();
return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)));
}
} else if (MediaType.IMAGE_GIF.equals(contentType) ||
MediaType.IMAGE_JPEG.equals(contentType) ||
MediaType.IMAGE_PNG.equals(contentType)) {
String encode = Base64.encode(body);
result.setBody(encode);
} else {
// If the body is not of JSON type, just set it as is.
String bodyString = new String(body);
result.setBody(bodyString.trim());
}
}
return result;
})
.doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)));
}
private Mono<ClientResponse> httpCall(WebClient webClient, HttpMethod httpMethod, URI uri, String requestBody, int iteration) {
if (iteration == MAX_REDIRECTS) {
System.out.println("Exceeded the http redirect limits. Returning error");
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Exceeded the HTTO redirect limits of " + MAX_REDIRECTS));
}
return webClient
.method(httpMethod)
.uri(uri)
.body(BodyInserters.fromObject(requestBody))
.exchange()
.doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)))
.flatMap(res -> {
ClientResponse response = (ClientResponse) res;
if (response.statusCode().is3xxRedirection()) {
String redirectUrl = response.headers().header("Location").get(0);
/**
* TODO
* In case the redirected URL is not absolute (complete), create the new URL using the relative path
* This particular scenario is seen in the URL : https://rickandmortyapi.com/api/character
* It redirects to partial URI : /api/character/
* In this scenario we should convert the partial URI to complete URI
*/
URI redirectUri = null;
try {
redirectUri = new URI(redirectUrl);
} catch (URISyntaxException e) {
e.printStackTrace();
}
return httpCall(webClient, httpMethod, redirectUri, requestBody, iteration + 1);
}
return Mono.just(response);
});
}
@Override
public Object datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
return null;
}
@Override
public void datasourceDestroy(Object connection) {
}
@Override
public Boolean isDatasourceValid(DatasourceConfiguration datasourceConfiguration) {
if (datasourceConfiguration.getUrl() == null) {
System.out.println("URL is null. Data validation failed");
return false;
}
// Check for URL validity
try {
new URL(datasourceConfiguration.getUrl()).toURI();
return true;
} catch (Exception e) {
System.out.println("URL is invalid. Data validation failed");
return false;
}
}
private void addHeadersToRequest(WebClient.Builder webClientBuilder, List<Property> headers) {
for (Property header : headers) {
if (header.getKey() != null && !header.getKey().isEmpty()) {
webClientBuilder.defaultHeader(header.getKey(), header.getValue());
}
}
}
private URI createFinalUriWithQueryParams(String url, List<Property> queryParams) throws URISyntaxException {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.newInstance();
uriBuilder.uri(new URI(url));
if (queryParams != null) {
for (Property queryParam : queryParams) {
if (queryParam.getKey() != null && !queryParam.getKey().isEmpty()) {
uriBuilder.queryParam(queryParam.getKey(), queryParam.getValue());
}
}
}
return uriBuilder.build(true).toUri();
}
/**
* TODO :
* Add a function which is called during import of a template to an action. As part of that do the following :
* 1. Get the provider and the template
* 2. Check if the provider is subscribed to, and if not, subscribe.
* 3. Set Property field isRedacted for fields like host, etc. These fields in turn would not be displayed to
* the user during GET Actions.
*/
}
}

View File

@ -0,0 +1,18 @@
package com.external.plugins;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
/**
* Unit test for simple App.
*/
public class RapidApiPluginTest {
/**
* Rigorous Test :-)
*/
@Test
public void shouldAnswerWithTrue() {
assertTrue(true);
}
}

View File

@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@ -45,7 +46,8 @@ public class ActionController extends BaseController<ActionService, Action, Stri
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<Action>> create(@Valid @RequestBody Action resource) throws AppsmithException {
public Mono<ResponseDTO<Action>> create(@Valid @RequestBody Action resource,
@RequestHeader(name = "Origin", required = false) String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
return actionCollectionService.createAction(resource)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@ -35,7 +36,8 @@ public class ApplicationController extends BaseController<ApplicationService, Ap
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<Application>> create(@Valid @RequestBody Application resource) throws AppsmithException {
public Mono<ResponseDTO<Application>> create(@Valid @RequestBody Application resource,
@RequestHeader(name = "Origin", required = false) String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
return applicationPageService.createApplication(resource)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import reactor.core.publisher.Mono;
@ -29,7 +30,8 @@ public abstract class BaseController<S extends CrudService, T extends BaseDomain
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<T>> create(@Valid @RequestBody T resource) throws AppsmithException {
public Mono<ResponseDTO<T>> create(@Valid @RequestBody T resource,
@RequestHeader(name = "Origin", required = false) String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
return service.create(resource)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));

View File

@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@ -32,7 +33,8 @@ public class CollectionController extends BaseController<CollectionService, Coll
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<Collection>> create(@Valid @RequestBody Collection resource) throws AppsmithException {
public Mono<ResponseDTO<Collection>> create(@Valid @RequestBody Collection resource,
@RequestHeader(name = "Origin", required = false) String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
return actionCollectionService.createCollection(resource)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));

View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@ -37,7 +38,8 @@ public class PageController extends BaseController<PageService, Page, String> {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<Page>> create(@Valid @RequestBody Page resource) throws AppsmithException {
public Mono<ResponseDTO<Page>> create(@Valid @RequestBody Page resource,
@RequestHeader(name = "Origin", required = false) String originHeader) {
log.debug("Going to create resource {}", resource.getClass().getName());
return applicationPageService.createPage(resource)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));

View File

@ -12,6 +12,7 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;

View File

@ -2,9 +2,15 @@ package com.appsmith.server.controllers;
import com.appsmith.external.models.Provider;
import com.appsmith.server.constants.Url;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.services.ProviderService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.List;
@RestController
@RequestMapping(Url.PROVIDER_URL)
@ -13,4 +19,10 @@ public class ProviderController extends BaseController<ProviderService, Provider
public ProviderController(ProviderService service) {
super(service);
}
@GetMapping("/categories")
public Mono<ResponseDTO<List<String>>> getAllCategories() {
return service.getAllCategories()
.map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null));
}
}

View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
@ -43,7 +44,9 @@ public class RestApiImportController {
public Mono<ResponseDTO<Action>> create(@Valid @RequestBody Object input,
@RequestParam RestApiImporterType type,
@RequestParam String pageId,
@RequestParam String name) {
@RequestParam String name,
@RequestHeader(name = "Origin", required = false) String originHeader
) {
log.debug("Going to import API");
ApiImporter service;
@ -61,7 +64,8 @@ public class RestApiImportController {
@PostMapping("/postman")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<TemplateCollection>> importPostmanCollection(@RequestBody Object input, @RequestParam String type) {
public Mono<ResponseDTO<TemplateCollection>> importPostmanCollection(@RequestBody Object input,
@RequestParam String type) {
return Mono.just(postmanImporterService.importPostmanCollection(input))
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}

View File

@ -1,33 +0,0 @@
package com.appsmith.server.controllers;
import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.services.SignupService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping(Url.SIGNUP_URL)
public class SignupController {
private final SignupService signupService;
@Autowired
public SignupController(SignupService signupService) {
this.signupService = signupService;
}
@PostMapping("/organization")
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<Organization>> signupOrganization(@RequestBody Organization organization) {
return signupService.createOrganization(organization)
.map(org -> new ResponseDTO<>(HttpStatus.CREATED.value(), org, null));
}
}

View File

@ -5,9 +5,11 @@ import com.appsmith.server.domains.InviteUser;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ResetUserPasswordDTO;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.dtos.UserProfileDTO;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserOrganizationService;
import com.appsmith.server.services.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
@ -18,11 +20,15 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@RestController
@RequestMapping(Url.USER_URL)
@Slf4j
public class UserController extends BaseController<UserService, User, String> {
private final SessionUserService sessionUserService;
@ -37,6 +43,14 @@ public class UserController extends BaseController<UserService, User, String> {
this.userOrganizationService = userOrganizationService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<ResponseDTO<User>> create(@Valid @RequestBody User resource,
@RequestHeader(name = "Origin", required = false) String originHeader) {
return service.createUser(resource, originHeader)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}
@PutMapping("/switchOrganization/{orgId}")
public Mono<ResponseDTO<User>> setCurrentOrganization(@PathVariable String orgId) {
return service.switchCurrentOrganization(orgId)
@ -77,12 +91,19 @@ public class UserController extends BaseController<UserService, User, String> {
.map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null));
}
@Deprecated
@GetMapping("/me")
public Mono<ResponseDTO<User>> getUserProfile() {
return sessionUserService.getCurrentUser()
.map(user -> new ResponseDTO<>(HttpStatus.OK.value(), user, null));
}
@GetMapping("/profile")
public Mono<ResponseDTO<UserProfileDTO>> getEnhancedUserProfile() {
return service.getUserProfile()
.map(user -> new ResponseDTO<>(HttpStatus.OK.value(), user, null));
}
/**
* This function creates an invite for a new user to join the Appsmith platform. We require the Origin header
* in order to construct client facing URLs that will be sent to the user via email.
@ -104,8 +125,9 @@ public class UserController extends BaseController<UserService, User, String> {
}
@PutMapping("/invite/confirm")
public Mono<ResponseDTO<Boolean>> confirmInviteUser(@RequestBody InviteUser inviteUser) {
return service.confirmInviteUser(inviteUser)
public Mono<ResponseDTO<Boolean>> confirmInviteUser(@RequestBody InviteUser inviteUser,
@RequestHeader("Origin") String originHeader) {
return service.confirmInviteUser(inviteUser, originHeader)
.map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null));
}
}

View File

@ -7,6 +7,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
@ -50,6 +51,13 @@ public class Action extends BaseDomain {
String templateId; //If action is created via a template, store the id here.
String providerId; //If action is created via a template, store the template's provider id here.
@Transient
ActionProvider provider;
Documentation documentation;
/**
* If the Datasource is null, create one and set the autoGenerated flag to true. This is required because spring-data
* cannot add the createdAt and updatedAt properties for null embedded objects. At this juncture, we couldn't find

View File

@ -0,0 +1,20 @@
package com.appsmith.server.domains;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ActionProvider {
String name;
String imageUrl;
String url;
String description;
String credentialSteps;
}

View File

@ -0,0 +1,13 @@
package com.appsmith.server.domains;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Documentation {
String text;
String url;
}

View File

@ -1,11 +1,13 @@
package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.server.dtos.ApplicationNameIdDTO;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.security.core.GrantedAuthority;
@ -13,6 +15,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

View File

@ -0,0 +1,13 @@
package com.appsmith.server.dtos;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ApplicationNameIdDTO {
String id;
String name;
}

View File

@ -0,0 +1,19 @@
package com.appsmith.server.dtos;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.User;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class UserProfileDTO {
User user;
Organization currentOrganization;
List<ApplicationNameIdDTO> applications;
}

View File

@ -48,7 +48,7 @@ public class EmailSender {
* @throws MailException
*/
public void sendMail(String to, String subject, String text) {
log.debug("Got request to send email to: {} with subject: {} and text: {}", to, subject, text);
log.debug("Got request to send email to: {} with subject: {}", to, subject);
// Don't send an email for local, dev or test environments
if (!emailConfig.isEmailEnabled()) {
return;

View File

@ -10,6 +10,7 @@ import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.constants.AnalyticsEvents;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.ActionProvider;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
@ -68,6 +69,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
private final DatasourceContextService datasourceContextService;
private final PluginExecutorHelper pluginExecutorHelper;
private final SessionUserService sessionUserService;
private final ProviderService providerService;
@Autowired
public ActionServiceImpl(Scheduler scheduler,
@ -82,7 +84,8 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
ObjectMapper objectMapper,
DatasourceContextService datasourceContextService,
PluginExecutorHelper pluginExecutorHelper,
SessionUserService sessionUserService) {
SessionUserService sessionUserService,
ProviderService providerService) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.repository = repository;
this.datasourceService = datasourceService;
@ -92,6 +95,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
this.datasourceContextService = datasourceContextService;
this.pluginExecutorHelper = pluginExecutorHelper;
this.sessionUserService = sessionUserService;
this.providerService = providerService;
}
/**
@ -271,6 +275,7 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
return action;
}
@Override
public Mono<ActionExecutionResult> executeAction(ExecuteActionDTO executeActionDTO) {
Action actionFromDto = executeActionDTO.getAction();
@ -478,13 +483,8 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
Mono<Action> actionMono = repository.findById(id)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "action", id)));
return actionMono
.flatMap(toDelete ->
repository.delete(toDelete)
.thenReturn(toDelete))
.map(deletedObj -> {
analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Action) deletedObj);
return (Action) deletedObj;
});
.flatMap(toDelete -> repository.delete(toDelete).thenReturn(toDelete))
.flatMap(deletedObj -> analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Action) deletedObj));
}
@Override
@ -523,6 +523,27 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
.flatMapMany(orgId -> {
actionExample.setOrganizationId(orgId);
return repository.findAll(Example.of(actionExample), sort);
})
.flatMap(action -> {
if ((action.getTemplateId()!=null) && (action.getProviderId() != null)) {
// In case of an action which was imported from a 3P API, fill in the extra information of the
// provider required by the front end UI.
return providerService
.getById(action.getProviderId())
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "Provider")))
.map(provider -> {
ActionProvider actionProvider = new ActionProvider();
actionProvider.setName(provider.getName());
actionProvider.setCredentialSteps(provider.getCredentialSteps());
actionProvider.setDescription(provider.getDescription());
actionProvider.setImageUrl(provider.getImageUrl());
actionProvider.setUrl(provider.getUrl());
action.setProvider(actionProvider);
return action;
});
}
return Mono.just(action);
});
}

View File

@ -43,8 +43,17 @@ public class AnalyticsService<T extends BaseDomain> {
});
}
private User createAnonymousUser() {
User user = new User();
user.setId("anonymousUser");
return user;
}
public Mono<T> sendEvent(String eventTag, T object) {
Mono<User> userMono = sessionUserService.getCurrentUser();
// We will create an anonymous user object for event tracking if no user is present
// Without this, a lot of flows meant for anonymous users will error out
Mono<User> userMono = sessionUserService.getCurrentUser()
.defaultIfEmpty(createAnonymousUser());
return userMono
.map(user -> {
HashMap<String, String> analyticsProperties = new HashMap<>();

View File

@ -261,10 +261,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService {
.flatMap(application -> applicationService.archive(application));
return applicationMono
.map(deletedObj -> {
analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Application) deletedObj);
return (Application) deletedObj;
});
.flatMap(deletedObj -> analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Application) deletedObj));
}
}

View File

@ -9,6 +9,7 @@ import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.repositories.BaseRepository;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
@ -22,6 +23,7 @@ import reactor.core.scheduler.Scheduler;
import javax.validation.Validator;
import java.util.Map;
@Slf4j
public abstract class BaseService<R extends BaseRepository, T extends BaseDomain, ID> implements CrudService<T, ID> {
final Scheduler scheduler;
@ -90,10 +92,7 @@ public abstract class BaseService<R extends BaseRepository, T extends BaseDomain
return Mono.just(object)
.flatMap(this::validateObject)
.flatMap(repository::save)
.map(savedObj -> {
analyticsService.sendEvent(AnalyticsEvents.CREATE + "_" + savedObj.getClass().getSimpleName().toUpperCase(), (T) savedObj);
return savedObj;
});
.flatMap(savedObj -> analyticsService.sendEvent(AnalyticsEvents.CREATE + "_" + savedObj.getClass().getSimpleName().toUpperCase(), (T) savedObj));
}
protected DBObject getDbObject(Object o) {

View File

@ -4,6 +4,7 @@ import com.appsmith.external.models.ApiTemplate;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Action;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Documentation;
import com.appsmith.server.dtos.AddItemToPageDTO;
import com.appsmith.server.dtos.ItemDTO;
import com.appsmith.server.dtos.ItemType;
@ -66,13 +67,22 @@ public class ItemServiceImpl implements ItemService {
action.setName(addItemToPageDTO.getName());
action.setPageId(addItemToPageDTO.getPageId());
action.setTemplateId(apiTemplate.getId());
action.setProviderId(apiTemplate.getProviderId());
Documentation documentation = new Documentation();
documentation.setText(apiTemplate.getApiTemplateConfiguration().getDocumentation());
documentation.setUrl(apiTemplate.getApiTemplateConfiguration().getDocumentationUrl());
action.setDocumentation(documentation);
/** TODO
* Also hit the Marketplace to update the number of imports.
*/
// Set Action Fields
action.setActionConfiguration(apiTemplate.getActionConfiguration());
action.setCacheResponse(apiTemplate.getApiTemplateConfiguration().getSampleResponse().getBody().toString());
if (apiTemplate.getApiTemplateConfiguration().getSampleResponse() != null &&
apiTemplate.getApiTemplateConfiguration().getSampleResponse().getBody() != null ) {
action.setCacheResponse(apiTemplate.getApiTemplateConfiguration().getSampleResponse().getBody().toString());
}
return pluginService
.findByPackageName(apiTemplate.getPackageName())

View File

@ -118,10 +118,8 @@ public class PageServiceImpl extends BaseService<PageRepository, Page, String> i
});
});
return pageMono.map(deletedObj -> {
analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Page) deletedObj);
return (Page) deletedObj;
});
return pageMono
.flatMap(deletedObj -> analyticsService.sendEvent(AnalyticsEvents.DELETE + "_" + deletedObj.getClass().getSimpleName().toUpperCase(), (Page) deletedObj));
}
@Override

View File

@ -1,6 +1,10 @@
package com.appsmith.server.services;
import com.appsmith.external.models.Provider;
import reactor.core.publisher.Mono;
import java.util.List;
public interface ProviderService extends CrudService<Provider, String> {
public Mono<List<String>> getAllCategories();
}

View File

@ -11,16 +11,25 @@ import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import javax.validation.Validator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
@Slf4j
public class ProviderServiceImpl extends BaseService<ProviderRepository, Provider, String> implements ProviderService {
private static final List<String> CATEGORIES = Arrays.asList("Business","Visual Recognition","Location","Science",
"Food","Travel, Transportation","Music","Tools","Text Analysis","Weather","Gaming","SMS","Events","Health, Fitness",
"Payments","Financial","Translation","Storage","Logistics","Database","Search","Reward","Mapping","Machine Learning",
"Email","News, Media","Video, Images","eCommerce","Medical","Devices","Business Software","Advertising","Education",
"Media","Social","Commerce","Communication","Other","Monitoring","Energy");
private static final String DEFAULT_CATEGORY = "Business Software";
public ProviderServiceImpl(Scheduler scheduler,
Validator validator,
MongoConverter mongoConverter,
@ -39,12 +48,21 @@ public class ProviderServiceImpl extends BaseService<ProviderRepository, Provide
providerExample.setName(params.getFirst(FieldName.NAME));
}
List<String> categories = new ArrayList<>();
if (params.getFirst(FieldName.CATEGORY) != null) {
List<String> categories = new ArrayList<>();
categories.add(params.getFirst(FieldName.CATEGORY));
providerExample.setCategories(categories);
} else {
// No category has been provided. Set the default category.
categories.add(DEFAULT_CATEGORY);
}
providerExample.setCategories(categories);
return repository.findAll(Example.of(providerExample), sort);
}
@Override
public Mono<List<String>> getAllCategories() {
return Mono.just(CATEGORIES);
}
}

View File

@ -3,6 +3,7 @@ package com.appsmith.server.services;
import com.appsmith.server.domains.InviteUser;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ResetUserPasswordDTO;
import com.appsmith.server.dtos.UserProfileDTO;
import org.springframework.security.core.GrantedAuthority;
import reactor.core.publisher.Mono;
@ -24,7 +25,11 @@ public interface UserService extends CrudService<User, String> {
Mono<Boolean> verifyInviteToken(String email, String token);
Mono<Boolean> confirmInviteUser(InviteUser inviteUser);
Mono<Boolean> confirmInviteUser(InviteUser inviteUser, String originHeader);
Mono<Collection<GrantedAuthority>> getAnonymousAuthorities();
Mono<UserProfileDTO> getUserProfile();
Mono<User> createUser(User user, String originHeader);
}

View File

@ -1,22 +1,26 @@
package com.appsmith.server.services;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.InviteUser;
import com.appsmith.server.domains.LoginSource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.PasswordResetToken;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ApplicationNameIdDTO;
import com.appsmith.server.dtos.ResetUserPasswordDTO;
import com.appsmith.server.dtos.UserProfileDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.BeanCopyUtils;
import com.appsmith.server.notifications.EmailSender;
import com.appsmith.server.repositories.GroupRepository;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.InviteUserRepository;
import com.appsmith.server.repositories.PasswordResetTokenRepository;
import com.appsmith.server.repositories.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.security.core.GrantedAuthority;
@ -25,6 +29,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -33,6 +38,7 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@ -49,15 +55,17 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
private final PasswordResetTokenRepository passwordResetTokenRepository;
private final PasswordEncoder passwordEncoder;
private final EmailSender emailSender;
private final GroupRepository groupRepository;
private final InviteUserRepository inviteUserRepository;
private final UserOrganizationService userOrganizationService;
private final ApplicationRepository applicationRepository;
private static final String WELCOME_USER_EMAIL_TEMPLATE = "email/welcomeUserTemplate.html";
private static final String INVITE_USER_EMAIL_TEMPLATE = "email/inviteUserTemplate.html";
private static final String INVITE_USER_EMAIL_TEMPLATE = "email/inviteUserCreatorTemplate.html";
private static final String FORGOT_PASSWORD_EMAIL_TEMPLATE = "email/forgotPasswordTemplate.html";
private static final String INVITE_USER_CLIENT_URL_FORMAT = "%s/user/createPassword?token=%s&email=%s";
private static final String FORGOT_PASSWORD_CLIENT_URL_FORMAT = "%s/user/resetPassword?token=%s&email=%s";
// We default the origin header to the production deployment of the client's URL
private static final String DEFAULT_ORIGIN_HEADER = "https://app.appsmith.com";
@Autowired
public UserServiceImpl(Scheduler scheduler,
@ -71,9 +79,9 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
PasswordResetTokenRepository passwordResetTokenRepository,
PasswordEncoder passwordEncoder,
EmailSender emailSender,
GroupRepository groupRepository,
InviteUserRepository inviteUserRepository,
UserOrganizationService userOrganizationService) {
UserOrganizationService userOrganizationService,
ApplicationRepository applicationRepository) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.repository = repository;
this.organizationService = organizationService;
@ -82,9 +90,9 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
this.passwordResetTokenRepository = passwordResetTokenRepository;
this.passwordEncoder = passwordEncoder;
this.emailSender = emailSender;
this.groupRepository = groupRepository;
this.inviteUserRepository = inviteUserRepository;
this.userOrganizationService = userOrganizationService;
this.applicationRepository = applicationRepository;
}
@Override
@ -274,35 +282,52 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORIGIN));
}
// Create an invite token for the user. This token is linked to the email ID and the organization to which the user was invited.
// Create an invite token for the user. This token is linked to the email ID and the organization to which the
// user was invited.
String token = UUID.randomUUID().toString();
return sessionUserService.getCurrentUser()
.map(reqUser -> {
// Caching the response from sessionUserService because it's re-used multiple times in this flow
Mono<User> currentUserMono = sessionUserService.getCurrentUser().cache();
Mono<InviteUser> inviteUserMono = currentUserMono
.map(currentUser -> {
log.debug("Got request to invite user {} by user: {} for org: {}",
user.getEmail(), reqUser.getEmail(), reqUser.getCurrentOrganizationId());
user.getEmail(), currentUser.getEmail(), currentUser.getCurrentOrganizationId());
InviteUser inviteUser = new InviteUser();
inviteUser.setEmail(user.getEmail());
inviteUser.setCurrentOrganizationId(reqUser.getCurrentOrganizationId());
inviteUser.setCurrentOrganizationId(currentUser.getCurrentOrganizationId());
inviteUser.setToken(passwordEncoder.encode(token));
inviteUser.setGroupIds(user.getGroupIds());
inviteUser.setPermissions(user.getPermissions());
inviteUser.setInviterUserId(reqUser.getId());
inviteUser.setInviterUserId(currentUser.getId());
return inviteUser;
})
// Save the invited user in the DB
.flatMap(inviteUserRepository::save)
// Send an email to the invited user with the token
.map(inviteUser -> {
.flatMap(inviteUserRepository::save);
Mono<Organization> currentOrgMono = currentUserMono
.flatMap(currentUser -> organizationService.findById(currentUser.getCurrentOrganizationId()));
// Send an email to the invited user with the token
return Mono.zip(currentUserMono, inviteUserMono, currentOrgMono)
.map(tuple -> {
User currentUser = tuple.getT1();
InviteUser inviteUser = tuple.getT2();
Organization currentUserOrg = tuple.getT3();
log.debug("Going to send email for invite user to {} with token {}", inviteUser.getEmail(), token);
try {
String inviteUrl = String.format(INVITE_USER_CLIENT_URL_FORMAT, originHeader,
URLEncoder.encode(token, StandardCharsets.UTF_8),
URLEncoder.encode(inviteUser.getEmail(), StandardCharsets.UTF_8));
Map<String, String> params = Map.of(
"token", token,
"inviteUrl", inviteUrl);
Map<String, String> params = new HashMap<>();
params.put("token", token);
params.put("inviteUrl", inviteUrl);
if (!StringUtils.isEmpty(currentUser.getName())) {
params.put("Inviter_First_Name", currentUser.getName());
} else {
params.put("Inviter_First_Name", currentUser.getEmail());
}
params.put("inviter_org_name", currentUserOrg.getName());
String emailBody = emailSender.replaceEmailTemplate(INVITE_USER_EMAIL_TEMPLATE, params);
emailSender.sendMail(inviteUser.getEmail(), "Invite for Appsmith", emailBody);
} catch (IOException e) {
@ -310,7 +335,6 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
}
return inviteUser;
});
}
/**
@ -338,7 +362,7 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
* @return
*/
@Override
public Mono<Boolean> confirmInviteUser(InviteUser inviteUser) {
public Mono<Boolean> confirmInviteUser(InviteUser inviteUser, String originHeader) {
if (inviteUser.getToken() == null || inviteUser.getToken().isEmpty()) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "token"));
}
@ -376,7 +400,7 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
log.debug("The invited user {} doesn't exist in the system. Creating a new record", inviteUser.getEmail());
// The user doesn't exist in the system. Create a new user object
newUser.setPassword(inviteUser.getPassword());
return this.create(newUser)
return this.createUser(newUser, originHeader)
.flatMap(createdUser -> userOrganizationService.addUserToOrganization(newUser.getCurrentOrganizationId(), createdUser))
.thenReturn(newUser)
.flatMap(userToDelete -> inviteUserRepository.delete(userToDelete))
@ -390,6 +414,11 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
.map(user -> user.getAuthorities());
}
@Override
public Mono<User> create(User user) {
return createUser(user, null);
}
/**
* This function creates a new user in the system. Primarily used by new users signing up for the first time on the
* platform. This flow also ensures that a personal workspace name is created for the user. The new user is then
@ -401,7 +430,12 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
* @return
*/
@Override
public Mono<User> create(User user) {
public Mono<User> createUser(User user, String originHeader) {
if (originHeader == null || originHeader.isBlank()) {
// Default to the production link
originHeader = DEFAULT_ORIGIN_HEADER;
}
final String finalOriginHeader = originHeader;
// Only encode the password if it's a form signup. For OAuth signups, we don't need password
if (LoginSource.FORM.equals(user.getSource())) {
@ -418,8 +452,8 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
firstName = user.getEmail().split("@")[0];
}
String personalWorkspaceName = firstName + "'s Personal Workspace";
personalOrg.setName(personalWorkspaceName);
String personalOrganizationName = firstName + "'s Personal Organization";
personalOrg.setName(personalOrganizationName);
// Save the new user
Mono<User> savedUserMono = super.create(user);
@ -434,7 +468,10 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
.map(savedUser -> {
// Send an email to the user welcoming them to the Appsmith platform
try {
Map<String, String> params = Map.of("personalWorkspaceName", personalWorkspaceName);
Map<String, String> params = new HashMap<>();
params.put("personalOrganizationName", personalOrganizationName);
params.put("firstName", savedUser.getName());
params.put("appsmithLink", finalOriginHeader);
String emailBody = emailSender.replaceEmailTemplate(WELCOME_USER_EMAIL_TEMPLATE, params);
emailSender.sendMail(savedUser.getEmail(), "Welcome to Appsmith", emailBody);
} catch (IOException e) {
@ -496,4 +533,35 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> i
// Doesn't work without this.
.map(user -> (UserDetails) user);
}
@Override
public Mono<UserProfileDTO> getUserProfile() {
return sessionUserService.getCurrentUser()
.flatMap(user -> {
String currentOrganizationId = user.getCurrentOrganizationId();
UserProfileDTO userProfile = new UserProfileDTO();
userProfile.setUser(user);
Mono<UserProfileDTO> userProfileDTOMono = organizationService.findById(currentOrganizationId)
.flatMap(org -> {
userProfile.setCurrentOrganization(org);
Application applicationExample = new Application();
applicationExample.setOrganizationId(org.getId());
return applicationRepository.findAll(Example.of(applicationExample))
.map(application -> {
ApplicationNameIdDTO dto = new ApplicationNameIdDTO();
dto.setId(application.getId());
dto.setName(application.getName());
return dto;
}).collectList()
.map(dtos -> {
userProfile.setApplications(dtos);
return userProfile;
});
});
return userProfileDTOMono;
});
}
}

View File

@ -24,7 +24,7 @@ spring.security.oauth2.client.provider.github.userNameAttribute=login
oauth2.allowed-domains=
# Segment & Rollbar Properties
segment.writeKey=FIRLqUgMYuTlyS6yqOE2hBGZs5umkWhr
segment.writeKey=B3UBOacfOky4l6dfk5xyR6Dh8vUZYizW
com.rollbar.access-token=b91c4d5b9cac444088f4db9216ed6f42
com.rollbar.environment=development

View File

@ -1,10 +1,189 @@
<html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {width: 600px;margin: 0 auto;}
table {border-collapse: collapse;}
table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;}
img {-ms-interpolation-mode: bicubic;}
</style>
<![endif]-->
<style type="text/css">
body, p, div {
font-family: arial,helvetica,sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188E6;
text-decoration: none;
}
p { margin: 0; padding: 0; }
table.wrapper {
width:100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width:480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start--><!--End Head user entered-->
</head>
<body>
You can reset your password by clicking <a href={{resetUrl}}>this link</a>.
<br /><br />
Alternatively, you can copy paste the following URL in your browser: {{resetUrl}}
<br /><br />
Cheers, <br />
Appsmith
</body>
</html>
<center class="wrapper" data-link-color="#1188E6" data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tbody><tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0">
<tbody><tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody><tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%; max-width:600px;" align="center">
<tbody><tr>
<td role="modules-container" style="padding:0px 0px 0px 0px; color:#000000; text-align:left;" bgcolor="#ffffff" width="100%" align="left"><table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tbody><tr>
<td role="module-content">
<p></p>
</td>
</tr>
</tbody></table><table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="40dbb7f1-8428-4188-86b0-1b0245659a17">
<tbody>
<tr>
<td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;" valign="top" align="center">
<a href="https://www.appsmith.com/"><img class="max-width" border="0" style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:25% !important; width:25%; height:auto !important;" width="150" alt="" data-proportionally-constrained="true" data-responsive="true" src="http://cdn.mcauto-images-production.sendgrid.net/4bbae2fffe647858/b21738f2-3a49-4774-aae9-c8e80ad9c26e/924x284.png"></a>
</td>
</tr>
</tbody>
</table><table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="71d7e9fb-0f3b-43f4-97e1-994b33bfc82a" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="padding:0px 0px 10px 0px; line-height:22px; text-align:inherit; background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff" role="module-content"><div><div style="font-family: inherit; text-align: inherit; margin-left: 0px"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Hello,</span></div>
<div style="font-family: inherit; text-align: inherit; margin-left: 0px"><br></div>
<div style="font-family: inherit; text-align: start"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Forgot the password to your Appsmith account? No worries, we've got you covered.</span></div><div></div></div></td>
</tr>
</tbody>
</table><table border="0" cellpadding="0" cellspacing="0" class="module" data-role="module-button" data-type="button" role="module" style="table-layout:fixed;" width="100%" data-muid="f00155e0-813d-4e9b-b61d-384e6f99e5b7">
<tbody>
<tr>
<td align="center" bgcolor="" class="outer-td" style="padding:0px 0px 0px 0px;">
<table border="0" cellpadding="0" cellspacing="0" class="wrapper-mobile" style="text-align:center;">
<tbody>
<tr>
<td align="center" bgcolor="#ff6d2d" class="inner-td" style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;">
<a href="{{resetUrl}}" style="background-color:#ff6d2d; border:1px solid #ff6d2d; border-color:#ff6d2d; border-radius:6px; border-width:1px; color:#ffffff; display:inline-block; font-weight:400; letter-spacing:0px; line-height:6px; padding:12px 18px 12px 18px; text-align:center; text-decoration:none; border-style:solid; font-family:tahoma,geneva,sans-serif; font-size:16px;" target="_blank">Reset Password</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table><table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="cab2544f-5a6c-49a0-b246-efe5ac8c5208" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="padding:18px 0px 18px 0px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content"><div><div style="font-family: inherit; text-align: start"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; font-size: 14px">The link will expire in 48 hours. If you didn't request a password reset, you can safely ignore this email.</span></div>
<div style="font-family: inherit; text-align: start"><br></div>
<div style="font-family: inherit; text-align: inherit; margin-left: 0px"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; font-size: 14px; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial">Cheers</span></div>
<div style="font-family: inherit; text-align: inherit; margin-left: 0px"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; font-size: 14px; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial">Devs at Appsmith</span></div><div></div></div></td>
</tr>
</tbody>
</table></td>
</tr>
</tbody></table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</div>
</center>
</body></html>

View File

@ -0,0 +1,187 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {width: 600px;margin: 0 auto;}
table {border-collapse: collapse;}
table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;}
img {-ms-interpolation-mode: bicubic;}
</style>
<![endif]-->
<style type="text/css">
body, p, div {
font-family: arial,helvetica,sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188E6;
text-decoration: none;
}
p { margin: 0; padding: 0; }
table.wrapper {
width:100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width:480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start--><!--End Head user entered-->
</head>
<body>
<center class="wrapper" data-link-color="#1188E6" data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tbody><tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0">
<tbody><tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody><tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%; max-width:600px;" align="center">
<tbody><tr>
<td role="modules-container" style="padding:0px 0px 0px 0px; color:#000000; text-align:left;" bgcolor="#ffffff" width="100%" align="left"><table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tbody><tr>
<td role="module-content">
<p></p>
</td>
</tr>
</tbody></table><table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="40dbb7f1-8428-4188-86b0-1b0245659a17">
<tbody>
<tr>
<td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;" valign="top" align="center">
<a href="https://www.appsmith.com/"><img class="max-width" border="0" style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:25% !important; width:25%; height:auto !important;" width="150" alt="" data-proportionally-constrained="true" data-responsive="true" src="http://cdn.mcauto-images-production.sendgrid.net/4bbae2fffe647858/b21738f2-3a49-4774-aae9-c8e80ad9c26e/924x284.png"></a>
</td>
</tr>
</tbody>
</table><table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="71d7e9fb-0f3b-43f4-97e1-994b33bfc82a" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="padding:0px 0px 18px 0px; line-height:22px; text-align:inherit; background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff" role="module-content"><div><div style="font-family: inherit; text-align: center"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959"><strong>You've been invited to collaborate.</strong></span></div>
<div style="font-family: inherit; text-align: center"><br></div>
<div style="font-family: inherit; text-align: inherit; margin-left: 0px"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">{{Inviter_First_Name}} has invited you to collaborate on the organization "<strong>{{inviter_org_name}}</strong>" in Appsmith.</span></div><div></div></div></td>
</tr>
</tbody>
</table><table border="0" cellpadding="0" cellspacing="0" class="module" data-role="module-button" data-type="button" role="module" style="table-layout:fixed;" width="100%" data-muid="f00155e0-813d-4e9b-b61d-384e6f99e5b7">
<tbody>
<tr>
<td align="center" bgcolor="" class="outer-td" style="padding:0px 0px 0px 0px;">
<table border="0" cellpadding="0" cellspacing="0" class="wrapper-mobile" style="text-align:center;">
<tbody>
<tr>
<td align="center" bgcolor="#ff6d2d" class="inner-td" style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;">
<a href="{{inviteUrl}}" style="background-color:#ff6d2d; border:1px solid #ff6d2d; border-color:#ff6d2d; border-radius:6px; border-width:1px; color:#ffffff; display:inline-block; font-weight:400; letter-spacing:0px; line-height:6px; padding:12px 18px 12px 18px; text-align:center; text-decoration:none; border-style:solid; font-family:tahoma,geneva,sans-serif; font-size:16px;" target="_blank">Accept invite</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table><table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="cab2544f-5a6c-49a0-b246-efe5ac8c5208" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="padding:0px 0px 0px 0px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content"><div><div style="font-family: inherit; text-align: start"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; font-size: 14px">Cheers</span></div>
<div style="font-family: inherit; text-align: start"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; font-size: 14px">Devs at Appsmith</span></div><div></div></div></td>
</tr>
</tbody>
</table></td>
</tr>
</tbody></table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</div>
</center>
</body></html>

View File

@ -1,11 +0,0 @@
<html>
<body>
You've been invited to the Appsmith platform. Please complete your sign up by clicking <a href={{inviteUrl}}>this link</a><br /><br />
Alternatively, you can copy & paste the following url in your browser: {{inviteUrl}}
<br /><br />
For reference, your invite token is: {{token}}
<br /><br />
Cheers,
Appsmith
</body>
</html>

View File

@ -1,9 +1,203 @@
<html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<!--<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if (gte mso 9)|(IE)]>
<style type="text/css">
body {width: 600px;margin: 0 auto;}
table {border-collapse: collapse;}
table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;}
img {-ms-interpolation-mode: bicubic;}
</style>
<![endif]-->
<style type="text/css">
body, p, div {
font-family: arial,helvetica,sans-serif;
font-size: 14px;
}
body {
color: #000000;
}
body a {
color: #1188E6;
text-decoration: none;
}
p { margin: 0; padding: 0; }
table.wrapper {
width:100% !important;
table-layout: fixed;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img.max-width {
max-width: 100% !important;
}
.column.of-2 {
width: 50%;
}
.column.of-3 {
width: 33.333%;
}
.column.of-4 {
width: 25%;
}
@media screen and (max-width:480px) {
.preheader .rightColumnContent,
.footer .rightColumnContent {
text-align: left !important;
}
.preheader .rightColumnContent div,
.preheader .rightColumnContent span,
.footer .rightColumnContent div,
.footer .rightColumnContent span {
text-align: left !important;
}
.preheader .rightColumnContent,
.preheader .leftColumnContent {
font-size: 80% !important;
padding: 5px 0;
}
table.wrapper-mobile {
width: 100% !important;
table-layout: fixed;
}
img.max-width {
height: auto !important;
max-width: 100% !important;
}
a.bulletproof-button {
display: block !important;
width: auto !important;
font-size: 80%;
padding-left: 0 !important;
padding-right: 0 !important;
}
.columns {
width: 100% !important;
}
.column {
display: block !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
}
</style>
<!--user entered Head Start--><!--End Head user entered-->
</head>
<body>
Thank you for signing up for the Appsmith platform.<br /><br />
Your personal workspace is: {{personalWorkspaceName}}
<br /><br />
Cheers,<br />
Appsmith
</body>
</html>
<center class="wrapper" data-link-color="#1188E6" data-body-style="font-size:14px; font-family:arial,helvetica,sans-serif; color:#000000; background-color:#FFFFFF;">
<div class="webkit">
<table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#FFFFFF">
<tbody><tr>
<td valign="top" bgcolor="#FFFFFF" width="100%">
<table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0">
<tbody><tr>
<td width="100%">
<table width="100%" cellpadding="0" cellspacing="0" border="0">
<tbody><tr>
<td>
<!--[if mso]>
<center>
<table><tr><td width="600">
<![endif]-->
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%; max-width:600px;" align="center">
<tbody><tr>
<td role="modules-container" style="padding:0px 0px 0px 0px; color:#000000; text-align:left;" bgcolor="#ffffff" width="100%" align="left"><table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;">
<tbody><tr>
<td role="module-content">
<p></p>
</td>
</tr>
</tbody></table><table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="40dbb7f1-8428-4188-86b0-1b0245659a17">
<tbody>
<tr>
<td style="font-size:6px; line-height:10px; padding:0px 0px 0px 0px;" valign="top" align="center">
<a href="https://www.appsmith.com/"><img class="max-width" border="0" style="display:block; color:#000000; text-decoration:none; font-family:Helvetica, arial, sans-serif; font-size:16px; max-width:25% !important; width:25%; height:auto !important;" width="150" alt="" data-proportionally-constrained="true" data-responsive="true" src="http://cdn.mcauto-images-production.sendgrid.net/4bbae2fffe647858/b21738f2-3a49-4774-aae9-c8e80ad9c26e/924x284.png"></a>
</td>
</tr>
</tbody>
</table><table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="71d7e9fb-0f3b-43f4-97e1-994b33bfc82a" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="padding:0px 0px 18px 0px; line-height:22px; text-align:inherit; background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff" role="module-content"><div><div style="font-family: inherit; text-align: inherit; margin-left: 0px"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Hi {{firstName}},</span></div>
<div style="font-family: inherit; text-align: inherit; margin-left: 0px"><br></div>
<div style="font-family: inherit; text-align: start"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">I am really excited you signed up for Appsmith and wanted to personally reach out to welcome you.</span></div>
<div style="font-family: inherit; text-align: inherit; margin-left: 0px"><br></div>
<div style="font-family: inherit; text-align: inherit"><span style="color: #e8e7e3; font-family: &quot;apple color emoji&quot;, &quot;segoe ui emoji&quot;, &quot;noto color emoji&quot;, &quot;android emoji&quot;, emojisymbols, &quot;emojione mozilla&quot;, &quot;twemoji mozilla&quot;, &quot;segoe ui symbol&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline; font-size: 14px; background-color: rgb(239, 239, 239)">🚀</span><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">Hope you started creating your first app already. In case you didn't and are looking for a little inspiration, you may want to </span><a href="https://docs.appsmith.com/tutorials/"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif">check out a few use cases</span></a><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959"> other developers are building.</span></div>
<div style="font-family: inherit; text-align: inherit"><br></div>
<div style="font-family: inherit; text-align: left"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-image: initial; background-position-x: 0px; background-position-y: 0px; background-size: initial; background-repeat-x: initial; background-repeat-y: initial; background-attachment: initial; background-origin: initial; background-clip: initial; background-color: initial; text-decoration-style: initial; text-decoration-color: initial; outline-color: initial; outline-style: initial; outline-width: 0px; color: #e8e7e3; white-space: normal; font-size: 14px">📅</span><span style="font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">If you would like to explore the product together or collaborate while creating your first app, </span><a href="https://calendly.com/arpit-appsmith/15min"><span style="font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif">schedule a time here</span></a><span style="font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif"> </span><span style="font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959">and let's get building.</span></div>
<div style="font-family: inherit; text-align: left"><br></div>
<div style="font-family: inherit; text-align: left"><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; outline-color: initial; outline-style: initial; outline-width: 0px; vertical-align: baseline; background-image: initial; background-position-x: 0px; background-position-y: 0px; background-size: initial; background-repeat-x: initial; background-repeat-y: initial; background-attachment: initial; background-origin: initial; background-clip: initial; background-color: initial; color: #e8e7e3; font-family: &quot;apple color emoji&quot;, &quot;segoe ui emoji&quot;, &quot;noto color emoji&quot;, &quot;android emoji&quot;, emojisymbols, &quot;emojione mozilla&quot;, &quot;twemoji mozilla&quot;, &quot;segoe ui symbol&quot;; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; font-weight: 400; font-size: 14px">🙋</span><span style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-variant-numeric: normal; font-variant-east-asian: normal; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; color: #5c5959"> If you need anything at all, I'm here to help. Reach out anytime you want to chat - all thoughts, questions, and feedback are welcome.</span></div><div></div></div></td>
</tr>
</tbody>
</table><table border="0" cellpadding="0" cellspacing="0" class="module" data-role="module-button" data-type="button" role="module" style="table-layout:fixed;" width="100%" data-muid="f00155e0-813d-4e9b-b61d-384e6f99e5b7">
<tbody>
<tr>
<td align="center" bgcolor="" class="outer-td" style="padding:0px 0px 0px 0px;">
<table border="0" cellpadding="0" cellspacing="0" class="wrapper-mobile" style="text-align:center;">
<tbody>
<tr>
<td align="center" bgcolor="#ff6d2d" class="inner-td" style="border-radius:6px; font-size:16px; text-align:center; background-color:inherit;">
<a href="{{appsmithLink}}" style="background-color:#ff6d2d; border:1px solid #ff6d2d; border-color:#ff6d2d; border-radius:6px; border-width:1px; color:#ffffff; display:inline-block; font-weight:400; letter-spacing:0px; line-height:6px; padding:12px 18px 12px 18px; text-align:center; text-decoration:none; border-style:solid; font-family:tahoma,geneva,sans-serif; font-size:16px;" target="_blank">Go to Appsmith</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table><table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;" data-muid="cab2544f-5a6c-49a0-b246-efe5ac8c5208" data-mc-module-version="2019-10-22">
<tbody>
<tr>
<td style="padding:0px 0px 0px 0px; line-height:22px; text-align:inherit;" height="100%" valign="top" bgcolor="" role="module-content"><div><div style="font-family: inherit; text-align: start"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; font-size: 14px">Cheers</span></div>
<div style="font-family: inherit; text-align: start"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; font-size: 14px">Arpit Mohan</span></div>
<div style="font-family: inherit; text-align: start"><br></div>
<!-- <div style="font-family: inherit; text-align: inherit; margin-left: 0px"><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: normal; font-variant-east-asian: normal; font-weight: inherit; font-stretch: normal; line-height: normal; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; font-size: 14px">P.S:</span><span style="font-size: 14px"> </span><span style="box-sizing: border-box; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; font-style: inherit; font-variant-ligatures: inherit; font-variant-caps: inherit; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: &quot;lucida sans unicode&quot;, &quot;lucida grande&quot;, sans-serif; vertical-align: baseline; border-top-width: 0px; border-right-width: 0px; border-bottom-width: 0px; border-left-width: 0px; border-top-style: initial; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image-source: initial; border-image-slice: initial; border-image-width: initial; border-image-outset: initial; border-image-repeat: initial; color: #5c5959; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline; font-size: 14px">I'll send you a few more emails in the coming days to help you make the most of Appsmith. If you don't want to receive these, hit unsubscribe.</span></div> -->
<div></div></div></td>
</tr>
</tbody>
</table>
<!-- <div data-role="module-unsubscribe" class="module" role="module" data-type="unsubscribe" style="color:#444444; font-size:10px; line-height:20px; padding:0px 16px 0px 16px; text-align:center;" data-muid="1c219d7b-fb60-4317-8a55-f9d8bbd8592d">
<div class="Unsubscribe--addressLine"></div>
<p style="font-family:tahoma,geneva,sans-serif; font-size:10px; line-height:20px;"><a class="Unsubscribe--unsubscribeLink" href="{{{unsubscribe}}}" target="_blank" style="">Unsubscribe</a> - <a href="{{{unsubscribe_preferences}}}" target="_blank" class="Unsubscribe--unsubscribePreferences" style="">Unsubscribe Preferences</a></p>
</div> -->
</td>
</tr>
</tbody></table>
<!--[if mso]>
</td>
</tr>
</table>
</center>
<![endif]-->
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</td>
</tr>
</tbody></table>
</div>
</center>
</body></html>