Skip to content

Commit 8d6c4bd

Browse files
authored
Merge branch 'dev' into module-layers-issue
2 parents 9137090 + cdbbb6c commit 8d6c4bd

File tree

16 files changed

+208
-3
lines changed

16 files changed

+208
-3
lines changed

client/packages/lowcoder/src/comps/utils/gridCompOperator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class GridCompOperator {
7777
return true;
7878
}
7979

80+
// FALK TODO: How can we enable Copy and Paste of components across Browser Tabs / Windows?
8081
static pasteComp(editorState: EditorState) {
8182
if (!this.copyComps || _.size(this.copyComps) <= 0 || !this.sourcePositionParams) {
8283
messageInstance.info(trans("gridCompOperator.selectCompFirst"));

server/api-service/lowcoder-domain/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
<groupId>org.lowcoder</groupId>
5151
<artifactId>lowcoder-infra</artifactId>
5252
</dependency>
53+
<dependency>
54+
<groupId>org.springframework.boot</groupId>
55+
<artifactId>spring-boot-starter-mail</artifactId>
56+
</dependency>
5357

5458
<dependency>
5559
<groupId>com.github.cloudyrock.mongock</groupId>

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public OrganizationCommonSettings getCommonSettings() {
8686
public static class OrganizationCommonSettings extends HashMap<String, Object> {
8787
public static final String USER_EXTRA_TRANSFORMER = "userExtraTransformer";
8888
public static final String USER_EXTRA_TRANSFORMER_UPDATE_TIME = "userExtraTransformer_updateTime";
89-
89+
public static final String PASSWORD_RESET_EMAIL_TEMPLATE = "passwordResetEmailTemplate";
9090
// custom branding configs
9191
public static final String CUSTOM_BRANDING_KEY = "branding";
9292
}

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.lowcoder.domain.organization.service;
22

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

5758
private final Conf<Integer> logoMaxSizeInKb;
5859

60+
private static final String PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT = "<p>Hi, %s<br/>" +
61+
"Here is the link to reset your password: %s<br/>" +
62+
"Please note that the link will expire after 12 hours.<br/><br/>" +
63+
"Regards,<br/>" +
64+
"The Lowcoder Team</p>";
65+
5966
@Autowired
6067
private AssetRepository assetRepository;
6168

@@ -151,6 +158,9 @@ public Mono<Organization> create(Organization organization, String creatorId, bo
151158
if (organization == null || StringUtils.isNotBlank(organization.getId())) {
152159
return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ORGANIZATION));
153160
}
161+
organization.setCommonSettings(new OrganizationCommonSettings());
162+
organization.getCommonSettings().put("PASSWORD_RESET_EMAIL_TEMPLATE",
163+
PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT);
154164
organization.setState(ACTIVE);
155165
return Mono.just(organization);
156166
})

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static com.google.common.base.Suppliers.memoize;
44
import static org.lowcoder.infra.util.AssetUtils.toAssetPath;
55

6+
import java.time.Instant;
67
import java.util.*;
78
import java.util.function.Supplier;
89

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

56+
private String passwordResetToken;
57+
58+
private Instant passwordResetTokenExpiry;
59+
5560
@Transient
5661
Boolean isAnonymous = false;
5762

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.lowcoder.domain.user.service;
2+
3+
import jakarta.mail.internet.MimeMessage;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.lowcoder.sdk.config.CommonConfig;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.mail.javamail.JavaMailSender;
8+
import org.springframework.mail.javamail.MimeMessageHelper;
9+
import org.springframework.stereotype.Service;
10+
11+
@Service
12+
@Slf4j(topic = "EmailCommunicationService")
13+
public class EmailCommunicationService {
14+
15+
@Autowired
16+
private JavaMailSender javaMailSender;
17+
18+
@Autowired
19+
private CommonConfig config;
20+
21+
public boolean sendPasswordResetEmail(String to, String token, String message) {
22+
try {
23+
String subject = "Reset Your Lost Password";
24+
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
25+
26+
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
27+
28+
mimeMessageHelper.setFrom(config.getLostPasswordEmailSender());
29+
mimeMessageHelper.setTo(to);
30+
mimeMessageHelper.setSubject(subject);
31+
32+
// Construct the message with the token link
33+
String resetLink = config.getLowcoderPublicUrl() + "/lost-password?token=" + token;
34+
String formattedMessage = String.format(message, to, resetLink);
35+
mimeMessageHelper.setText(formattedMessage, true); // Set HTML to true to allow links
36+
37+
javaMailSender.send(mimeMessage);
38+
39+
return true;
40+
41+
} catch (Exception e) {
42+
log.error("Failed to send mail to: {}, Exception: ", to, e);
43+
return false;
44+
}
45+
46+
47+
}
48+
49+
}

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public interface UserService {
5050

5151
Mono<String> resetPassword(String userId);
5252

53+
Mono<Boolean> lostPassword(String userEmail);
54+
55+
Mono<Boolean> resetLostPassword(String userEmail, String token, String newPassword);
56+
5357
Mono<Boolean> setPassword(String userId, String password);
5458

5559
Mono<UserDetail> buildUserDetail(User user, boolean withoutDynamicGroups);

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.lowcoder.domain.group.service.GroupService;
1717
import org.lowcoder.domain.organization.model.OrgMember;
1818
import org.lowcoder.domain.organization.service.OrgMemberService;
19+
import org.lowcoder.domain.organization.service.OrganizationService;
1920
import org.lowcoder.domain.user.model.*;
2021
import org.lowcoder.domain.user.model.User.TransformedUserInfo;
2122
import org.lowcoder.domain.user.repository.UserRepository;
@@ -29,6 +30,7 @@
2930
import org.lowcoder.sdk.constants.WorkspaceMode;
3031
import org.lowcoder.sdk.exception.BizError;
3132
import org.lowcoder.sdk.exception.BizException;
33+
import org.lowcoder.sdk.util.HashUtils;
3234
import org.lowcoder.sdk.util.LocaleUtils;
3335
import org.springframework.beans.factory.annotation.Autowired;
3436
import org.springframework.dao.DuplicateKeyException;
@@ -40,6 +42,8 @@
4042

4143
import javax.annotation.Nonnull;
4244
import java.security.SecureRandom;
45+
import java.time.Instant;
46+
import java.time.temporal.ChronoUnit;
4347
import java.util.*;
4448
import java.util.function.Function;
4549
import java.util.stream.Collectors;
@@ -69,12 +73,15 @@ public class UserServiceImpl implements UserService {
6973
@Autowired
7074
private OrgMemberService orgMemberService;
7175
@Autowired
76+
private OrganizationService organizationService;
77+
@Autowired
7278
private GroupService groupService;
7379
@Autowired
7480
private CommonConfig commonConfig;
7581
@Autowired
7682
private AuthenticationService authenticationService;
77-
83+
@Autowired
84+
private EmailCommunicationService emailCommunicationService;
7885
private Conf<Integer> avatarMaxSizeInKb;
7986

8087
@PostConstruct
@@ -262,6 +269,47 @@ public Mono<String> resetPassword(String userId) {
262269
});
263270
}
264271

