Skip to content

Commit

Permalink
Merge branch 'dev' into module-layers-issue
Browse files Browse the repository at this point in the history
  • Loading branch information
FalkWolsky authored Mar 15, 2024
2 parents 9137090 + cdbbb6c commit 8d6c4bd
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class GridCompOperator {
return true;
}

// FALK TODO: How can we enable Copy and Paste of components across Browser Tabs / Windows?
static pasteComp(editorState: EditorState) {
if (!this.copyComps || _.size(this.copyComps) <= 0 || !this.sourcePositionParams) {
messageInstance.info(trans("gridCompOperator.selectCompFirst"));
Expand Down
4 changes: 4 additions & 0 deletions server/api-service/lowcoder-domain/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
<groupId>org.lowcoder</groupId>
<artifactId>lowcoder-infra</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<dependency>
<groupId>com.github.cloudyrock.mongock</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public OrganizationCommonSettings getCommonSettings() {
public static class OrganizationCommonSettings extends HashMap<String, Object> {
public static final String USER_EXTRA_TRANSFORMER = "userExtraTransformer";
public static final String USER_EXTRA_TRANSFORMER_UPDATE_TIME = "userExtraTransformer_updateTime";

public static final String PASSWORD_RESET_EMAIL_TEMPLATE = "passwordResetEmailTemplate";
// custom branding configs
public static final String CUSTOM_BRANDING_KEY = "branding";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lowcoder.domain.organization.service;

import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG;
import static org.lowcoder.domain.organization.model.Organization.OrganizationCommonSettings.PASSWORD_RESET_EMAIL_TEMPLATE;
import static org.lowcoder.domain.organization.model.OrganizationState.ACTIVE;
import static org.lowcoder.domain.organization.model.OrganizationState.DELETED;
import static org.lowcoder.domain.util.QueryDslUtils.fieldName;
Expand Down Expand Up @@ -56,6 +57,12 @@ public class OrganizationServiceImpl implements OrganizationService {

private final Conf<Integer> logoMaxSizeInKb;

private static final String PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT = "<p>Hi, %s<br/>" +
"Here is the link to reset your password: %s<br/>" +
"Please note that the link will expire after 12 hours.<br/><br/>" +
"Regards,<br/>" +
"The Lowcoder Team</p>";

@Autowired
private AssetRepository assetRepository;

Expand Down Expand Up @@ -151,6 +158,9 @@ public Mono<Organization> create(Organization organization, String creatorId, bo
if (organization == null || StringUtils.isNotBlank(organization.getId())) {
return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ORGANIZATION));
}
organization.setCommonSettings(new OrganizationCommonSettings());
organization.getCommonSettings().put("PASSWORD_RESET_EMAIL_TEMPLATE",
PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT);
organization.setState(ACTIVE);
return Mono.just(organization);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.google.common.base.Suppliers.memoize;
import static org.lowcoder.infra.util.AssetUtils.toAssetPath;

import java.time.Instant;
import java.util.*;
import java.util.function.Supplier;

Expand Down Expand Up @@ -52,6 +53,10 @@ public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterM
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;

private String passwordResetToken;

private Instant passwordResetTokenExpiry;

@Transient
Boolean isAnonymous = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.lowcoder.domain.user.service;

import jakarta.mail.internet.MimeMessage;
import lombok.extern.slf4j.Slf4j;
import org.lowcoder.sdk.config.CommonConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

@Service
@Slf4j(topic = "EmailCommunicationService")
public class EmailCommunicationService {

@Autowired
private JavaMailSender javaMailSender;

@Autowired
private CommonConfig config;

public boolean sendPasswordResetEmail(String to, String token, String message) {
try {
String subject = "Reset Your Lost Password";
MimeMessage mimeMessage = javaMailSender.createMimeMessage();

MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

mimeMessageHelper.setFrom(config.getLostPasswordEmailSender());
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);

// Construct the message with the token link
String resetLink = config.getLowcoderPublicUrl() + "/lost-password?token=" + token;
String formattedMessage = String.format(message, to, resetLink);
mimeMessageHelper.setText(formattedMessage, true); // Set HTML to true to allow links

javaMailSender.send(mimeMessage);

return true;

} catch (Exception e) {
log.error("Failed to send mail to: {}, Exception: ", to, e);
return false;
}


}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public interface UserService {

Mono<String> resetPassword(String userId);

Mono<Boolean> lostPassword(String userEmail);

Mono<Boolean> resetLostPassword(String userEmail, String token, String newPassword);

Mono<Boolean> setPassword(String userId, String password);

Mono<UserDetail> buildUserDetail(User user, boolean withoutDynamicGroups);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.lowcoder.domain.group.service.GroupService;
import org.lowcoder.domain.organization.model.OrgMember;
import org.lowcoder.domain.organization.service.OrgMemberService;
import org.lowcoder.domain.organization.service.OrganizationService;
import org.lowcoder.domain.user.model.*;
import org.lowcoder.domain.user.model.User.TransformedUserInfo;
import org.lowcoder.domain.user.repository.UserRepository;
Expand All @@ -29,6 +30,7 @@
import org.lowcoder.sdk.constants.WorkspaceMode;
import org.lowcoder.sdk.exception.BizError;
import org.lowcoder.sdk.exception.BizException;
import org.lowcoder.sdk.util.HashUtils;
import org.lowcoder.sdk.util.LocaleUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
Expand All @@ -40,6 +42,8 @@

import javax.annotation.Nonnull;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -69,12 +73,15 @@ public class UserServiceImpl implements UserService {
@Autowired
private OrgMemberService orgMemberService;
@Autowired
private OrganizationService organizationService;
@Autowired
private GroupService groupService;
@Autowired
private CommonConfig commonConfig;
@Autowired
private AuthenticationService authenticationService;

@Autowired
private EmailCommunicationService emailCommunicationService;
private Conf<Integer> avatarMaxSizeInKb;

@PostConstruct
Expand Down Expand Up @@ -262,6 +269,47 @@ public Mono<String> resetPassword(String userId) {
});
}

@Override
public Mono<Boolean> lostPassword(String userEmail) {
return findByName(userEmail)
.zipWhen(user -> orgMemberService.getCurrentOrgMember(user.getId())
.flatMap(orgMember -> organizationService.getById(orgMember.getOrgId()))
.map(organization -> organization.getCommonSettings().get("PASSWORD_RESET_EMAIL_TEMPLATE")))
.flatMap(tuple -> {
User user = tuple.getT1();
String emailTemplate = (String)tuple.getT2();

String token = generateNewRandomPwd();
Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS);
if (!emailCommunicationService.sendPasswordResetEmail(userEmail, token, emailTemplate)) {
return Mono.empty();
}
user.setPasswordResetToken(HashUtils.hash(token.getBytes()));
user.setPasswordResetTokenExpiry(tokenExpiry);
return repository.save(user).then(Mono.empty());
});
}

@Override
public Mono<Boolean> resetLostPassword(String userEmail, String token, String newPassword) {
return findByName(userEmail)
.flatMap(user -> {
if (Instant.now().until(user.getPasswordResetTokenExpiry(), ChronoUnit.MINUTES) <= 0) {
return ofError(BizError.INVALID_PARAMETER, "TOKEN_EXPIRED");
}

if (!StringUtils.equals(HashUtils.hash(token.getBytes()), user.getPasswordResetToken())) {
return ofError(BizError.INVALID_PARAMETER, "INVALID_TOKEN");
}

user.setPassword(encryptionService.encryptPassword(newPassword));
user.setPasswordResetToken(StringUtils.EMPTY);
user.setPasswordResetTokenExpiry(Instant.now());
return repository.save(user)
.thenReturn(true);
});
}

@SuppressWarnings("SpellCheckingInspection")
@Nonnull
private static String generateNewRandomPwd() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class CommonConfig {
private List<String> pluginDirs = new ArrayList<>();
private SuperAdmin superAdmin = new SuperAdmin();
private Marketplace marketplace = new Marketplace();
private String lowcoderPublicUrl;
private String lostPasswordEmailSender;

public boolean isSelfHost() {
return !isCloud();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ CANNOT_DELETE_SYSTEM_GROUP=System group cannot be deleted.
NEED_DEV_TO_CREATE_RESOURCE=Invalid operation, workspace developers or admin required.
UNABLE_TO_FIND_VALID_ORG=Cannot find a valid workspace for current user.
USER_BANNED=Current account is frozen.
SENDING_EMAIL_FAILED=Email could not be sent. Please check the smtp settings for the org.
TOKEN_EXPIRED=Token to reset the password has expired
INVALID_TOKEN=Invalid token received for password reset request
# invitation
INVALID_INVITATION_CODE=Invitation code not found.
ALREADY_IN_ORGANIZATION=You are already in this workspace.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {

ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/me"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/currentUser"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/lost-password"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/reset-lost-password"),

ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, GROUP_URL + "/list"), // application view
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, QUERY_URL + "/execute"), // application view
Expand All @@ -133,6 +135,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.APPLICATION_URL + "/marketplace-apps"), // marketplace apps
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/me"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/currentUser"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/lost-password"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/reset-lost-password"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.GROUP_URL + "/list"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.QUERY_URL + "/execute"),
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.MATERIAL_URL + "/**"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ public Mono<String> resetPassword(String userId) {
.then(userService.resetPassword(userId));
}

public Mono<Boolean> lostPassword(String userEmail) {
return userService.lostPassword(userEmail);
}

public Mono<Boolean> resetLostPassword(String userEmail, String token, String newPassword) {
return userService.resetLostPassword(userEmail, token, newPassword);
}

// ========================== TOKEN OPERATIONS START ==========================

public Mono<Void> saveToken(String userId, String source, String token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,26 @@ public Mono<ResponseView<String>> resetPassword(@RequestBody ResetPasswordReques

}

@Override
public Mono<ResponseView<Boolean>> lostPassword(@RequestBody LostPasswordRequest request) {
if (StringUtils.isBlank(request.userEmail())) {
return Mono.empty();
}
return userApiService.lostPassword(request.userEmail())
.map(ResponseView::success);
}

@Override
public Mono<ResponseView<Boolean>> resetLostPassword(@RequestBody ResetLostPasswordRequest request) {
if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(request.token())
|| StringUtils.isBlank(request.newPassword())) {
return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER");
}

return userApiService.resetLostPassword(request.userEmail(), request.token(), request.newPassword())
.map(ResponseView::success);
}

@Override
public Mono<ResponseView<Boolean>> setPassword(@RequestParam String password) {
if (StringUtils.isBlank(password)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ public interface UserEndpoints
@PostMapping("/reset-password")
public Mono<ResponseView<String>> resetPassword(@RequestBody ResetPasswordRequest request);

@PostMapping("/lost-password")
public Mono<ResponseView<Boolean>> lostPassword(@RequestBody LostPasswordRequest request);

@PostMapping("/reset-lost-password")
public Mono<ResponseView<Boolean>> resetLostPassword(@RequestBody ResetLostPasswordRequest request);

@Operation(
tags = TAG_USER_PASSWORD_MANAGEMENT,
operationId = "setPassword",
Expand Down Expand Up @@ -151,6 +157,12 @@ public interface UserEndpoints
public record ResetPasswordRequest(String userId) {
}

public record LostPasswordRequest(String userEmail) {
}

public record ResetLostPasswordRequest(String token, String userEmail, String newPassword) {
}

public record UpdatePasswordRequest(String oldPassword, String newPassword) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ spring:
main:
allow-bean-definition-overriding: true
allow-circular-references: true
mail:
host: smtp.gmail.com
port: 587
username: [email protected]
password: yourpass
properties:
mail:
smtp:
auth: true
ssl:
enable: false
starttls:
enable: true
required: true
transport:
protocol: smtp

logging:
level:
Expand Down Expand Up @@ -58,6 +74,8 @@ common:
password: Password@123
marketplace:
private-mode: false
lowcoder-public-url: http://localhost:8080
notifications-email-sender: [email protected]

material:
mongodb-grid-fs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,22 @@ spring:
max-in-memory-size: 20MB
webflux:
base-path: /

mail:
host: ${LOWCODER_ADMIN_SMTP_HOST:smtp.gmail.com}
port: ${LOWCODER_ADMIN_SMTP_PORT:587}
username: ${LOWCODER_ADMIN_SMTP_USERNAME:[email protected]}
password: ${LOWCODER_ADMIN_SMTP_PASSWORD:yourpass}
properties:
mail:
smtp:
auth: ${LOWCODER_ADMIN_SMTP_AUTH:true}
ssl:
enable: ${LOWCODER_ADMIN_SMTP_SSL_ENABLED:false}
starttls:
enable: ${LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED:true}
required: ${LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED:true}
transport:
protocol: smtp
server:
compression:
enabled: true
Expand Down Expand Up @@ -57,6 +72,8 @@ common:
- ${LOWCODER_PLUGINS_DIR:plugins}
marketplace:
private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true}
lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:8080}
notifications-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:[email protected]}

material:
mongodb-grid-fs:
Expand Down

0 comments on commit 8d6c4bd

Please sign in to comment.