Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Lost Password Feature#715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
a2bd841
feature: Initial Implementation of `lost-password` api endpoint
irfan-ikhwaFeb 14, 2024
ceabd3b
feature: Initial Implementation of Email Sending Feature with dummy D…
irfan-ikhwaFeb 17, 2024
c4b18bd
feature: Initial Password reset flow implementation
irfan-ikhwaFeb 18, 2024
96081d3
feature: Added email template in OrganizationCommonSettings, Added pu…
irfan-ikhwaFeb 20, 2024
d5c63ea
feature: Added emailTemplate in common-settings.
irfan-ikhwaFeb 22, 2024
0a213f4
Merge branch 'dev' into lost-password-feature
aq-ikhwa-techFeb 25, 2024
65d9e2a
Misc fixes
aq-ikhwa-techFeb 25, 2024
39ad3ac
Misc fixes
aq-ikhwa-techFeb 26, 2024
72518dc
Update application.props
aq-ikhwa-techFeb 26, 2024
d384418
Update application.props to include smtp server auth
aq-ikhwa-techFeb 26, 2024
810d669
Merge branch 'lowcoder-org:main' into lost-password-feature
aq-ikhwa-techFeb 26, 2024
758f86f
Merge branch 'dev' into lost-password-feature
aq-ikhwa-techFeb 26, 2024
0391d2a
feature: Added Env Variables for SMPT server
irfan-ikhwaFeb 27, 2024
bc74e8a
feature: Rename SMTP Env variable to ADMIN
irfan-ikhwaFeb 27, 2024
30dcd9c
Merge branch 'dev' into lost-password-feature
FalkWolskyMar 5, 2024
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
4 changes: 4 additions & 0 deletionsserver/api-service/lowcoder-domain/pom.xml
View file
Open in desktop
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -151,6 +158,9 @@ public Mono<Organization> create(Organization organization, String creatorId) {
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 ofError(BizError.AUTH_ERROR, "SENDING_EMAIL_FAILED");
}
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 numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,8 @@ public class CommonConfig {
private JsExecutor jsExecutor = new JsExecutor();
private Set<String> disallowedHosts = new HashSet<>();
private Marketplace marketplace = new Marketplace();
private String lowcoderPublicUrl;
private String lostPasswordEmailSender;

public boolean isSelfHost() {
return !isCloud();
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -113,6 +113,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@@ -139,6 +141,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER");
}
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 numberDiff line numberDiff 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 DownExpand 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
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,20 @@ 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
starttls:
enable: true
required: true
transport:
protocol: smtp

server:
compression:
Expand DownExpand Up@@ -46,6 +60,8 @@ common:
host: http://127.0.0.1:6060
marketplace:
private-mode: false
lowcoder-public-url: http://localhost:8080
lost-password-email-sender: info@lowcoder.org

material:
mongodb-grid-fs:
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,11 @@ spring:
max-in-memory-size: 20MB
webflux:
context-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}

server:
compression:
Expand DownExpand Up@@ -55,6 +60,8 @@ common:
mode: ${LOWCODER_WORKSPACE_MODE:SAAS}
marketplace:
private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true}
lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:8080}
lost-password-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:info@lowcoder.org}

material:
mongodb-grid-fs:
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp