logoMaxSizeInKb;
+ private static final String PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT = "Hi, %s
" +
+ "Here is the link to reset your password: %s
" +
+ "Please note that the link will expire after 12 hours.
" +
+ "Regards,
" +
+ "The Lowcoder Team
";
+
@Autowired
private AssetRepository assetRepository;
@@ -151,6 +158,9 @@ public Mono 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);
})
diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java
index 80efd4ac5..a8350e0b5 100644
--- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java
+++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java
@@ -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;
@@ -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;
diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java
new file mode 100644
index 000000000..439aa65a8
--- /dev/null
+++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java
@@ -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;
+ }
+
+
+ }
+
+}
diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java
index aebed82ef..667f48c44 100644
--- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java
+++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java
@@ -50,6 +50,10 @@ public interface UserService {
Mono resetPassword(String userId);
+ Mono lostPassword(String userEmail);
+
+ Mono resetLostPassword(String userEmail, String token, String newPassword);
+
Mono setPassword(String userId, String password);
Mono buildUserDetail(User user, boolean withoutDynamicGroups);
diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java
index 16ac4f8e2..056bd69dc 100644
--- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java
+++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java
@@ -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;
@@ -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;
@@ -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;
@@ -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 avatarMaxSizeInKb;
@PostConstruct
@@ -262,6 +269,47 @@ public Mono resetPassword(String userId) {
});
}
+ @Override
+ public Mono 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 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() {
diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java
index 8334e5562..b6fed12ee 100644
--- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java
+++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java
@@ -47,6 +47,8 @@ public class CommonConfig {
private List pluginDirs = new ArrayList<>();
private SuperAdmin superAdmin = new SuperAdmin();
private Marketplace marketplace = new Marketplace();
+ private String lowcoderPublicUrl;
+ private String lostPasswordEmailSender;
public boolean isSelfHost() {
return !isCloud();
diff --git a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties
index d81ecbdf2..1e3a18c31 100644
--- a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties
+++ b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties
@@ -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.
diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java
index 555c0a64b..e255cc5dc 100644
--- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java
+++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java
@@ -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
@@ -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 + "/**"),
diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java
index 252a4f837..2b5ce21fb 100644
--- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java
+++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java
@@ -65,6 +65,14 @@ public Mono resetPassword(String userId) {
.then(userService.resetPassword(userId));
}
+ public Mono lostPassword(String userEmail) {
+ return userService.lostPassword(userEmail);
+ }
+
+ public Mono resetLostPassword(String userEmail, String token, String newPassword) {
+ return userService.resetLostPassword(userEmail, token, newPassword);
+ }
+
// ========================== TOKEN OPERATIONS START ==========================
public Mono saveToken(String userId, String source, String token) {
diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java
index d865cba4f..56cccffd5 100644
--- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java
+++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java
@@ -146,6 +146,26 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques
}
+ @Override
+ public Mono> lostPassword(@RequestBody LostPasswordRequest request) {
+ if (StringUtils.isBlank(request.userEmail())) {
+ return Mono.empty();
+ }
+ return userApiService.lostPassword(request.userEmail())
+ .map(ResponseView::success);
+ }
+
+ @Override
+ public Mono> 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> setPassword(@RequestParam String password) {
if (StringUtils.isBlank(password)) {
diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java
index 96c93c472..196925134 100644
--- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java
+++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java
@@ -121,6 +121,12 @@ public interface UserEndpoints
@PostMapping("/reset-password")
public Mono> resetPassword(@RequestBody ResetPasswordRequest request);
+ @PostMapping("/lost-password")
+ public Mono> lostPassword(@RequestBody LostPasswordRequest request);
+
+ @PostMapping("/reset-lost-password")
+ public Mono> resetLostPassword(@RequestBody ResetLostPasswordRequest request);
+
@Operation(
tags = TAG_USER_PASSWORD_MANAGEMENT,
operationId = "setPassword",
@@ -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) {
}
diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml
index d7ad21a53..29128279c 100644
--- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml
+++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml
@@ -9,6 +9,22 @@ spring:
main:
allow-bean-definition-overriding: true
allow-circular-references: true
+ mail:
+ host: smtp.gmail.com
+ port: 587
+ username: yourmail@gmail.com
+ password: yourpass
+ properties:
+ mail:
+ smtp:
+ auth: true
+ ssl:
+ enable: false
+ starttls:
+ enable: true
+ required: true
+ transport:
+ protocol: smtp
logging:
level:
@@ -58,6 +74,8 @@ common:
password: Password@123
marketplace:
private-mode: false
+ lowcoder-public-url: http://localhost:8080
+ notifications-email-sender: info@lowcoder.org
material:
mongodb-grid-fs:
diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml
index 30cd78b3b..8e51ca239 100644
--- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml
+++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml
@@ -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:yourmail@gmail.com}
+ 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
@@ -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:info@lowcoder.org}
material:
mongodb-grid-fs: