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

+1
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

+4
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

+1-1
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

+10
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

+5
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

+4
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

+49-1
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

+2
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

+3
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.

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java

+4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
107107

108108
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/me"),
109109
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/currentUser"),
110+
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/lost-password"),
111+
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/reset-lost-password"),
110112

111113
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, GROUP_URL + "/list"), // application view
112114
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, QUERY_URL + "/execute"), // application view
@@ -133,6 +135,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
133135
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.APPLICATION_URL + "/marketplace-apps"), // marketplace apps
134136
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/me"),
135137
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/currentUser"),
138+
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/lost-password"),
139+
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/reset-lost-password"),
136140
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.GROUP_URL + "/list"),
137141
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.QUERY_URL + "/execute"),
138142
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.MATERIAL_URL + "/**"),

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public Mono<String> resetPassword(String userId) {
6565
.then(userService.resetPassword(userId));
6666
}
6767

68+
public Mono<Boolean> lostPassword(String userEmail) {
69+
return userService.lostPassword(userEmail);
70+
}
71+
72+
public Mono<Boolean> resetLostPassword(String userEmail, String token, String newPassword) {
73+
return userService.resetLostPassword(userEmail, token, newPassword);
74+
}
75+
6876
// ========================== TOKEN OPERATIONS START ==========================
6977

7078
public Mono<Void> saveToken(String userId, String source, String token) {

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java

+20
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,26 @@ public Mono<ResponseView<String>> resetPassword(@RequestBody ResetPasswordReques
146146

147147
}
148148

149+
@Override
150+
public Mono<ResponseView<Boolean>> lostPassword(@RequestBody LostPasswordRequest request) {
151+
if (StringUtils.isBlank(request.userEmail())) {
152+
return Mono.empty();
153+
}
154+
return userApiService.lostPassword(request.userEmail())
155+
.map(ResponseView::success);
156+
}
157+
158+
@Override
159+
public Mono<ResponseView<Boolean>> resetLostPassword(@RequestBody ResetLostPasswordRequest request) {
160+
if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(request.token())
161+
|| StringUtils.isBlank(request.newPassword())) {
162+
return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER");
163+
}
164+
165+
return userApiService.resetLostPassword(request.userEmail(), request.token(), request.newPassword())
166+
.map(ResponseView::success);
167+
}
168+
149169
@Override
150170
public Mono<ResponseView<Boolean>> setPassword(@RequestParam String password) {
151171
if (StringUtils.isBlank(password)) {

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java

+12
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ public interface UserEndpoints
121121
@PostMapping("/reset-password")
122122
public Mono<ResponseView<String>> resetPassword(@RequestBody ResetPasswordRequest request);
123123

124+
@PostMapping("/lost-password")
125+
public Mono<ResponseView<Boolean>> lostPassword(@RequestBody LostPasswordRequest request);
126+
127+
@PostMapping("/reset-lost-password")
128+
public Mono<ResponseView<Boolean>> resetLostPassword(@RequestBody ResetLostPasswordRequest request);
129+
124130
@Operation(
125131
tags = TAG_USER_PASSWORD_MANAGEMENT,
126132
operationId = "setPassword",
@@ -151,6 +157,12 @@ public interface UserEndpoints
151157
public record ResetPasswordRequest(String userId) {
152158
}
153159

160+
public record LostPasswordRequest(String userEmail) {
161+
}
162+
163+
public record ResetLostPasswordRequest(String token, String userEmail, String newPassword) {
164+
}
165+
154166
public record UpdatePasswordRequest(String oldPassword, String newPassword) {
155167
}
156168

server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ spring:
99
main:
1010
allow-bean-definition-overriding: true
1111
allow-circular-references: true
12+
mail:
13+
host: smtp.gmail.com
14+
port: 587
15+
username: yourmail@gmail.com
16+
password: yourpass
17+
properties:
18+
mail:
19+
smtp:
20+
auth: true
21+
ssl:
22+
enable: false
23+
starttls:
24+
enable: true
25+
required: true
26+
transport:
27+
protocol: smtp
1228

1329
logging:
1430
level:
@@ -58,6 +74,8 @@ common:
5874
password: Password@123
5975
marketplace:
6076
private-mode: false
77+
lowcoder-public-url: http://localhost:8080
78+
notifications-email-sender: info@lowcoder.org
6179

6280
material:
6381
mongodb-grid-fs:

server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml

+18-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,22 @@ spring:
1818
max-in-memory-size: 20MB
1919
webflux:
2020
base-path: /
21-
21+
mail:
22+
host: ${LOWCODER_ADMIN_SMTP_HOST:smtp.gmail.com}
23+
port: ${LOWCODER_ADMIN_SMTP_PORT:587}
24+
username: ${LOWCODER_ADMIN_SMTP_USERNAME:yourmail@gmail.com}
25+
password: ${LOWCODER_ADMIN_SMTP_PASSWORD:yourpass}
26+
properties:
27+
mail:
28+
smtp:
29+
auth: ${LOWCODER_ADMIN_SMTP_AUTH:true}
30+
ssl:
31+
enable: ${LOWCODER_ADMIN_SMTP_SSL_ENABLED:false}
32+
starttls:
33+
enable: ${LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED:true}
34+
required: ${LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED:true}
35+
transport:
36+
protocol: smtp
2237
server:
2338
compression:
2439
enabled: true
@@ -57,6 +72,8 @@ common:
5772
- ${LOWCODER_PLUGINS_DIR:plugins}
5873
marketplace:
5974
private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true}
75+
lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:8080}
76+
notifications-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:info@lowcoder.org}
6077

6178
material:
6279
mongodb-grid-fs:

0 commit comments

Comments
 (0)