Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby committed Feb 22, 2024
2 parents b2814c8 + 50fbe37 commit 68aeb2f
Show file tree
Hide file tree
Showing 25 changed files with 943 additions and 657 deletions.
1 change: 1 addition & 0 deletions api/src/main/java/run/halo/app/infra/SystemSetting.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public static class Basic {
public static class User {
public static final String GROUP = "user";
Boolean allowRegistration;
Boolean mustVerifyEmailOnRegistration;
String defaultRole;
String avatarPolicy;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,11 @@ public GlobalInfo globalInfo() {
handleBasicSetting(info, configMap);
handlePostSlugGenerationStrategy(info, configMap);
}));

return info;
}

@Data
public static class GlobalInfo {

private URL externalUrl;

private boolean useAbsolutePermalink;
Expand All @@ -85,6 +83,8 @@ public static class GlobalInfo {
private String postSlugGenerationStrategy;

private List<SocialAuthProvider> socialAuthProviders;

private Boolean mustVerifyEmailOnRegistration;
}

@Data
Expand Down Expand Up @@ -117,12 +117,14 @@ private void handleCommentSetting(GlobalInfo info, ConfigMap configMap) {
}

private void handleUserSetting(GlobalInfo info, ConfigMap configMap) {
var user = SystemSetting.get(configMap, User.GROUP, User.class);
if (user == null) {
var userSetting = SystemSetting.get(configMap, User.GROUP, User.class);
if (userSetting == null) {
info.setAllowRegistration(false);
info.setMustVerifyEmailOnRegistration(false);
} else {
info.setAllowRegistration(
user.getAllowRegistration() != null && user.getAllowRegistration());
userSetting.getAllowRegistration() != null && userSetting.getAllowRegistration());
info.setMustVerifyEmailOnRegistration(userSetting.getMustVerifyEmailOnRegistration());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,21 @@ public interface EmailVerificationService {
* @throws EmailVerificationFailed if send failed
*/
Mono<Void> verify(String username, String code);

/**
* Send verification code.
* The only difference is use email as username.
*
* @param email email to send must not be blank
*/
Mono<Void> sendRegisterVerificationCode(String email);

/**
* Verify email by given code.
*
* @param email email as username to verify email must not be blank
* @param code code to verify email must not be blank
* @throws EmailVerificationFailed if send failed
*/
Mono<Boolean> verifyRegisterVerificationCode(String email, String code);
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ public Mono<Void> verify(String username, String code) {
.then();
}

@Override
public Mono<Void> sendRegisterVerificationCode(String email) {
Assert.state(StringUtils.isNotBlank(email), "Email must not be blank");
return sendVerificationNotification(email, email);
}

@Override
public Mono<Boolean> verifyRegisterVerificationCode(String email, String code) {
Assert.state(StringUtils.isNotBlank(email), "Username must not be blank");
Assert.state(StringUtils.isNotBlank(code), "Code must not be blank");
return Mono.just(emailVerificationManager.verifyCode(email, email, code));
}

Mono<Void> sendVerificationNotification(String username, String email) {
var code = emailVerificationManager.generateCode(username, email);
var subscribeNotification = autoSubscribeVerificationEmailNotification(email);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package run.halo.app.theme.endpoint;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
Expand All @@ -11,6 +12,7 @@
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.http.HttpStatus;
Expand All @@ -29,8 +31,14 @@
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.service.EmailPasswordRecoveryService;
import run.halo.app.core.extension.service.EmailVerificationService;
import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.GroupVersion;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.ValidationUtils;
import run.halo.app.infra.exception.AccessDeniedException;
import run.halo.app.infra.exception.EmailVerificationFailed;
import run.halo.app.infra.exception.RateLimitExceededException;
import run.halo.app.infra.utils.IpAddressUtils;

Expand All @@ -48,6 +56,8 @@ public class PublicUserEndpoint implements CustomEndpoint {
private final ReactiveUserDetailsService reactiveUserDetailsService;
private final EmailPasswordRecoveryService emailPasswordRecoveryService;
private final RateLimiterRegistry rateLimiterRegistry;
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
private final EmailVerificationService emailVerificationService;

@Override
public RouterFunction<ServerResponse> endpoint() {
Expand All @@ -62,6 +72,22 @@ public RouterFunction<ServerResponse> endpoint() {
)
.response(responseBuilder().implementation(User.class))
)
.POST("/users/-/send-register-verify-email", this::sendRegisterVerifyEmail,
builder -> builder.operationId("SendRegisterVerifyEmail")
.description(
"Send registration verification email, which can be called when "
+ "mustVerifyEmailOnRegistration in user settings is true"
)
.tag(tag)
.requestBody(requestBodyBuilder()
.required(true)
.implementation(RegisterVerifyEmailRequest.class)
)
.response(responseBuilder()
.responseCode(HttpStatus.NO_CONTENT.toString())
.implementation(Void.class)
)
)
.POST("/users/-/send-password-reset-email", this::sendPasswordResetEmail,
builder -> builder.operationId("SendPasswordResetEmail")
.description("Send password reset email when forgot password")
Expand Down Expand Up @@ -126,6 +152,9 @@ record ResetPasswordRequest(@Schema(requiredMode = REQUIRED, minLength = 6) Stri
@Schema(requiredMode = REQUIRED) String token) {
}

record RegisterVerifyEmailRequest(@Schema(requiredMode = REQUIRED) String email) {
}

private Mono<ServerResponse> sendPasswordResetEmail(ServerRequest request) {
return request.bodyToMono(PasswordResetEmailRequest.class)
.flatMap(passwordResetRequest -> {
Expand Down Expand Up @@ -154,6 +183,30 @@ public GroupVersion groupVersion() {

private Mono<ServerResponse> signUp(ServerRequest request) {
return request.bodyToMono(SignUpRequest.class)
.doOnNext(signUpRequest -> signUpRequest.user().getSpec().setEmailVerified(false))
.flatMap(signUpRequest -> environmentFetcher.fetch(SystemSetting.User.GROUP,
SystemSetting.User.class)
.map(user -> BooleanUtils.isTrue(user.getMustVerifyEmailOnRegistration()))
.defaultIfEmpty(false)
.flatMap(mustVerifyEmailOnRegistration -> {
if (!mustVerifyEmailOnRegistration) {
return Mono.just(signUpRequest);
}
if (!StringUtils.isNumeric(signUpRequest.verifyCode)) {
return Mono.error(new EmailVerificationFailed());
}
return emailVerificationService.verifyRegisterVerificationCode(
signUpRequest.user().getSpec().getEmail(),
signUpRequest.verifyCode)
.flatMap(verified -> {
if (BooleanUtils.isNotTrue(verified)) {
return Mono.error(new EmailVerificationFailed());
}
signUpRequest.user().getSpec().setEmailVerified(true);
return Mono.just(signUpRequest);
});
})
)
.flatMap(signUpRequest ->
userService.signUp(signUpRequest.user(), signUpRequest.password())
)
Expand All @@ -168,6 +221,35 @@ private Mono<ServerResponse> signUp(ServerRequest request) {
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);
}

private Mono<ServerResponse> sendRegisterVerifyEmail(ServerRequest request) {
return request.bodyToMono(RegisterVerifyEmailRequest.class)
.switchIfEmpty(Mono.error(
() -> new ServerWebInputException("Required request body is missing."))
)
.map(emailReq -> {
var email = emailReq.email();
if (!ValidationUtils.isValidEmail(email)) {
throw new ServerWebInputException("Invalid email address.");
}
return email;
})
.flatMap(email -> environmentFetcher.fetch(SystemSetting.User.GROUP,
SystemSetting.User.class)
.map(config -> BooleanUtils.isTrue(config.getMustVerifyEmailOnRegistration()))
.defaultIfEmpty(false)
.doOnNext(mustVerifyEmailOnRegistration -> {
if (!mustVerifyEmailOnRegistration) {
throw new AccessDeniedException("Email verification is not required.");
}
})
.transformDeferred(sendRegisterEmailVerificationCodeRateLimiter(email))
.flatMap(s -> emailVerificationService.sendRegisterVerificationCode(email)
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new))
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new)
)
.then(ServerResponse.ok().build());
}

private <T> RateLimiterOperator<T> getRateLimiterForSignUp(ServerWebExchange exchange) {
var clientIp = IpAddressUtils.getClientIp(exchange.getRequest());
var rateLimiter = rateLimiterRegistry.rateLimiter("signup-from-ip-" + clientIp,
Expand All @@ -187,7 +269,17 @@ private Mono<Void> authenticate(String username, ServerWebExchange exchange) {
});
}

private <T> RateLimiterOperator<T> sendRegisterEmailVerificationCodeRateLimiter(String email) {
String rateLimiterKey = "send-register-verify-email:" + email;
var rateLimiter =
rateLimiterRegistry.rateLimiter(rateLimiterKey, "send-email-verification-code");
return RateLimiterOperator.of(rateLimiter);
}

record SignUpRequest(@Schema(requiredMode = REQUIRED) User user,
@Schema(requiredMode = REQUIRED, minLength = 6) String password) {
@Schema(requiredMode = REQUIRED, minLength = 6) String password,
@Schema(requiredMode = NOT_REQUIRED, minLength = 6, maxLength = 6)
String verifyCode
) {
}
}
4 changes: 4 additions & 0 deletions application/src/main/resources/extensions/system-setting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ spec:
name: allowRegistration
label: "开放注册"
value: false
- $formkit: checkbox
name: mustVerifyEmailOnRegistration
label: "注册需验证邮箱(请确保启用邮件通知)"
value: false
- $formkit: roleSelect
name: defaultRole
label: "默认角色"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand All @@ -21,6 +22,8 @@
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.Metadata;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;

/**
* Tests for {@link PublicUserEndpoint}.
Expand All @@ -36,7 +39,8 @@ class PublicUserEndpointTest {
private ServerSecurityContextRepository securityContextRepository;
@Mock
private ReactiveUserDetailsService reactiveUserDetailsService;

@Mock
SystemConfigurableEnvironmentFetcher environmentFetcher;
@Mock
RateLimiterRegistry rateLimiterRegistry;

Expand Down Expand Up @@ -67,14 +71,17 @@ void signUp() {
.password("123456")
.authorities("test-role")
.build()));
SystemSetting.User userSetting = mock(SystemSetting.User.class);
when(environmentFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class))
.thenReturn(Mono.just(userSetting));

when(rateLimiterRegistry.rateLimiter("signup-from-ip-127.0.0.1", "signup"))
.thenReturn(RateLimiter.ofDefaults("signup"));

webClient.post()
.uri("/users/-/signup")
.header("X-Forwarded-For", "127.0.0.1")
.bodyValue(new PublicUserEndpoint.SignUpRequest(user, "fake-password"))
.bodyValue(new PublicUserEndpoint.SignUpRequest(user, "fake-password", ""))
.exchange()
.expectStatus().isOk();

Expand Down
26 changes: 13 additions & 13 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,18 @@
"qs": "^6.11.1",
"short-unique-id": "^5.0.2",
"transliteration": "^2.3.5",
"vue": "^3.3.4",
"vue-demi": "^0.14.5",
"vue": "^3.4.19",
"vue-demi": "^0.14.7",
"vue-grid-layout": "3.0.0-beta1",
"vue-i18n": "9.3.0-beta.25",
"vue-router": "^4.2.4",
"vue-i18n": "^9.9.1",
"vue-router": "^4.2.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@changesets/cli": "^2.25.2",
"@iconify-json/mdi": "^1.1.50",
"@iconify-json/vscode-icons": "^1.1.22",
"@intlify/unplugin-vue-i18n": "^0.12.2",
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@rushstack/eslint-patch": "^1.3.2",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/container-queries": "^0.1.0",
Expand All @@ -115,14 +115,14 @@
"@types/node": "^18.11.19",
"@types/qs": "^6.9.7",
"@types/randomstring": "^1.1.8",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vitest/ui": "^0.34.1",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.4.19",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"@vue/test-utils": "^2.4.4",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.14",
"c8": "^7.12.0",
"cypress": "^10.11.0",
Expand All @@ -144,15 +144,15 @@
"tailwindcss": "^3.2.7",
"tailwindcss-safe-area": "^0.2.2",
"tailwindcss-themer": "^2.0.3",
"typescript": "~5.0.4",
"typescript": "~5.3.0",
"unplugin-icons": "^0.14.15",
"vite": "^4.2.3",
"vite-plugin-externals": "^0.6.2",
"vite-plugin-html": "^3.2.0",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-static-copy": "^0.17.0",
"vite-plugin-vue-devtools": "^7.0.13",
"vite-plugin-vue-devtools": "^7.0.15",
"vitest": "^0.34.1",
"vue-tsc": "^1.8.8"
"vue-tsc": "^1.8.27"
}
}
Loading

0 comments on commit 68aeb2f

Please sign in to comment.