Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ public class EmailVerificationToken extends BaseDomain {
String tokenHash;
String email;
Instant tokenGeneratedAt;
String organizationId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.appsmith.server.migrations.db.ce;

import com.appsmith.server.domains.EmailVerificationToken;
import com.appsmith.server.domains.Organization;
import io.mongock.api.annotations.ChangeUnit;
import io.mongock.api.annotations.Execution;
import io.mongock.api.annotations.RollbackExecution;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;

import static org.springframework.data.mongodb.core.query.Criteria.where;

@Slf4j
@ChangeUnit(order = "070", id = "add-organization-id-to-email-verification-token", author = "")
public class Migration070_AddOrganizationIdToEmailVerificationToken {

private final MongoTemplate mongoTemplate;

public Migration070_AddOrganizationIdToEmailVerificationToken(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}

@Execution
public void addOrganizationIdToEmailVerificationToken() {
log.info("Adding organizationId to EmailVerificationToken documents");

// Get the first organization (there should be only one at this point)
Organization organization = mongoTemplate.findOne(new Query(), Organization.class);
if (organization == null) {
log.warn("No organization found. Skipping migration.");
return;
}

String organizationId = organization.getId();

// Update all EmailVerificationToken documents to include the organizationId
Query query = new Query(where("organizationId").exists(false));
Update update = new Update().set("organizationId", organizationId);

long modifiedCount = mongoTemplate
.updateMulti(query, update, EmailVerificationToken.class)
.getModifiedCount();
log.info("Updated {} EmailVerificationToken documents with organizationId", modifiedCount);
}

@RollbackExecution
public void rollbackExecution() {
log.info("Rollback not supported for this migration");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ public class UserServiceCEImpl extends BaseService<UserRepository, User, String>
private final UserServiceHelper userPoliciesComputeHelper;
private final InstanceVariablesHelper instanceVariablesHelper;

private static final WebFilterChain EMPTY_WEB_FILTER_CHAIN = serverWebExchange -> Mono.empty();
protected static final WebFilterChain EMPTY_WEB_FILTER_CHAIN = serverWebExchange -> Mono.empty();
private static final String FORGOT_PASSWORD_CLIENT_URL_FORMAT = "%s/user/resetPassword?token=%s";
private static final Pattern ALLOWED_ACCENTED_CHARACTERS_PATTERN = Pattern.compile("^[\\p{L} 0-9 .\'\\-]+$");

private static final String EMAIL_VERIFICATION_CLIENT_URL_FORMAT =
"%s/user/verify?token=%s&email=%s&redirectUrl=%s";
"%s/user/verify?token=%s&email=%s&organizationId=%s&redirectUrl=%s";

private static final String EMAIL_VERIFICATION_ERROR_URL_FORMAT = "/user/verify-error?code=%s&message=%s&email=%s";
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
Expand Down Expand Up @@ -415,20 +415,8 @@ public Mono<User> userCreate(User user, boolean isAdminUser) {
// convert the user email to lowercase
user.setEmail(user.getEmail().toLowerCase());

Mono<User> userWithOrgMono = Mono.just(user)
.flatMap(userBeforeSave -> {
if (userBeforeSave.getOrganizationId() == null) {
return organizationService
.getCurrentUserOrganizationId()
.map(organizationId -> {
userBeforeSave.setOrganizationId(organizationId);
return userBeforeSave;
});
}
// The org has been set already. No need to set the default org id.
return Mono.just(userBeforeSave);
})
.cache();
Mono<User> userWithOrgMono =
Mono.just(user).flatMap(this::setOrganizationIdForUser).cache();
// Save the new user
return userWithOrgMono
.flatMap(this::validateObject)
Expand Down Expand Up @@ -541,6 +529,22 @@ public Mono<UserSignupDTO> createUser(User user) {
}));
}

/**
* Sets the organization ID for a new user during signup.
*
* @param user User object for which to set the organization ID
* @return Mono<User> with organization ID set
*/
protected Mono<User> setOrganizationIdForUser(User user) {
if (user.getOrganizationId() == null) {
return organizationService.getCurrentUserOrganizationId().map(organizationId -> {
user.setOrganizationId(organizationId);
return user;
});
}
return Mono.just(user);
}

/**
* 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 default workspace name is created for the user. The new user is then
Expand Down Expand Up @@ -579,11 +583,16 @@ public Mono<User> signupIfAllowed(User user) {
isAdminUser = true;
}

// No special configurations found, allow signup for the new user.
return userCreate(user, isAdminUser).elapsed().map(pair -> {
log.debug("UserServiceCEImpl::Time taken for create user: {} ms", pair.getT1());
return pair.getT2();
});
// First set the organization ID for the user
boolean finalIsAdminUser = isAdminUser;
return setOrganizationIdForUser(user)
// Then proceed with user creation
.flatMap(userWithOrgId -> userCreate(userWithOrgId, finalIsAdminUser))
.elapsed()
.map(pair -> {
log.debug("UserServiceCEImpl::Time taken for create user: {} ms", pair.getT1());
return pair.getT2();
});
}

@Override
Expand Down Expand Up @@ -701,7 +710,12 @@ public Mono<Boolean> isUsersEmpty() {

@Override
public Mono<UserProfileDTO> buildUserProfileDTO(User user) {
// For anonymous users, build the profile directly from the in-memory user object
if (user.isAnonymous()) {
return getAnonymousUserProfile(user);
}

// For regular users, proceed with the existing flow
Mono<User> userFromDbMono = findByEmail(user.getEmail()).cache();

Mono<Boolean> isSuperUserMono =
Expand Down Expand Up @@ -741,6 +755,22 @@ public Mono<UserProfileDTO> buildUserProfileDTO(User user) {
});
}

protected Mono<UserProfileDTO> getAnonymousUserProfile(User user) {
return this.isUsersEmpty().map(isUsersEmpty -> {
UserProfileDTO profile = new UserProfileDTO();
profile.setEmail(user.getEmail());
profile.setUsername(user.getUsername());
profile.setAnonymous(true);
profile.setEnabled(user.isEnabled());
profile.setEnableTelemetry(!commonConfig.isTelemetryDisabled());
profile.setEmptyInstance(isUsersEmpty);
// Intercom consent is defaulted to true on cloud hosting
profile.setIntercomConsentGiven(commonConfig.isCloudHosting());

return profile;
});
}

private EmailTokenDTO parseValueFromEncryptedToken(String encryptedToken) {
String decryptString = EncryptionHelper.decrypt(encryptedToken);
List<NameValuePair> nameValuePairs = WWWFormCodec.parse(decryptString, StandardCharsets.UTF_8);
Expand Down Expand Up @@ -807,12 +837,14 @@ public Mono<Boolean> resendEmailVerification(
emailVerificationToken.setEmail(user.getEmail());
emailVerificationToken.setTokenGeneratedAt(Instant.now());
emailVerificationToken.setTokenHash(passwordEncoder.encode(token));
emailVerificationToken.setOrganizationId(user.getOrganizationId());
return Mono.just(emailVerificationToken);
}))
.map(emailVerificationToken -> {
// generate new token and update in db
emailVerificationToken.setTokenHash(passwordEncoder.encode(token));
emailVerificationToken.setTokenGeneratedAt(Instant.now());
emailVerificationToken.setOrganizationId(user.getOrganizationId());
return emailVerificationToken;
});
});
Expand All @@ -822,9 +854,11 @@ public Mono<Boolean> resendEmailVerification(
.flatMap(tuple -> {
EmailVerificationToken emailVerificationToken = tuple.getT1();
User user = tuple.getT2();
List<NameValuePair> nameValuePairs = new ArrayList<>(2);
List<NameValuePair> nameValuePairs = new ArrayList<>(3);
nameValuePairs.add(new BasicNameValuePair("email", emailVerificationToken.getEmail()));
nameValuePairs.add(new BasicNameValuePair("token", token));
nameValuePairs.add(
new BasicNameValuePair("organizationId", emailVerificationToken.getOrganizationId()));
String urlParams = WWWFormCodec.format(nameValuePairs, StandardCharsets.UTF_8);
String redirectUrlCopy = redirectUrl;
if (redirectUrlCopy == null) {
Expand All @@ -835,6 +869,7 @@ public Mono<Boolean> resendEmailVerification(
resendEmailVerificationDTO.getBaseUrl(),
EncryptionHelper.encrypt(urlParams),
URLEncoder.encode(emailVerificationToken.getEmail(), StandardCharsets.UTF_8),
emailVerificationToken.getOrganizationId(),
redirectUrlCopy);

return emailService.sendEmailVerificationEmail(
Expand Down Expand Up @@ -862,6 +897,7 @@ public Mono<Void> verifyEmailVerificationToken(ServerWebExchange exchange) {
String requestEmail = formData.getFirst("email");
String requestedToken = formData.getFirst("token");
String redirectUrl = formData.getFirst("redirectUrl");
String organizationId = formData.getFirst("organizationId");
String enableFirstTimeUserExperienceParam =
ObjectUtils.defaultIfNull(formData.getFirst("enableFirstTimeUserExperience"), "false");

Expand All @@ -887,19 +923,30 @@ public Mono<Void> verifyEmailVerificationToken(ServerWebExchange exchange) {

try {
parsedEmailTokenDTO = parseValueFromEncryptedToken(requestedToken);
} catch (ArrayIndexOutOfBoundsException | IllegalStateException | IllegalArgumentException e) {
errorRedirectUrl = getEmailVerificationErrorRedirectUrl(
AppsmithError.INVALID_PARAMETER, requestEmail, FieldName.TOKEN);
} catch (IllegalStateException | IllegalArgumentException e) {
errorRedirectUrl =
getEmailVerificationErrorRedirectUrl(AppsmithError.INVALID_EMAIL_VERIFICATION, requestEmail);
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), URI.create(errorRedirectUrl));
}

Mono<WebSession> sessionMono = exchange.getSession();
Mono<SecurityContext> securityContextMono = ReactiveSecurityContextHolder.getContext();
Mono<User> userMono = repository.findByEmail(parsedEmailTokenDTO.getEmail());
if (parsedEmailTokenDTO == null) {
errorRedirectUrl =
getEmailVerificationErrorRedirectUrl(AppsmithError.INVALID_EMAIL_VERIFICATION, requestEmail);
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), URI.create(errorRedirectUrl));
}

Mono<EmailVerificationToken> emailVerificationTokenMono = emailVerificationTokenRepository
.findByEmail(parsedEmailTokenDTO.getEmail())
.defaultIfEmpty(new EmailVerificationToken());
.findByEmail(requestEmail)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "email token")));

Mono<User> userMono = repository
.findByEmailAndOrganizationId(requestEmail, organizationId)
.switchIfEmpty(
Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "user", requestEmail)));

Mono<WebSession> sessionMono = exchange.getSession();

Mono<SecurityContext> securityContextMono = ReactiveSecurityContextHolder.getContext();

return Mono.zip(emailVerificationTokenMono, userMono, sessionMono, securityContextMono)
.flatMap(tuple -> {
Expand All @@ -915,6 +962,14 @@ public Mono<Void> verifyEmailVerificationToken(ServerWebExchange exchange) {
return redirectStrategy.sendRedirect(
webFilterExchange.getExchange(), URI.create(errorRedirectUrl1));
}

if (!Objects.equals(emailVerificationToken.getOrganizationId(), organizationId)) {
errorRedirectUrl1 = getEmailVerificationErrorRedirectUrl(
AppsmithError.INVALID_PARAMETER, requestEmail, "Organization");
return redirectStrategy.sendRedirect(
webFilterExchange.getExchange(), URI.create(errorRedirectUrl1));
}

if (FALSE.equals(isEmailVerificationTokenValid(emailVerificationToken))) {
errorRedirectUrl1 = getEmailVerificationErrorRedirectUrl(
AppsmithError.EMAIL_VERIFICATION_TOKEN_EXPIRED, requestEmail);
Expand Down