diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BaseDomain.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BaseDomain.java index 28d6dcfd6b..c55d3cd038 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BaseDomain.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BaseDomain.java @@ -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 { protected Boolean deleted = false; - @JsonIgnore - @Version - protected Long documentVersion; - @JsonIgnore protected Set policies; - @JsonIgnore @Override public boolean isNew() { return this.getId() == null; diff --git a/app/server/appsmith-plugins/pom.xml b/app/server/appsmith-plugins/pom.xml index 11ebe088e9..eb1160af2c 100644 --- a/app/server/appsmith-plugins/pom.xml +++ b/app/server/appsmith-plugins/pom.xml @@ -18,6 +18,7 @@ postgresPlugin restApiPlugin mongoPlugin + rapidApiPlugin \ No newline at end of file diff --git a/app/server/appsmith-plugins/rapidApiPlugin/pom.xml b/app/server/appsmith-plugins/rapidApiPlugin/pom.xml new file mode 100644 index 0000000000..32621c6e67 --- /dev/null +++ b/app/server/appsmith-plugins/rapidApiPlugin/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + com.external.plugins + rapidApiPlugin + 1.0-SNAPSHOT + + rapidApiPlugin + + + UTF-8 + 11 + ${java.version} + ${java.version} + rapidapi-plugin + com.external.plugins.RapidApiPlugin + 1.0-SNAPSHOT + tech@appsmith.com + + + + + + junit + junit + 4.11 + test + + + + org.pf4j + pf4j-spring + 0.5.0 + + + + com.appsmith + interfaces + 1.0-SNAPSHOT + + + + org.springframework + spring-webflux + 5.1.5.RELEASE + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + + + + + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.dependencies} + + + + + + + + diff --git a/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java b/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java new file mode 100644 index 0000000000..420f4e3cc8 --- /dev/null +++ b/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java @@ -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 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 strStrMap = new HashMap(); + + List 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 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 headers) { + for (Property header : headers) { + if (header.getKey() != null && !header.getKey().isEmpty()) { + webClientBuilder.defaultHeader(header.getKey(), header.getValue()); + } + } + } + + private URI createFinalUriWithQueryParams(String url, List 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. + */ + } +} diff --git a/app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java b/app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java new file mode 100644 index 0000000000..6436bda5ab --- /dev/null +++ b/app/server/appsmith-plugins/rapidApiPlugin/src/test/java/com/external/plugins/RapidApiPluginTest.java @@ -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); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java index 499cba64ad..a212b84e1c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java @@ -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> create(@Valid @RequestBody Action resource) throws AppsmithException { + public Mono> 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)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java index 8b919f479c..039842bc56 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java @@ -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> create(@Valid @RequestBody Application resource) throws AppsmithException { + public Mono> 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)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java index 8b54ff09a9..d9989d6d67 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java @@ -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> create(@Valid @RequestBody T resource) throws AppsmithException { + public Mono> 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)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java index 0710edc1f8..f6488defbc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java @@ -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> create(@Valid @RequestBody Collection resource) throws AppsmithException { + public Mono> 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)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java index 9378db06a7..e45745f6cd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java @@ -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 { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public Mono> create(@Valid @RequestBody Page resource) throws AppsmithException { + public Mono> 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)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PluginController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PluginController.java index 828b3c30af..2284278bd2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PluginController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PluginController.java @@ -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; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ProviderController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ProviderController.java index 7a422469dc..999a2652bb 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ProviderController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ProviderController.java @@ -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>> getAllCategories() { + return service.getAllCategories() + .map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java index 23b0d60866..3ccf8f7e8c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java @@ -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> 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> importPostmanCollection(@RequestBody Object input, @RequestParam String type) { + public Mono> importPostmanCollection(@RequestBody Object input, + @RequestParam String type) { return Mono.just(postmanImporterService.importPostmanCollection(input)) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/SignupController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/SignupController.java deleted file mode 100644 index f266071fe6..0000000000 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/SignupController.java +++ /dev/null @@ -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> signupOrganization(@RequestBody Organization organization) { - return signupService.createOrganization(organization) - .map(org -> new ResponseDTO<>(HttpStatus.CREATED.value(), org, null)); - } - -} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java index 347459d6fa..6904686f12 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java @@ -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 { private final SessionUserService sessionUserService; @@ -37,6 +43,14 @@ public class UserController extends BaseController { this.userOrganizationService = userOrganizationService; } + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Mono> 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> setCurrentOrganization(@PathVariable String orgId) { return service.switchCurrentOrganization(orgId) @@ -77,12 +91,19 @@ public class UserController extends BaseController { .map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null)); } + @Deprecated @GetMapping("/me") public Mono> getUserProfile() { return sessionUserService.getCurrentUser() .map(user -> new ResponseDTO<>(HttpStatus.OK.value(), user, null)); } + @GetMapping("/profile") + public Mono> 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 { } @PutMapping("/invite/confirm") - public Mono> confirmInviteUser(@RequestBody InviteUser inviteUser) { - return service.confirmInviteUser(inviteUser) + public Mono> confirmInviteUser(@RequestBody InviteUser inviteUser, + @RequestHeader("Origin") String originHeader) { + return service.confirmInviteUser(inviteUser, originHeader) .map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null)); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java index 9af67ec1e5..1a74211b99 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java @@ -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 diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ActionProvider.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ActionProvider.java new file mode 100644 index 0000000000..2984dc9df6 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ActionProvider.java @@ -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; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java new file mode 100644 index 0000000000..b4f39fe3b2 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Documentation.java @@ -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; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java index 27e2f5e66c..72f20f1d4d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/User.java @@ -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; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ApplicationNameIdDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ApplicationNameIdDTO.java new file mode 100644 index 0000000000..42d6b1b5b9 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ApplicationNameIdDTO.java @@ -0,0 +1,13 @@ +package com.appsmith.server.dtos; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ApplicationNameIdDTO { + + String id; + + String name; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserProfileDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserProfileDTO.java new file mode 100644 index 0000000000..7733221ffc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/UserProfileDTO.java @@ -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 applications; +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java index 260069f5d0..70400ce9d4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/notifications/EmailSender.java @@ -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; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java index 38f1434667..53c6283acc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java @@ -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 executeAction(ExecuteActionDTO executeActionDTO) { Action actionFromDto = executeActionDTO.getAction(); @@ -478,13 +483,8 @@ public class ActionServiceImpl extends BaseService 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 { 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); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AnalyticsService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AnalyticsService.java index 195b6e6477..11de1af389 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AnalyticsService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AnalyticsService.java @@ -43,8 +43,17 @@ public class AnalyticsService { }); } + private User createAnonymousUser() { + User user = new User(); + user.setId("anonymousUser"); + return user; + } + public Mono sendEvent(String eventTag, T object) { - Mono 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 userMono = sessionUserService.getCurrentUser() + .defaultIfEmpty(createAnonymousUser()); return userMono .map(user -> { HashMap analyticsProperties = new HashMap<>(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java index 978a9050b9..9500ef5e24 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java @@ -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)); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseService.java index 462ef12d7d..4c1c6eb325 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseService.java @@ -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 implements CrudService { final Scheduler scheduler; @@ -90,10 +92,7 @@ public abstract class BaseService { - 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) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java index ea23434000..ca856c2bd8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java @@ -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()) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PageServiceImpl.java index 9ba448c6aa..6290103685 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PageServiceImpl.java @@ -118,10 +118,8 @@ public class PageServiceImpl extends BaseService 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 diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderService.java index b33acb0bbf..0b06159fa0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderService.java @@ -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 { + public Mono> getAllCategories(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderServiceImpl.java index bb826f27ea..7f5c4623b8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ProviderServiceImpl.java @@ -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 implements ProviderService { + private static final List 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 categories = new ArrayList<>(); if (params.getFirst(FieldName.CATEGORY) != null) { - List 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> getAllCategories() { + return Mono.just(CATEGORIES); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserService.java index 750f0564e0..a7decf3f51 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserService.java @@ -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 { Mono verifyInviteToken(String email, String token); - Mono confirmInviteUser(InviteUser inviteUser); + Mono confirmInviteUser(InviteUser inviteUser, String originHeader); Mono> getAnonymousAuthorities(); + + Mono getUserProfile(); + + Mono createUser(User user, String originHeader); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index a3d19cd8a0..953d021167 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -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 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 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 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 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 currentUserMono = sessionUserService.getCurrentUser().cache(); + Mono 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 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 params = Map.of( - "token", token, - "inviteUrl", inviteUrl); + Map 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 i } return inviteUser; }); - } /** @@ -338,7 +362,7 @@ public class UserServiceImpl extends BaseService i * @return */ @Override - public Mono confirmInviteUser(InviteUser inviteUser) { + public Mono 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 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 i .map(user -> user.getAuthorities()); } + @Override + public Mono 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 i * @return */ @Override - public Mono create(User user) { + public Mono 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 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 savedUserMono = super.create(user); @@ -434,7 +468,10 @@ public class UserServiceImpl extends BaseService i .map(savedUser -> { // Send an email to the user welcoming them to the Appsmith platform try { - Map params = Map.of("personalWorkspaceName", personalWorkspaceName); + Map 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 i // Doesn't work without this. .map(user -> (UserDetails) user); } + + @Override + public Mono getUserProfile() { + return sessionUserService.getCurrentUser() + .flatMap(user -> { + String currentOrganizationId = user.getCurrentOrganizationId(); + UserProfileDTO userProfile = new UserProfileDTO(); + userProfile.setUser(user); + + Mono 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; + }); + } } diff --git a/app/server/appsmith-server/src/main/resources/application-staging.properties b/app/server/appsmith-server/src/main/resources/application-staging.properties index 84cf71fbb4..9c6c8af343 100644 --- a/app/server/appsmith-server/src/main/resources/application-staging.properties +++ b/app/server/appsmith-server/src/main/resources/application-staging.properties @@ -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 diff --git a/app/server/appsmith-server/src/main/resources/email/forgotPasswordTemplate.html b/app/server/appsmith-server/src/main/resources/email/forgotPasswordTemplate.html index dff342e15d..046d9a0d19 100644 --- a/app/server/appsmith-server/src/main/resources/email/forgotPasswordTemplate.html +++ b/app/server/appsmith-server/src/main/resources/email/forgotPasswordTemplate.html @@ -1,10 +1,189 @@ - + + + + + + + + + + + - You can reset your password by clicking this link. -

- Alternatively, you can copy paste the following URL in your browser: {{resetUrl}} -

- Cheers,
- Appsmith - - \ No newline at end of file +
+
+ + + + +
+ + + + +
+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
Hello,
+

+
Forgot the password to your Appsmith account? No worries, we've got you covered.
+ + + + + +
+ + + + + + +
+ Reset Password +
+
+ + + + + +
The link will expire in 48 hours. If you didn't request a password reset, you can safely ignore this email.
+

+
Cheers
+
Devs at Appsmith
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/resources/email/inviteUserCreatorTemplate.html b/app/server/appsmith-server/src/main/resources/email/inviteUserCreatorTemplate.html new file mode 100644 index 0000000000..33232d5156 --- /dev/null +++ b/app/server/appsmith-server/src/main/resources/email/inviteUserCreatorTemplate.html @@ -0,0 +1,187 @@ + + + + + + + + + + + + +
+
+ + + + +
+ + + + +
+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
You've been invited to collaborate.
+

+
{{Inviter_First_Name}} has invited you to collaborate on the organization "{{inviter_org_name}}" in Appsmith.
+ + + + + +
+ + + + + + +
+ Accept invite +
+
+ + + + + +
Cheers
+
Devs at Appsmith
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/resources/email/inviteUserTemplate.html b/app/server/appsmith-server/src/main/resources/email/inviteUserTemplate.html deleted file mode 100644 index 63eebfbe5c..0000000000 --- a/app/server/appsmith-server/src/main/resources/email/inviteUserTemplate.html +++ /dev/null @@ -1,11 +0,0 @@ - - - You've been invited to the Appsmith platform. Please complete your sign up by clicking this link

- Alternatively, you can copy & paste the following url in your browser: {{inviteUrl}} -

- For reference, your invite token is: {{token}} -

- Cheers, - Appsmith - - \ No newline at end of file diff --git a/app/server/appsmith-server/src/main/resources/email/welcomeUserTemplate.html b/app/server/appsmith-server/src/main/resources/email/welcomeUserTemplate.html index 40772c94a5..0441063bcc 100644 --- a/app/server/appsmith-server/src/main/resources/email/welcomeUserTemplate.html +++ b/app/server/appsmith-server/src/main/resources/email/welcomeUserTemplate.html @@ -1,9 +1,203 @@ - + + + + + + + + + + + - Thank you for signing up for the Appsmith platform.

- Your personal workspace is: {{personalWorkspaceName}} -

- Cheers,
- Appsmith - - +
+
+ + + + +
+ + + + +
+ + + + +
+ + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
Hi {{firstName}},
+

+
I am really excited you signed up for Appsmith and wanted to personally reach out to welcome you.
+

+
🚀Hope you started creating your first app already. In case you didn't and are looking for a little inspiration, you may want to check out a few use cases other developers are building.
+

+
📅If you would like to explore the product together or collaborate while creating your first app, schedule a time here and let's get building.
+

+
🙋 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.
+ + + + + +
+ + + + + + +
+ Go to Appsmith +
+
+ + + + + + +
Cheers
+
Arpit Mohan
+

+ + +
+ +
+ +
+
+
+
+
+ + + \ No newline at end of file