272+
@Override
273+
public Mono<Boolean> lostPassword(String userEmail) {
274+
return findByName(userEmail)
275+
.zipWhen(user -> orgMemberService.getCurrentOrgMember(user.getId())
276+
.flatMap(orgMember -> organizationService.getById(orgMember.getOrgId()))
277+
.map(organization -> organization.getCommonSettings().get("PASSWORD_RESET_EMAIL_TEMPLATE")))
278+
.flatMap(tuple -> {
279+
User user = tuple.getT1();
280+
String emailTemplate = (String)tuple.getT2();
281+
282+
String token = generateNewRandomPwd();
283+
Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS);
284+
if (!emailCommunicationService.sendPasswordResetEmail(userEmail, token, emailTemplate)) {
285+
return Mono.empty();
286+
}
287+
user.setPasswordResetToken(HashUtils.hash(token.getBytes()));
288+
user.setPasswordResetTokenExpiry(tokenExpiry);
289+
return repository.save(user).then(Mono.empty());
290+
});
291+
}
292+
293+
@Override
294+
public Mono<Boolean> resetLostPassword(String userEmail, String token, String newPassword) {
295+
return findByName(userEmail)
296+
.flatMap(user -> {
297+
if (Instant.now().until(user.getPasswordResetTokenExpiry(), ChronoUnit.MINUTES) <= 0) {
298+
return ofError(BizError.INVALID_PARAMETER, "TOKEN_EXPIRED");
299+
}
300+
301+
if (!StringUtils.equals(HashUtils.hash(token.getBytes()), user.getPasswordResetToken())) {
302+
return ofError(BizError.INVALID_PARAMETER, "INVALID_TOKEN");
303+
}
304+
305+
user.setPassword(encryptionService.encryptPassword(newPassword));
306+
user.setPasswordResetToken(StringUtils.EMPTY);
307+
user.setPasswordResetTokenExpiry(Instant.now());
308+
return repository.save(user)
309+
.thenReturn(true);
310+
});
311+
}
312+
265313
@SuppressWarnings("SpellCheckingInspection")
266314
@Nonnull
267315
private static String generateNewRandomPwd() {

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public class CommonConfig {
4747
private List<String> pluginDirs = new ArrayList<>();
4848
private SuperAdmin superAdmin = new SuperAdmin();
4949
private Marketplace marketplace = new Marketplace();
50+
private String lowcoderPublicUrl;
51+
private String lostPasswordEmailSender;
5052

5153
public boolean isSelfHost() {
5254
return !isCloud();

server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ CANNOT_DELETE_SYSTEM_GROUP=System group cannot be deleted.
1717
NEED_DEV_TO_CREATE_RESOURCE=Invalid operation, workspace developers or admin required.
1818
UNABLE_TO_FIND_VALID_ORG=Cannot find a valid workspace for current user.
1919
USER_BANNED=Current account is frozen.
20+
SENDING_EMAIL_FAILED=Email could not be sent. Please check the smtp settings for the org.
21+
TOKEN_EXPIRED=Token to reset the password has expired
22+
INVALID_TOKEN=Invalid token received for password reset request
2023
# invitation
2124
INVALID_INVITATION_CODE=Invitation code not found.
2225
ALREADY_IN_ORGANIZATION=You are already in this workspace.

0 commit comments

Comments
 (0)