Accept richer inputs from welcome signup form (#6650)

* Accept richer inputs from welcome signup form

* Send subscribe event when user has approved it
This commit is contained in:
Shrikant Sharat Kandula 2021-08-20 10:10:04 +05:30 committed by GitHub
parent 17edf11d00
commit 094b77832f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 152 additions and 28 deletions

View File

@ -9,7 +9,6 @@ import com.appsmith.server.services.ConfigService;
import io.sentry.Sentry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.ParameterizedTypeReference;
@ -71,10 +70,7 @@ public class InstanceConfig implements ApplicationListener<ApplicationReadyEvent
Objects.requireNonNull(responseEntity.getBody()).getResponseMeta().getError().getMessage()));
})
.flatMap(instanceId -> configService
.updateByName(Appsmith.APPSMITH_REGISTERED, new Config(
new JSONObject(Map.of("value", true)),
Appsmith.APPSMITH_REGISTERED
))
.save(Appsmith.APPSMITH_REGISTERED, Map.of("value", true))
);
}
}

View File

@ -135,6 +135,7 @@ public class SecurityConfig {
// This is because the flow enters AclFilter as well and needs to be whitelisted there
.matchers(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, Url.LOGIN_URL),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/super"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/forgotPassword"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/verifyPasswordResetToken"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.PUT, USER_URL + "/resetPassword"),

View File

@ -11,7 +11,10 @@ public enum AnalyticsEvents {
UPDATE_LAYOUT,
PUBLISH_APPLICATION("publish_APPLICATION"),
FORK,
GENERATE_CRUD_PAGE("generate_CRUD_PAGE")
GENERATE_CRUD_PAGE("generate_CRUD_PAGE"),
CREATE_SUPERUSER,
SUBSCRIBE_MARKETING_EMAILS,
UNSUBSCRIBE_MARKETING_EMAILS,
;
private final String eventName;

View File

@ -0,0 +1,13 @@
package com.appsmith.server.constants;
/**
* Names used in config collections.
*/
public class ConfigNames {
public static final String COMPANY_NAME = "company-name";
// Disallow instantiation of this class.
private ConfigNames() {}
}

View File

@ -30,7 +30,7 @@ public class ConfigController {
@PutMapping("/name/{name}")
public Mono<ResponseDTO<Config>> updateByName(@PathVariable String name, @RequestBody Config config) {
return service.updateByName(name, config)
return service.updateByName(config)
.map(resource -> new ResponseDTO<>(HttpStatus.OK.value(), resource, null));
}
}

View File

@ -7,6 +7,7 @@ import com.appsmith.server.dtos.InviteUsersDTO;
import com.appsmith.server.dtos.ResetUserPasswordDTO;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.dtos.UserProfileDTO;
import com.appsmith.server.dtos.UserSignupRequestDTO;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.UserDataService;
import com.appsmith.server.services.UserOrganizationService;
@ -75,7 +76,10 @@ public class UserController extends BaseController<UserService, User, String> {
}
@PostMapping("/super")
public Mono<ResponseDTO<User>> createSuperUser(@Valid @RequestBody User resource, ServerWebExchange exchange) {
public Mono<ResponseDTO<User>> createSuperUser(
@Valid @RequestBody UserSignupRequestDTO resource,
ServerWebExchange exchange
) {
return userSignup.signupAndLoginSuper(resource, exchange)
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
}

View File

@ -24,6 +24,9 @@ public class UserData extends BaseDomain {
@JsonIgnore
String userId;
// Role of the user in their organization, example, Designer, Developer, Product Lead etc.
private String role;
// The ID of the asset which has the profile photo of this user.
private String profilePhotoAssetId;

View File

@ -0,0 +1,30 @@
package com.appsmith.server.dtos;
import com.appsmith.server.domains.LoginSource;
import com.appsmith.server.domains.UserState;
import lombok.Data;
@Data
public class UserSignupRequestDTO {
private String email;
private String name;
private LoginSource source = LoginSource.FORM;
private UserState state = UserState.ACTIVATED;
private boolean isEnabled = true;
private String password;
private String role;
private String companyName;
private boolean allowCollectingAnonymousData;
private boolean signupForNewsletter;
}

View File

@ -65,10 +65,6 @@ public class AnalyticsService {
});
}
public void sendEvent(String event, String userId) {
sendEvent(event, userId, null);
}
public void sendEvent(String event, String userId, Map<String, Object> properties) {
if (!isActive()) {
return;
@ -78,10 +74,12 @@ public class AnalyticsService {
// java.lang.UnsupportedOperationException: null
// at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java)
// at java.base/java.util.ImmutableCollections$AbstractImmutableMap.put(ImmutableCollections.java)
Map<String, Object> analyticsProperties = new HashMap<>(properties);
Map<String, Object> analyticsProperties = properties == null ? new HashMap<>() : new HashMap<>(properties);
// Hash usernames at all places for self-hosted instance
if (!commonConfig.isCloudHosting()) {
if (!commonConfig.isCloudHosting()
// But send the email intact for the subscribe event, which is sent only if the user has explicitly agreed to it.
&& !AnalyticsEvents.SUBSCRIBE_MARKETING_EMAILS.name().equals(event)) {
final String hashedUserId = DigestUtils.sha256Hex(userId);
analyticsProperties.remove("request");
if (!CollectionUtils.isEmpty(analyticsProperties)) {

View File

@ -6,10 +6,16 @@ import com.appsmith.server.domains.Datasource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
public interface ConfigService {
Mono<Config> getByName(String name);
Mono<Config> updateByName(String name, Config config);
Mono<Config> updateByName(Config config);
Mono<Config> save(Config config);
Mono<Config> save(String name, Map<String, Object> config);
Mono<String> getInstanceId();

View File

@ -10,12 +10,14 @@ import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.ConfigRepository;
import com.appsmith.server.repositories.DatasourceRepository;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ -47,7 +49,8 @@ public class ConfigServiceImpl implements ConfigService {
}
@Override
public Mono<Config> updateByName(String name, Config config) {
public Mono<Config> updateByName(Config config) {
final String name = config.getName();
return repository.findByName(name)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.CONFIG, name)))
.flatMap(dbConfig -> {
@ -57,6 +60,21 @@ public class ConfigServiceImpl implements ConfigService {
});
}
@Override
public Mono<Config> save(Config config) {
return repository.findByName(config.getName())
.flatMap(dbConfig -> {
dbConfig.setConfig(config.getConfig());
return repository.save(dbConfig);
})
.switchIfEmpty(Mono.defer(() -> repository.save(config)));
}
@Override
public Mono<Config> save(String name, Map<String, Object> config) {
return save(new Config(new JSONObject(config), name));
}
@Override
public Mono<String> getInstanceId() {
if (instanceId != null) {

View File

@ -19,6 +19,8 @@ public interface UserDataService {
Mono<UserData> updateForCurrentUser(UserData updates);
Mono<UserData> updateForUser(User user, UserData updates);
Mono<User> setViewedCurrentVersionReleaseNotes(User user);
Mono<User> setViewedCurrentVersionReleaseNotes(User user, String version);

View File

@ -106,6 +106,14 @@ public class UserDataServiceImpl extends BaseService<UserDataRepository, UserDat
});
}
@Override
public Mono<UserData> updateForUser(User user, UserData updates) {
updates.setUserId(user.getId());
final Mono<UserData> updaterMono = update(user.getId(), updates);
final Mono<UserData> creatorMono = Mono.just(updates).flatMap(this::create);
return updaterMono.switchIfEmpty(creatorMono);
}
@Override
public Mono<UserData> update(String userId, UserData resource) {
if (userId == null) {

View File

@ -3,15 +3,21 @@ package com.appsmith.server.solutions;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.authentication.handlers.AuthenticationSuccessHandler;
import com.appsmith.server.constants.AnalyticsEvents;
import com.appsmith.server.constants.ConfigNames;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.LoginSource;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserData;
import com.appsmith.server.domains.UserState;
import com.appsmith.server.dtos.UserSignupRequestDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.CaptchaService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.UserDataService;
import com.appsmith.server.services.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -47,10 +53,12 @@ import static org.springframework.security.web.server.context.WebSessionServerSe
public class UserSignup {
private final UserService userService;
private final UserDataService userDataService;
private final CaptchaService captchaService;
private final AuthenticationSuccessHandler authenticationSuccessHandler;
private final ConfigService configService;
private final AnalyticsService analyticsService;
private final PolicyUtils policyUtils;
private final UserRepository userRepository;
private static final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
@ -109,10 +117,10 @@ public class UserSignup {
String recaptchaToken = exchange.getRequest().getQueryParams().getFirst("recaptchaToken");
return captchaService.verify(recaptchaToken).flatMap(verified -> {
if (!verified) {
return Mono.error(new AppsmithException(AppsmithError.GOOGLE_RECAPTCHA_FAILED));
}
return exchange.getFormData();
if (!Boolean.TRUE.equals(verified)) {
return Mono.error(new AppsmithException(AppsmithError.GOOGLE_RECAPTCHA_FAILED));
}
return exchange.getFormData();
})
.map(formData -> {
final User user = new User();
@ -151,19 +159,45 @@ public class UserSignup {
});
}
public Mono<User> signupAndLoginSuper(User user, ServerWebExchange exchange) {
public Mono<User> signupAndLoginSuper(UserSignupRequestDTO userFromRequest, ServerWebExchange exchange) {
return userService.isUsersEmpty()
.flatMap(isEmpty -> {
if (!isEmpty) {
if (!Boolean.TRUE.equals(isEmpty)) {
return Mono.error(new AppsmithException(AppsmithError.UNAUTHORIZED_ACCESS));
}
final User user = new User();
user.setEmail(userFromRequest.getEmail());
user.setName(userFromRequest.getName());
user.setSource(userFromRequest.getSource());
user.setState(userFromRequest.getState());
user.setIsEnabled(userFromRequest.isEnabled());
user.setPassword(userFromRequest.getPassword());
policyUtils.addPoliciesToExistingObject(Map.of(
AclPermission.MANAGE_INSTANCE_ENV.getValue(),
Policy.builder().permission(AclPermission.MANAGE_INSTANCE_ENV.getValue()).users(Set.of(user.getUsername())).build()
Policy.builder().permission(AclPermission.MANAGE_INSTANCE_ENV.getValue()).users(Set.of(user.getEmail())).build()
), user);
return signupAndLogin(user, exchange);
})
.flatMap(user -> {
final UserData userData = new UserData();
userData.setRole(userFromRequest.getRole());
if (userFromRequest.isSignupForNewsletter()) {
analyticsService.sendEvent(
AnalyticsEvents.SUBSCRIBE_MARKETING_EMAILS.name(),
user.getEmail(),
Map.of("id", user.getEmail())
);
}
return Mono.when(
userDataService.updateForUser(user, userData),
configService.save(ConfigNames.COMPANY_NAME, Map.of("value", userFromRequest.getCompanyName())),
analyticsService.sendObjectEvent(AnalyticsEvents.CREATE_SUPERUSER, user, null)
).thenReturn(user);
});
}

View File

@ -5,8 +5,10 @@ import com.appsmith.server.domains.User;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.helpers.ValidationUtils;
import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.CaptchaService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.UserDataService;
import com.appsmith.server.services.UserService;
import org.junit.Before;
import org.junit.Test;
@ -21,23 +23,29 @@ public class UserSignupTest {
@MockBean
private UserService userService;
@MockBean
private UserDataService userDataService;
@MockBean
private CaptchaService captchaService;
@MockBean
private AuthenticationSuccessHandler authenticationSuccessHandler;
@MockBean
private ConfigService configService;
@MockBean
private PolicyUtils policyUtils;
@MockBean
private UserRepository userRepository;
private AnalyticsService analyticsService;
private UserSignup userSignup;
@Before
public void setUp() {
userSignup = new UserSignup(userService, captchaService, authenticationSuccessHandler, policyUtils, userRepository);
userSignup = new UserSignup(userService, userDataService, captchaService, authenticationSuccessHandler, configService, analyticsService, policyUtils);
}
private String createRandomString(int length) {