diff --git a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java index e76396b76..b835bf49d 100644 --- a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java +++ b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java @@ -83,6 +83,8 @@ public class GlobalInfoEndpoint { private String website; private String authenticationUrl; + + private String bindingUrl; } private void handleCommentSetting(GlobalInfo info, ConfigMap configMap) { @@ -127,6 +129,7 @@ public class GlobalInfoEndpoint { socialAuthProvider.setLogo(provider.getLogo()); socialAuthProvider.setWebsite(provider.getWebsite()); socialAuthProvider.setAuthenticationUrl(provider.getAuthenticationUrl()); + socialAuthProvider.setBindingUrl(provider.getBindingUrl()); return socialAuthProvider; }) .toList() diff --git a/application/src/main/java/run/halo/app/core/extension/service/UserService.java b/application/src/main/java/run/halo/app/core/extension/service/UserService.java index e958c1734..dfed6837b 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/UserService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/UserService.java @@ -19,4 +19,6 @@ public interface UserService { Flux listRoles(String username); Mono grantRoles(String username, Set roles); + + Mono signUp(User user, String password); } diff --git a/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java index 6589808b0..fce44104c 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/extension/service/UserServiceImpl.java @@ -5,9 +5,12 @@ import static run.halo.app.core.extension.RoleBinding.containsUser; import java.util.HashSet; import java.util.Objects; import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.BooleanUtils; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; @@ -17,9 +20,14 @@ import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.User; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.exception.ExtensionNotFoundException; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; +import run.halo.app.infra.exception.AccessDeniedException; +import run.halo.app.infra.exception.DuplicateNameException; import run.halo.app.infra.exception.UserNotFoundException; @Service +@RequiredArgsConstructor public class UserServiceImpl implements UserService { public static final String GHOST_USER_NAME = "ghost"; @@ -27,10 +35,8 @@ public class UserServiceImpl implements UserService { private final PasswordEncoder passwordEncoder; - public UserServiceImpl(ReactiveExtensionClient client, PasswordEncoder passwordEncoder) { - this.client = client; - this.passwordEncoder = passwordEncoder; - } + private final SystemConfigurableEnvironmentFetcher environmentFetcher; + @Override public Mono getUser(String username) { @@ -115,4 +121,49 @@ public class UserServiceImpl implements UserService { }); } + @Override + @Transactional + public Mono signUp(User user, String password) { + if (!StringUtils.hasText(password)) { + throw new IllegalArgumentException("Password must not be blank"); + } + return environmentFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class) + .switchIfEmpty(Mono.error(new IllegalStateException("User setting is not configured"))) + .flatMap(userSetting -> { + Boolean allowRegistration = userSetting.getAllowRegistration(); + if (BooleanUtils.isFalse(allowRegistration)) { + return Mono.error(new AccessDeniedException("Registration is not allowed", + "problemDetail.user.signUpFailed.disallowed", + null)); + } + String defaultRole = userSetting.getDefaultRole(); + if (!StringUtils.hasText(defaultRole)) { + return Mono.error(new AccessDeniedException( + "Default registration role is not configured by admin", + "problemDetail.user.signUpFailed.disallowed", + null)); + } + String encodedPassword = passwordEncoder.encode(password); + user.getSpec().setPassword(encodedPassword); + return createNewUser(user, defaultRole); + }); + } + + private Mono createNewUser(User user, String defaultRole) { + Assert.notNull(user, "User must not be null"); + return client.fetch(User.class, user.getMetadata().getName()) + .hasElement() + .flatMap(hasUser -> { + if (hasUser) { + return Mono.error( + new DuplicateNameException("User name is already in use", null, + "problemDetail.user.duplicateName", + new Object[] {user.getMetadata().getName()})); + } + return client.create(user) + .flatMap(newUser -> grantRoles(user.getMetadata().getName(), + Set.of(defaultRole)) + ); + }); + } } diff --git a/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java new file mode 100644 index 000000000..b46117aa6 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/endpoint/PublicUserEndpoint.java @@ -0,0 +1,89 @@ +package run.halo.app.theme.endpoint; + +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.requestbody.Builder.requestBodyBuilder; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.RequiredArgsConstructor; +import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.web.server.context.ServerSecurityContextRepository; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; +import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.core.extension.service.UserService; +import run.halo.app.extension.GroupVersion; + +/** + * User endpoint for unauthenticated user. + * + * @author guqing + * @since 2.4.0 + */ +@Component +@RequiredArgsConstructor +public class PublicUserEndpoint implements CustomEndpoint { + private final UserService userService; + private final ServerSecurityContextRepository securityContextRepository; + private final ReactiveUserDetailsService reactiveUserDetailsService; + + @Override + public RouterFunction endpoint() { + var tag = "api.halo.run/v1alpha1/User"; + return SpringdocRouteBuilder.route() + .POST("/users/-/signup", this::signUp, + builder -> builder.operationId("SignUp") + .description("Sign up a new user") + .tag(tag) + .requestBody(requestBodyBuilder().required(true) + .implementation(SignUpRequest.class) + ) + .response(responseBuilder().implementation(User.class)) + ) + .build(); + } + + @Override + public GroupVersion groupVersion() { + return GroupVersion.parseAPIVersion("api.halo.run/v1alpha1"); + } + + private Mono signUp(ServerRequest request) { + return request.bodyToMono(SignUpRequest.class) + .flatMap(signUpRequest -> + userService.signUp(signUpRequest.user(), signUpRequest.password()) + ) + .flatMap(user -> authenticate(user.getMetadata().getName(), request.exchange()) + .thenReturn(user) + ) + .flatMap(user -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(user) + ); + } + + private Mono authenticate(String username, ServerWebExchange exchange) { + return reactiveUserDetailsService.findByUsername(username) + .flatMap(userDetails -> { + SecurityContextImpl securityContext = new SecurityContextImpl(); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails.getUsername(), + userDetails.getPassword(), userDetails.getAuthorities()); + securityContext.setAuthentication(authentication); + return securityContextRepository.save(exchange, securityContext); + }); + } + + record SignUpRequest(@Schema(requiredMode = REQUIRED) User user, + @Schema(requiredMode = REQUIRED, minLength = 6) String password) { + } +} diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index 6ce6840b5..5571afb25 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -33,6 +33,8 @@ problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=File problemDetail.run.halo.app.infra.exception.DuplicateNameException=Duplicate name detected, please rename it and retry. problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin {0} already exists. +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. problemDetail.comment.systemUsersOnly=Allow only system users to comment problemDetail.theme.upgrade.missingManifest=Missing theme manifest file "theme.yaml" or "theme.yml". diff --git a/application/src/main/resources/config/i18n/messages_zh.properties b/application/src/main/resources/config/i18n/messages_zh.properties index 2028e9bd3..b98f51246 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -9,6 +9,9 @@ problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=文 problemDetail.run.halo.app.infra.exception.DuplicateNameException=检测到有重复的名称,请重命名后重试。 problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=插件 {0} 已经存。 +problemDetail.user.signUpFailed.disallowed=系统不允许注册新用户。 +problemDetail.user.duplicateName=用户名 {0} 已存在,请更换用户名后重试。 + problemDetail.plugin.version.unsatisfied.requires=插件要求一个最小的系统版本为 {0}, 但当前版本为 {1}。 problemDetail.theme.version.unsatisfied.requires=主题要求一个最小的系统版本为 {0}, 但当前版本为 {1}。 \ No newline at end of file diff --git a/application/src/main/resources/extensions/system-configurable-configmap.yaml b/application/src/main/resources/extensions/system-configurable-configmap.yaml index 0694ccc3d..2663d7353 100644 --- a/application/src/main/resources/extensions/system-configurable-configmap.yaml +++ b/application/src/main/resources/extensions/system-configurable-configmap.yaml @@ -3,6 +3,11 @@ kind: "ConfigMap" metadata: name: system-default data: + user: | + { + "allowRegistration": false, + "defaultRole": "" + } theme: | { "active": "theme-earth" diff --git a/application/src/main/resources/extensions/system-setting.yaml b/application/src/main/resources/extensions/system-setting.yaml index 2cf9640a6..d08436b4c 100644 --- a/application/src/main/resources/extensions/system-setting.yaml +++ b/application/src/main/resources/extensions/system-setting.yaml @@ -64,6 +64,16 @@ spec: - $formkit: textarea name: description label: "站点描述" + - group: user + label: 用户设置 + formSchema: + - $formkit: checkbox + name: allowRegistration + label: "开放注册" + value: false + - $formkit: text + name: defaultRole + label: "默认角色" - group: comment label: 评论设置 formSchema: diff --git a/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java b/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java index e143a6bbb..b34bceaf5 100644 --- a/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java +++ b/application/src/test/java/run/halo/app/core/extension/service/UserServiceImplTest.java @@ -1,12 +1,15 @@ package run.halo.app.core.extension.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -35,6 +38,10 @@ import run.halo.app.core.extension.User; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.exception.ExtensionNotFoundException; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; +import run.halo.app.infra.exception.AccessDeniedException; +import run.halo.app.infra.exception.DuplicateNameException; import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.infra.utils.JsonUtils; @@ -44,6 +51,9 @@ class UserServiceImplTest { @Mock ReactiveExtensionClient client; + @Mock + SystemConfigurableEnvironmentFetcher environmentFetcher; + @Mock PasswordEncoder passwordEncoder; @@ -378,4 +388,100 @@ class UserServiceImplTest { verify(client).update(notProvidedRoleBinding); } } + + + @Nested + class SignUpTest { + @Test + void signUpWhenRegistrationNotAllowed() { + SystemSetting.User userSetting = new SystemSetting.User(); + userSetting.setAllowRegistration(false); + when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP), + eq(SystemSetting.User.class))) + .thenReturn(Mono.just(userSetting)); + + User fakeUser = fakeSignUpUser("fake-user", "fake-password"); + + userService.signUp(fakeUser, "fake-password") + .as(StepVerifier::create) + .expectError(AccessDeniedException.class) + .verify(); + } + + @Test + void signUpWhenRegistrationDefaultRoleNotConfigured() { + SystemSetting.User userSetting = new SystemSetting.User(); + userSetting.setAllowRegistration(true); + when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP), + eq(SystemSetting.User.class))) + .thenReturn(Mono.just(userSetting)); + + User fakeUser = fakeSignUpUser("fake-user", "fake-password"); + + userService.signUp(fakeUser, "fake-password") + .as(StepVerifier::create) + .expectError(AccessDeniedException.class) + .verify(); + } + + @Test + void signUpWhenRegistrationUsernameExists() { + SystemSetting.User userSetting = new SystemSetting.User(); + userSetting.setAllowRegistration(true); + userSetting.setDefaultRole("fake-role"); + when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP), + eq(SystemSetting.User.class))) + .thenReturn(Mono.just(userSetting)); + when(passwordEncoder.encode(eq("fake-password"))).thenReturn("fake-password"); + when(client.fetch(eq(User.class), eq("fake-user"))) + .thenReturn(Mono.just(fakeSignUpUser("test", "test"))); + + User fakeUser = fakeSignUpUser("fake-user", "fake-password"); + + userService.signUp(fakeUser, "fake-password") + .as(StepVerifier::create) + .expectError(DuplicateNameException.class) + .verify(); + } + + @Test + void signUpWhenRegistrationSuccessfully() { + SystemSetting.User userSetting = new SystemSetting.User(); + userSetting.setAllowRegistration(true); + userSetting.setDefaultRole("fake-role"); + when(environmentFetcher.fetch(eq(SystemSetting.User.GROUP), + eq(SystemSetting.User.class))) + .thenReturn(Mono.just(userSetting)); + when(passwordEncoder.encode(eq("fake-password"))).thenReturn("fake-password"); + when(client.fetch(eq(User.class), eq("fake-user"))) + .thenReturn(Mono.empty()); + + User fakeUser = fakeSignUpUser("fake-user", "fake-password"); + + when(client.create(any(User.class))).thenReturn(Mono.just(fakeUser)); + UserServiceImpl spyUserService = spy(userService); + doReturn(Mono.just(fakeUser)).when(spyUserService).grantRoles(eq("fake-user"), + anySet()); + + spyUserService.signUp(fakeUser, "fake-password") + .as(StepVerifier::create) + .consumeNextWith(user -> { + assertThat(user.getMetadata().getName()).isEqualTo("fake-user"); + assertThat(user.getSpec().getPassword()).isEqualTo("fake-password"); + }) + .verifyComplete(); + + verify(client).create(any(User.class)); + verify(spyUserService).grantRoles(eq("fake-user"), anySet()); + } + + User fakeSignUpUser(String name, String password) { + User user = new User(); + user.setMetadata(new Metadata()); + user.getMetadata().setName(name); + user.setSpec(new User.UserSpec()); + user.getSpec().setPassword(password); + return user; + } + } } diff --git a/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java new file mode 100644 index 000000000..d6506cdcf --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/endpoint/PublicUserEndpointTest.java @@ -0,0 +1,76 @@ +package run.halo.app.theme.endpoint; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.web.server.context.ServerSecurityContextRepository; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; +import run.halo.app.core.extension.service.UserService; +import run.halo.app.extension.Metadata; + +/** + * Tests for {@link PublicUserEndpoint}. + * + * @author guqing + * @since 2.4.0 + */ +@ExtendWith(MockitoExtension.class) +class PublicUserEndpointTest { + @Mock + private UserService userService; + @Mock + private ServerSecurityContextRepository securityContextRepository; + @Mock + private ReactiveUserDetailsService reactiveUserDetailsService; + + @InjectMocks + private PublicUserEndpoint publicUserEndpoint; + + private WebTestClient webClient; + + @BeforeEach + void setUp() { + webClient = WebTestClient.bindToRouterFunction(publicUserEndpoint.endpoint()) + .build(); + } + + @Test + void signUp() { + User user = new User(); + user.setMetadata(new Metadata()); + user.getMetadata().setName("fake-user"); + user.setSpec(new User.UserSpec()); + user.getSpec().setDisplayName("hello"); + user.getSpec().setBio("bio"); + + when(userService.signUp(any(User.class), anyString())).thenReturn(Mono.just(user)); + when(securityContextRepository.save(any(), any())).thenReturn(Mono.empty()); + when(reactiveUserDetailsService.findByUsername(anyString())).thenReturn(Mono.just( + org.springframework.security.core.userdetails.User.withUsername("fake-user") + .password("123456") + .authorities("test-role") + .build())); + + webClient.post() + .uri("/users/-/signup") + .bodyValue(new PublicUserEndpoint.SignUpRequest(user, "fake-password")) + .exchange() + .expectStatus().isOk(); + + verify(userService).signUp(any(User.class), anyString()); + verify(securityContextRepository).save(any(), any()); + verify(reactiveUserDetailsService).findByUsername(eq("fake-user")); + } +} \ No newline at end of file diff --git a/console/packages/api-client/src/.openapi-generator/FILES b/console/packages/api-client/src/.openapi-generator/FILES index 4726ff93a..80d4bb9b7 100644 --- a/console/packages/api-client/src/.openapi-generator/FILES +++ b/console/packages/api-client/src/.openapi-generator/FILES @@ -15,6 +15,7 @@ api/api-console-halo-run-v1alpha1-user-api.ts api/api-halo-run-v1alpha1-comment-api.ts api/api-halo-run-v1alpha1-post-api.ts api/api-halo-run-v1alpha1-tracker-api.ts +api/api-halo-run-v1alpha1-user-api.ts api/auth-halo-run-v1alpha1-auth-provider-api.ts api/auth-halo-run-v1alpha1-user-connection-api.ts api/content-halo-run-v1alpha1-category-api.ts @@ -43,6 +44,7 @@ api/v1alpha1-menu-item-api.ts api/v1alpha1-personal-access-token-api.ts api/v1alpha1-role-api.ts api/v1alpha1-role-binding-api.ts +api/v1alpha1-secret-api.ts api/v1alpha1-setting-api.ts api/v1alpha1-user-api.ts base.ts @@ -169,11 +171,14 @@ models/role.ts models/search-engine-list.ts models/search-engine-spec.ts models/search-engine.ts +models/secret-list.ts +models/secret.ts models/setting-form.ts models/setting-list.ts models/setting-ref.ts models/setting-spec.ts models/setting.ts +models/sign-up-request.ts models/single-page-list.ts models/single-page-request.ts models/single-page-spec.ts diff --git a/console/packages/api-client/src/api.ts b/console/packages/api-client/src/api.ts index 119128b4e..3c8e00302 100644 --- a/console/packages/api-client/src/api.ts +++ b/console/packages/api-client/src/api.ts @@ -26,6 +26,7 @@ export * from "./api/api-console-halo-run-v1alpha1-user-api"; export * from "./api/api-halo-run-v1alpha1-comment-api"; export * from "./api/api-halo-run-v1alpha1-post-api"; export * from "./api/api-halo-run-v1alpha1-tracker-api"; +export * from "./api/api-halo-run-v1alpha1-user-api"; export * from "./api/auth-halo-run-v1alpha1-auth-provider-api"; export * from "./api/auth-halo-run-v1alpha1-user-connection-api"; export * from "./api/content-halo-run-v1alpha1-category-api"; diff --git a/console/packages/api-client/src/api/api-halo-run-v1alpha1-user-api.ts b/console/packages/api-client/src/api/api-halo-run-v1alpha1-user-api.ts new file mode 100644 index 000000000..0c4aed09a --- /dev/null +++ b/console/packages/api-client/src/api/api-halo-run-v1alpha1-user-api.ts @@ -0,0 +1,212 @@ +/* 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. + */ + +import type { Configuration } from "../configuration"; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios"; +import globalAxios from "axios"; +// Some imports not used depending on template conditions +// @ts-ignore +import { + DUMMY_BASE_URL, + assertParamExists, + setApiKeyToObject, + setBasicAuthToObject, + setBearerAuthToObject, + setOAuthToObject, + setSearchParams, + serializeDataIfNeeded, + toPathString, + createRequestFunction, +} from "../common"; +// @ts-ignore +import { + BASE_PATH, + COLLECTION_FORMATS, + RequestArgs, + BaseAPI, + RequiredError, +} from "../base"; +// @ts-ignore +import { SignUpRequest } from "../models"; +// @ts-ignore +import { User } from "../models"; +/** + * ApiHaloRunV1alpha1UserApi - axios parameter creator + * @export + */ +export const ApiHaloRunV1alpha1UserApiAxiosParamCreator = function ( + configuration?: Configuration +) { + return { + /** + * Sign up a new user + * @param {SignUpRequest} signUpRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signUp: async ( + signUpRequest: SignUpRequest, + options: AxiosRequestConfig = {} + ): Promise => { + // verify required parameter 'signUpRequest' is not null or undefined + assertParamExists("signUp", "signUpRequest", signUpRequest); + const localVarPath = `/apis/api.halo.run/v1alpha1/users/-/signup`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: "POST", + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication BasicAuth required + // http basic authentication required + setBasicAuthToObject(localVarRequestOptions, configuration); + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration); + + localVarHeaderParameter["Content-Type"] = "application/json"; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + localVarRequestOptions.data = serializeDataIfNeeded( + signUpRequest, + localVarRequestOptions, + configuration + ); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +/** + * ApiHaloRunV1alpha1UserApi - functional programming interface + * @export + */ +export const ApiHaloRunV1alpha1UserApiFp = function ( + configuration?: Configuration +) { + const localVarAxiosParamCreator = + ApiHaloRunV1alpha1UserApiAxiosParamCreator(configuration); + return { + /** + * Sign up a new user + * @param {SignUpRequest} signUpRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async signUp( + signUpRequest: SignUpRequest, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.signUp( + signUpRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, + }; +}; + +/** + * ApiHaloRunV1alpha1UserApi - factory interface + * @export + */ +export const ApiHaloRunV1alpha1UserApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance +) { + const localVarFp = ApiHaloRunV1alpha1UserApiFp(configuration); + return { + /** + * Sign up a new user + * @param {ApiHaloRunV1alpha1UserApiSignUpRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signUp( + requestParameters: ApiHaloRunV1alpha1UserApiSignUpRequest, + options?: AxiosRequestConfig + ): AxiosPromise { + return localVarFp + .signUp(requestParameters.signUpRequest, options) + .then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for signUp operation in ApiHaloRunV1alpha1UserApi. + * @export + * @interface ApiHaloRunV1alpha1UserApiSignUpRequest + */ +export interface ApiHaloRunV1alpha1UserApiSignUpRequest { + /** + * + * @type {SignUpRequest} + * @memberof ApiHaloRunV1alpha1UserApiSignUp + */ + readonly signUpRequest: SignUpRequest; +} + +/** + * ApiHaloRunV1alpha1UserApi - object-oriented interface + * @export + * @class ApiHaloRunV1alpha1UserApi + * @extends {BaseAPI} + */ +export class ApiHaloRunV1alpha1UserApi extends BaseAPI { + /** + * Sign up a new user + * @param {ApiHaloRunV1alpha1UserApiSignUpRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiHaloRunV1alpha1UserApi + */ + public signUp( + requestParameters: ApiHaloRunV1alpha1UserApiSignUpRequest, + options?: AxiosRequestConfig + ) { + return ApiHaloRunV1alpha1UserApiFp(this.configuration) + .signUp(requestParameters.signUpRequest, options) + .then((request) => request(this.axios, this.basePath)); + } +} diff --git a/console/packages/api-client/src/models/index.ts b/console/packages/api-client/src/models/index.ts index 94ee5f3cf..c4e5201fb 100644 --- a/console/packages/api-client/src/models/index.ts +++ b/console/packages/api-client/src/models/index.ts @@ -149,3 +149,4 @@ export * from "./user-permission"; export * from "./user-spec"; export * from "./user-status"; export * from "./vote-request"; +export * from "./sign-up-request"; diff --git a/console/packages/api-client/src/models/sign-up-request.ts b/console/packages/api-client/src/models/sign-up-request.ts new file mode 100644 index 000000000..c592e9740 --- /dev/null +++ b/console/packages/api-client/src/models/sign-up-request.ts @@ -0,0 +1,37 @@ +/* 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. + */ + +// May contain unused imports in some cases +// @ts-ignore +import { User } from "./user"; + +/** + * + * @export + * @interface SignUpRequest + */ +export interface SignUpRequest { + /** + * + * @type {string} + * @memberof SignUpRequest + */ + password: string; + /** + * + * @type {User} + * @memberof SignUpRequest + */ + user: User; +} diff --git a/console/src/components/signup/SignupForm.vue b/console/src/components/signup/SignupForm.vue new file mode 100644 index 000000000..514ae4b7e --- /dev/null +++ b/console/src/components/signup/SignupForm.vue @@ -0,0 +1,127 @@ + + + diff --git a/console/src/locales/en.yaml b/console/src/locales/en.yaml index 6fcec0049..a2378484b 100644 --- a/console/src/locales/en.yaml +++ b/console/src/locales/en.yaml @@ -1,5 +1,6 @@ core: login: + title: Login fields: username: placeholder: Username @@ -16,6 +17,23 @@ core: modal: title: Re-login other_login: "Other login:" + signup: + title: Sign up + fields: + username: + placeholder: Username + display_name: + placeholder: Display name + password: + placeholder: Password + password_confirm: + placeholder: Confirm password + operations: + submit: + button: Sign up + toast_success: Sign up successfully + binding: + title: Account binding sidebar: search: placeholder: Search diff --git a/console/src/locales/zh-CN.yaml b/console/src/locales/zh-CN.yaml index dfba91926..fb1c7fb26 100644 --- a/console/src/locales/zh-CN.yaml +++ b/console/src/locales/zh-CN.yaml @@ -1,5 +1,6 @@ core: login: + title: 登录 fields: username: placeholder: 用户名 @@ -16,6 +17,23 @@ core: modal: title: 重新登录 other_login: 其他登录: + signup: + title: 注册 + fields: + username: + placeholder: 用户名 + display_name: + placeholder: 名称 + password: + placeholder: 密码 + password_confirm: + placeholder: 确认密码 + operations: + submit: + button: 注册 + toast_success: 注册成功 + binding: + title: 账号绑定 sidebar: search: placeholder: 搜索 diff --git a/console/src/modules/system/actuator/types/index.ts b/console/src/modules/system/actuator/types/index.ts index 3c3e947cf..b07e9571a 100644 --- a/console/src/modules/system/actuator/types/index.ts +++ b/console/src/modules/system/actuator/types/index.ts @@ -104,4 +104,5 @@ export interface SocialAuthProvider { logo: string; website: string; authenticationUrl: string; + bindingUrl?: string; } diff --git a/console/src/modules/system/users/Binding.vue b/console/src/modules/system/users/Binding.vue new file mode 100644 index 000000000..7fbdbff93 --- /dev/null +++ b/console/src/modules/system/users/Binding.vue @@ -0,0 +1,80 @@ + + diff --git a/console/src/modules/system/users/Login.vue b/console/src/modules/system/users/Login.vue index f8b2f4e32..049d53639 100644 --- a/console/src/modules/system/users/Login.vue +++ b/console/src/modules/system/users/Login.vue @@ -4,6 +4,8 @@ import router from "@/router"; import IconLogo from "~icons/core/logo?width=5rem&height=2rem"; import { useUserStore } from "@/stores/user"; import LoginForm from "@/components/login/LoginForm.vue"; +import { useRouteQuery } from "@vueuse/router"; +import SignupForm from "@/components/signup/SignupForm.vue"; const userStore = useUserStore(); @@ -16,12 +18,26 @@ onBeforeMount(() => { function onLoginSucceed() { window.location.reload(); } + +const type = useRouteQuery("type", ""); + +function handleChangeType() { + type.value = type.value === "signup" ? "" : "signup"; +} diff --git a/console/src/modules/system/users/module.ts b/console/src/modules/system/users/module.ts index 864584401..f7f60e94d 100644 --- a/console/src/modules/system/users/module.ts +++ b/console/src/modules/system/users/module.ts @@ -9,6 +9,7 @@ import PersonalAccessTokens from "./PersonalAccessTokens.vue"; import Login from "./Login.vue"; import { IconUserSettings } from "@halo-dev/components"; import { markRaw } from "vue"; +import Binding from "./Binding.vue"; export default definePlugin({ components: { @@ -19,6 +20,17 @@ export default definePlugin({ path: "/login", name: "Login", component: Login, + meta: { + title: "core.login.title", + }, + }, + { + path: "/binding/:provider", + name: "Binding", + component: Binding, + meta: { + title: "core.binding.title", + }, }, { path: "/users", diff --git a/console/src/router/guards/auth-check.ts b/console/src/router/guards/auth-check.ts index bb3d2fde8..7d191bd1f 100644 --- a/console/src/router/guards/auth-check.ts +++ b/console/src/router/guards/auth-check.ts @@ -1,9 +1,11 @@ import { useUserStore } from "@/stores/user"; import type { Router } from "vue-router"; +const whiteList = ["Setup", "Login", "Binding"]; + export function setupAuthCheckGuard(router: Router) { router.beforeEach((to, from, next) => { - if (to.name === "Setup" || to.name === "Login") { + if (whiteList.includes(to.name as string)) { next(); return; } diff --git a/console/src/router/guards/check-states.ts b/console/src/router/guards/check-states.ts index 4f4ba3baa..1efd9b4f4 100644 --- a/console/src/router/guards/check-states.ts +++ b/console/src/router/guards/check-states.ts @@ -1,9 +1,11 @@ import { useSystemStatesStore } from "@/stores/system-states"; import type { Router } from "vue-router"; +const whiteList = ["Setup", "Login", "Binding"]; + export function setupCheckStatesGuard(router: Router) { router.beforeEach(async (to, from, next) => { - if (to.name === "Setup" || to.name === "Login") { + if (whiteList.includes(to.name as string)) { next(); return; } diff --git a/console/src/utils/api-client.ts b/console/src/utils/api-client.ts index a61aa5710..d973e7020 100644 --- a/console/src/utils/api-client.ts +++ b/console/src/utils/api-client.ts @@ -36,6 +36,7 @@ import { LoginApi, AuthHaloRunV1alpha1AuthProviderApi, AuthHaloRunV1alpha1UserConnectionApi, + ApiHaloRunV1alpha1UserApi, } from "@halo-dev/api-client"; import type { AxiosError, AxiosInstance } from "axios"; import axios from "axios"; @@ -214,6 +215,9 @@ function setupApiClient(axios: AxiosInstance) { baseURL, axios ), + common: { + user: new ApiHaloRunV1alpha1UserApi(undefined, baseURL, axios), + }, }; }