diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json index 5e1d7e49ff..57935394c3 100644 --- a/api-docs/openapi/v3_0/aggregated.json +++ b/api-docs/openapi/v3_0/aggregated.json @@ -19969,13 +19969,17 @@ }, "VerifyCodeRequest": { "required": [ - "code" + "code", + "password" ], "type": "object", "properties": { "code": { "minLength": 1, "type": "string" + }, + "password": { + "type": "string" } } }, diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index 0f7c71baec..9748840614 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -258,26 +258,38 @@ private Mono verifyEmail(ServerRequest request) { .switchIfEmpty(Mono.error( () -> new ServerWebInputException("Request body is required.")) ) - .flatMap(verifyEmailRequest -> ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .map(Principal::getName) - .map(username -> Tuples.of(username, verifyEmailRequest.code())) - ) - .flatMap(tuple2 -> { - var username = tuple2.getT1(); - var code = tuple2.getT2(); - return Mono.just(username) - .transformDeferred(verificationEmailRateLimiter(username)) - .flatMap(name -> emailVerificationService.verify(username, code)) - .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new); - }) + .flatMap(this::doVerifyCode) .then(ServerResponse.ok().build()); } + private Mono doVerifyCode(VerifyCodeRequest verifyCodeRequest) { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Principal::getName) + .flatMap(username -> verifyPasswordAndCode(username, verifyCodeRequest)); + } + + private Mono verifyPasswordAndCode(String username, VerifyCodeRequest verifyCodeRequest) { + return userService.confirmPassword(username, verifyCodeRequest.password()) + .filter(Boolean::booleanValue) + .switchIfEmpty(Mono.error(new UnsatisfiedAttributeValueException( + "Password is incorrect.", "problemDetail.user.password.notMatch", null))) + .flatMap(verified -> verifyEmailCode(username, verifyCodeRequest.code())); + } + + private Mono verifyEmailCode(String username, String code) { + return Mono.just(username) + .transformDeferred(verificationEmailRateLimiter(username)) + .flatMap(name -> emailVerificationService.verify(username, code)) + .onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new); + } + public record EmailVerifyRequest(@Schema(requiredMode = REQUIRED) String email) { } - public record VerifyCodeRequest(@Schema(requiredMode = REQUIRED, minLength = 1) String code) { + public record VerifyCodeRequest( + @Schema(requiredMode = REQUIRED) String password, + @Schema(requiredMode = REQUIRED, minLength = 1) String code) { } private Mono sendEmailVerificationCode(ServerRequest request) { diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index ef743089a5..1a8ee7a500 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -52,6 +52,7 @@ problemDetail.user.email.verify.maxAttempts=Too many verification attempts, plea problemDetail.user.password.unsatisfied=The password does not meet the specifications. problemDetail.user.username.unsatisfied=The username does not meet the specifications. problemDetail.user.oldPassword.notMatch=The old password does not match. +problemDetail.user.password.notMatch=The password does not match. problemDetail.user.signUpFailed.disallowed=System does not allow new users to register. problemDetail.user.duplicateName=The username {0} already exists, please rename it and retry. problemDetail.comment.turnedOff=The comment function has been turned off. diff --git a/application/src/main/resources/config/i18n/messages_zh.properties b/application/src/main/resources/config/i18n/messages_zh.properties index 46d7ab069a..a77164a5d1 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -29,6 +29,7 @@ problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试 problemDetail.user.password.unsatisfied=密码不符合规范。 problemDetail.user.username.unsatisfied=用户名不符合规范。 problemDetail.user.oldPassword.notMatch=旧密码不匹配。 +problemDetail.user.password.notMatch=密码不匹配。 problemDetail.user.signUpFailed.disallowed=系统不允许注册新用户。 problemDetail.user.duplicateName=用户名 {0} 已存在,请更换用户名后重试。 problemDetail.plugin.version.unsatisfied.requires=插件要求一个最小的系统版本为 {0}, 但当前版本为 {1}。 diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/EmailVerificationCodeTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/EmailVerificationCodeTest.java index 55bd0030d5..ee3838c103 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/EmailVerificationCodeTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/EmailVerificationCodeTest.java @@ -24,6 +24,7 @@ import reactor.core.publisher.Mono; import run.halo.app.core.extension.User; import run.halo.app.core.extension.service.EmailVerificationService; +import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; @@ -43,6 +44,10 @@ class EmailVerificationCodeTest { ReactiveExtensionClient client; @Mock EmailVerificationService emailVerificationService; + + @Mock + UserService userService; + @InjectMocks UserEndpoint endpoint; @@ -97,9 +102,11 @@ void sendEmailVerificationCode() { void verifyEmail() { when(emailVerificationService.verify(anyString(), anyString())) .thenReturn(Mono.empty()); + when(userService.confirmPassword(anyString(), anyString())) + .thenReturn(Mono.just(true)); webClient.post() .uri("/users/-/verify-email") - .bodyValue(Map.of("code", "fake-code-1")) + .bodyValue(Map.of("code", "fake-code-1", "password", "123456")) .exchange() .expectStatus() .isOk(); @@ -107,7 +114,7 @@ void verifyEmail() { // request again to trigger rate limit webClient.post() .uri("/users/-/verify-email") - .bodyValue(Map.of("code", "fake-code-2")) + .bodyValue(Map.of("code", "fake-code-2", "password", "123456")) .exchange() .expectStatus() .isEqualTo(HttpStatus.TOO_MANY_REQUESTS); diff --git a/ui/packages/api-client/src/.openapi-generator/FILES b/ui/packages/api-client/src/.openapi-generator/FILES index 1219e3ee4b..bb8c1cdc40 100644 --- a/ui/packages/api-client/src/.openapi-generator/FILES +++ b/ui/packages/api-client/src/.openapi-generator/FILES @@ -240,6 +240,7 @@ models/register-verify-email-request.ts models/reply-list.ts models/reply-request.ts models/reply-spec.ts +models/reply-status.ts models/reply-vo-list.ts models/reply-vo.ts models/reply.ts diff --git a/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-tag-api.ts b/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-tag-api.ts index 607d91e2fd..1421d10727 100644 --- a/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-tag-api.ts +++ b/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-tag-api.ts @@ -31,16 +31,16 @@ export const ApiConsoleHaloRunV1alpha1TagApiAxiosParamCreator = function (config return { /** * List Post Tags. - * @param {Array} [fieldSelector] Field selector for filtering. - * @param {string} [keyword] Keyword for searching. - * @param {Array} [labelSelector] Label selector for filtering. - * @param {number} [page] The page number. Zero indicates no page. - * @param {number} [size] Size of one page. Zero indicates no limit. - * @param {Array} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp, name + * @param {number} [page] Page number. Default is 0. + * @param {number} [size] Size number. Default is 0. + * @param {Array} [labelSelector] Label selector. e.g.: hidden!=true + * @param {Array} [fieldSelector] Field selector. e.g.: metadata.name==halo + * @param {Array} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. + * @param {string} [keyword] Post tags filtered by keyword. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listPostTags: async (fieldSelector?: Array, keyword?: string, labelSelector?: Array, page?: number, size?: number, sort?: Array, options: RawAxiosRequestConfig = {}): Promise => { + listPostTags: async (page?: number, size?: number, labelSelector?: Array, fieldSelector?: Array, sort?: Array, keyword?: string, options: RawAxiosRequestConfig = {}): Promise => { const localVarPath = `/apis/api.console.halo.run/v1alpha1/tags`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -61,28 +61,28 @@ export const ApiConsoleHaloRunV1alpha1TagApiAxiosParamCreator = function (config // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (fieldSelector) { - localVarQueryParameter['fieldSelector'] = fieldSelector; + if (page !== undefined) { + localVarQueryParameter['page'] = page; } - if (keyword !== undefined) { - localVarQueryParameter['keyword'] = keyword; + if (size !== undefined) { + localVarQueryParameter['size'] = size; } if (labelSelector) { localVarQueryParameter['labelSelector'] = labelSelector; } - if (page !== undefined) { - localVarQueryParameter['page'] = page; + if (fieldSelector) { + localVarQueryParameter['fieldSelector'] = fieldSelector; } - if (size !== undefined) { - localVarQueryParameter['size'] = size; + if (sort) { + localVarQueryParameter['sort'] = sort; } - if (sort) { - localVarQueryParameter['sort'] = Array.from(sort); + if (keyword !== undefined) { + localVarQueryParameter['keyword'] = keyword; } @@ -108,17 +108,17 @@ export const ApiConsoleHaloRunV1alpha1TagApiFp = function(configuration?: Config return { /** * List Post Tags. - * @param {Array} [fieldSelector] Field selector for filtering. - * @param {string} [keyword] Keyword for searching. - * @param {Array} [labelSelector] Label selector for filtering. - * @param {number} [page] The page number. Zero indicates no page. - * @param {number} [size] Size of one page. Zero indicates no limit. - * @param {Array} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp, name + * @param {number} [page] Page number. Default is 0. + * @param {number} [size] Size number. Default is 0. + * @param {Array} [labelSelector] Label selector. e.g.: hidden!=true + * @param {Array} [fieldSelector] Field selector. e.g.: metadata.name==halo + * @param {Array} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. + * @param {string} [keyword] Post tags filtered by keyword. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listPostTags(fieldSelector?: Array, keyword?: string, labelSelector?: Array, page?: number, size?: number, sort?: Array, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.listPostTags(fieldSelector, keyword, labelSelector, page, size, sort, options); + async listPostTags(page?: number, size?: number, labelSelector?: Array, fieldSelector?: Array, sort?: Array, keyword?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listPostTags(page, size, labelSelector, fieldSelector, sort, keyword, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1TagApi.listPostTags']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); @@ -140,7 +140,7 @@ export const ApiConsoleHaloRunV1alpha1TagApiFactory = function (configuration?: * @throws {RequiredError} */ listPostTags(requestParameters: ApiConsoleHaloRunV1alpha1TagApiListPostTagsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise { - return localVarFp.listPostTags(requestParameters.fieldSelector, requestParameters.keyword, requestParameters.labelSelector, requestParameters.page, requestParameters.size, requestParameters.sort, options).then((request) => request(axios, basePath)); + return localVarFp.listPostTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, options).then((request) => request(axios, basePath)); }, }; }; @@ -152,46 +152,46 @@ export const ApiConsoleHaloRunV1alpha1TagApiFactory = function (configuration?: */ export interface ApiConsoleHaloRunV1alpha1TagApiListPostTagsRequest { /** - * Field selector for filtering. - * @type {Array} + * Page number. Default is 0. + * @type {number} * @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags */ - readonly fieldSelector?: Array + readonly page?: number /** - * Keyword for searching. - * @type {string} + * Size number. Default is 0. + * @type {number} * @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags */ - readonly keyword?: string + readonly size?: number /** - * Label selector for filtering. + * Label selector. e.g.: hidden!=true * @type {Array} * @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags */ readonly labelSelector?: Array /** - * The page number. Zero indicates no page. - * @type {number} + * Field selector. e.g.: metadata.name==halo + * @type {Array} * @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags */ - readonly page?: number + readonly fieldSelector?: Array /** - * Size of one page. Zero indicates no limit. - * @type {number} + * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. + * @type {Array} * @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags */ - readonly size?: number + readonly sort?: Array /** - * Sort property and direction of the list result. Supported fields: creationTimestamp, name - * @type {Array} + * Post tags filtered by keyword. + * @type {string} * @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags */ - readonly sort?: Array + readonly keyword?: string } /** @@ -209,7 +209,7 @@ export class ApiConsoleHaloRunV1alpha1TagApi extends BaseAPI { * @memberof ApiConsoleHaloRunV1alpha1TagApi */ public listPostTags(requestParameters: ApiConsoleHaloRunV1alpha1TagApiListPostTagsRequest = {}, options?: RawAxiosRequestConfig) { - return ApiConsoleHaloRunV1alpha1TagApiFp(this.configuration).listPostTags(requestParameters.fieldSelector, requestParameters.keyword, requestParameters.labelSelector, requestParameters.page, requestParameters.size, requestParameters.sort, options).then((request) => request(this.axios, this.basePath)); + return ApiConsoleHaloRunV1alpha1TagApiFp(this.configuration).listPostTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/ui/packages/api-client/src/models/comment-status.ts b/ui/packages/api-client/src/models/comment-status.ts index 1f565a51be..86df5b65a5 100644 --- a/ui/packages/api-client/src/models/comment-status.ts +++ b/ui/packages/api-client/src/models/comment-status.ts @@ -32,6 +32,12 @@ export interface CommentStatus { * @memberof CommentStatus */ 'lastReplyTime'?: string; + /** + * + * @type {number} + * @memberof CommentStatus + */ + 'observedVersion'?: number; /** * * @type {number} diff --git a/ui/packages/api-client/src/models/index.ts b/ui/packages/api-client/src/models/index.ts index b0acf1a96b..fb7893f6f1 100644 --- a/ui/packages/api-client/src/models/index.ts +++ b/ui/packages/api-client/src/models/index.ts @@ -158,6 +158,7 @@ export * from './reply'; export * from './reply-list'; export * from './reply-request'; export * from './reply-spec'; +export * from './reply-status'; export * from './reply-vo'; export * from './reply-vo-list'; export * from './reset-password-request'; diff --git a/ui/packages/api-client/src/models/plugin-status.ts b/ui/packages/api-client/src/models/plugin-status.ts index 59dd538c90..13ae2787ff 100644 --- a/ui/packages/api-client/src/models/plugin-status.ts +++ b/ui/packages/api-client/src/models/plugin-status.ts @@ -79,8 +79,7 @@ export const PluginStatusLastProbeStateEnum = { Resolved: 'RESOLVED', Started: 'STARTED', Stopped: 'STOPPED', - Failed: 'FAILED', - Unloaded: 'UNLOADED' + Failed: 'FAILED' } as const; export type PluginStatusLastProbeStateEnum = typeof PluginStatusLastProbeStateEnum[keyof typeof PluginStatusLastProbeStateEnum]; diff --git a/ui/packages/api-client/src/models/reply-status.ts b/ui/packages/api-client/src/models/reply-status.ts new file mode 100644 index 0000000000..d50500be32 --- /dev/null +++ b/ui/packages/api-client/src/models/reply-status.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Halo Next API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 2.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface ReplyStatus + */ +export interface ReplyStatus { + /** + * + * @type {number} + * @memberof ReplyStatus + */ + 'observedVersion'?: number; +} + diff --git a/ui/packages/api-client/src/models/reply.ts b/ui/packages/api-client/src/models/reply.ts index 8ce8856e1d..6ec10ecd86 100644 --- a/ui/packages/api-client/src/models/reply.ts +++ b/ui/packages/api-client/src/models/reply.ts @@ -19,6 +19,9 @@ import { Metadata } from './metadata'; // May contain unused imports in some cases // @ts-ignore import { ReplySpec } from './reply-spec'; +// May contain unused imports in some cases +// @ts-ignore +import { ReplyStatus } from './reply-status'; /** * @@ -50,5 +53,11 @@ export interface Reply { * @memberof Reply */ 'spec': ReplySpec; + /** + * + * @type {ReplyStatus} + * @memberof Reply + */ + 'status': ReplyStatus; } diff --git a/ui/packages/api-client/src/models/verify-code-request.ts b/ui/packages/api-client/src/models/verify-code-request.ts index 4939463e5a..5e8c6ae42e 100644 --- a/ui/packages/api-client/src/models/verify-code-request.ts +++ b/ui/packages/api-client/src/models/verify-code-request.ts @@ -26,5 +26,11 @@ export interface VerifyCodeRequest { * @memberof VerifyCodeRequest */ 'code': string; + /** + * + * @type {string} + * @memberof VerifyCodeRequest + */ + 'password': string; } diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml index 5a3378babc..9797ae2c56 100644 --- a/ui/src/locales/en.yaml +++ b/ui/src/locales/en.yaml @@ -1161,6 +1161,9 @@ core: label: Email address new_email: label: New email address + password: + label: Password + help: The login password for the current account operations: send_code: buttons: diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml index 2e0fbc727e..eb596887a4 100644 --- a/ui/src/locales/zh-CN.yaml +++ b/ui/src/locales/zh-CN.yaml @@ -1106,6 +1106,9 @@ core: label: 电子邮箱 code: label: 验证码 + password: + label: 验证密码 + help: 当前账号的登录密码 operations: send_code: buttons: diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml index 910b144d23..36264c3ae4 100644 --- a/ui/src/locales/zh-TW.yaml +++ b/ui/src/locales/zh-TW.yaml @@ -1083,6 +1083,9 @@ core: label: 電子郵件信箱 new_email: label: 新電子郵件信箱 + password: + label: 驗證密碼 + help: 目前帳號的登入密碼 operations: send_code: buttons: diff --git a/ui/uc-src/modules/profile/components/EmailVerifyModal.vue b/ui/uc-src/modules/profile/components/EmailVerifyModal.vue index 2d283e26c7..818ce8ddc8 100644 --- a/ui/uc-src/modules/profile/components/EmailVerifyModal.vue +++ b/ui/uc-src/modules/profile/components/EmailVerifyModal.vue @@ -1,13 +1,11 @@ @@ -147,6 +146,13 @@ function handleVerify(data: { code: string }) { +