From 5690de3f2436bd541c4196b04dc6956b053d76b1 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 11 Aug 2023 09:10:35 +0800 Subject: [PATCH] refactor: improve the system initialization process (#4306) * refactor: improve the system initialization process * Sync api-client Signed-off-by: Ryan Wang * feat: add initialized state to global info * Refine setup page ui Signed-off-by: Ryan Wang * refactor: improve the system initialization process * Refine setup page ui Signed-off-by: Ryan Wang * Refine setup page ui Signed-off-by: Ryan Wang * fix: update with initialize state * Refactor setup Signed-off-by: Ryan Wang * refactor: initialization state * Refactor router guards Signed-off-by: Ryan Wang * Refine i18n Signed-off-by: Ryan Wang * Refactor init data Signed-off-by: Ryan Wang * Refactor init data Signed-off-by: Ryan Wang * Update console/src/views/system/Setup.vue Co-authored-by: Takagi * refactor: initialization interface --------- Signed-off-by: Ryan Wang Co-authored-by: Ryan Wang Co-authored-by: Takagi --- .../run/halo/app/infra/utils/JsonUtils.java | 4 + .../halo/app/actuator/GlobalInfoEndpoint.java | 11 + .../app/config/WebServerSecurityConfig.java | 13 - .../SystemInitializationEndpoint.java | 136 ++++++++++ .../DefaultInitializationStateGetter.java | 65 +++++ .../app/infra/InitializationStateGetter.java | 26 ++ .../run/halo/app/infra/SetupStateCache.java | 96 -------- .../java/run/halo/app/infra/SystemState.java | 74 ++++++ .../app/infra/properties/HaloProperties.java | 2 - .../infra/properties/SecurityProperties.java | 25 -- .../DefaultSuperAdminInitializer.java | 83 +++++++ .../InitializeRedirectionWebFilter.java | 23 +- .../app/security/SuperAdminInitializer.java | 116 ++------- .../src/main/resources/application-dev.yaml | 4 - .../resources/config/i18n/messages.properties | 2 + .../config/i18n/messages_zh.properties | 2 + .../extensions/role-template-anonymous.yaml | 2 + .../SystemInitializationEndpointTest.java | 39 +++ .../infra/InitializationStateGetterTest.java | 84 +++++++ .../run/halo/app/infra/SystemStateTest.java | 51 ++++ .../InitializeRedirectionWebFilterTest.java | 8 +- .../ThemeMessageResolverIntegrationTest.java | 6 +- .../api-client/src/.openapi-generator/FILES | 2 + console/packages/api-client/src/api.ts | 1 + ...pi-console-halo-run-v1alpha1-system-api.ts | 208 ++++++++++++++++ .../packages/api-client/src/models/index.ts | 1 + .../models/system-initialization-request.ts | 45 ++++ console/src/App.vue | 22 +- .../src/components/common/LocaleChange.vue | 42 ++++ .../login/SocialAuthProviderItem.vue | 2 +- console/src/composables/use-global-info.ts | 2 +- console/src/locales/en.yaml | 13 +- console/src/locales/zh-CN.yaml | 13 +- console/src/locales/zh-TW.yaml | 13 +- console/src/main.ts | 11 +- .../src/modules/system/actuator/Actuator.vue | 2 +- console/src/modules/system/users/Login.vue | 38 +-- console/src/router/guards/check-states.ts | 29 ++- console/src/router/index.ts | 2 +- console/src/router/routes.config.ts | 9 + console/src/stores/global-info.ts | 20 ++ console/src/stores/system-states.ts | 40 --- .../types/index.ts => types/actuator.ts} | 3 + console/src/types/index.ts | 1 + console/src/utils/api-client.ts | 2 + console/src/views/system/Setup.vue | 233 ++++++------------ console/src/views/system/SetupInitialData.vue | 165 +++++++++++++ console/tsconfig.app.json | 2 +- 48 files changed, 1272 insertions(+), 521 deletions(-) create mode 100644 application/src/main/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpoint.java create mode 100644 application/src/main/java/run/halo/app/infra/DefaultInitializationStateGetter.java create mode 100644 application/src/main/java/run/halo/app/infra/InitializationStateGetter.java delete mode 100644 application/src/main/java/run/halo/app/infra/SetupStateCache.java create mode 100644 application/src/main/java/run/halo/app/infra/SystemState.java create mode 100644 application/src/main/java/run/halo/app/security/DefaultSuperAdminInitializer.java create mode 100644 application/src/test/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpointTest.java create mode 100644 application/src/test/java/run/halo/app/infra/InitializationStateGetterTest.java create mode 100644 application/src/test/java/run/halo/app/infra/SystemStateTest.java create mode 100644 console/packages/api-client/src/api/api-console-halo-run-v1alpha1-system-api.ts create mode 100644 console/packages/api-client/src/models/system-initialization-request.ts create mode 100644 console/src/components/common/LocaleChange.vue create mode 100644 console/src/stores/global-info.ts delete mode 100644 console/src/stores/system-states.ts rename console/src/{modules/system/actuator/types/index.ts => types/actuator.ts} (95%) create mode 100644 console/src/types/index.ts create mode 100644 console/src/views/system/SetupInitialData.vue diff --git a/api/src/main/java/run/halo/app/infra/utils/JsonUtils.java b/api/src/main/java/run/halo/app/infra/utils/JsonUtils.java index b0b0dc924b..0b33a6750d 100644 --- a/api/src/main/java/run/halo/app/infra/utils/JsonUtils.java +++ b/api/src/main/java/run/halo/app/infra/utils/JsonUtils.java @@ -22,6 +22,10 @@ public class JsonUtils { private JsonUtils() { } + public static ObjectMapper mapper() { + return DEFAULT_JSON_MAPPER; + } + /** * Converts a map to the object specified type. * 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 9ed591f186..ab09f3d88a 100644 --- a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java +++ b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java @@ -14,6 +14,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; import org.springframework.stereotype.Component; import run.halo.app.extension.ConfigMap; +import run.halo.app.infra.InitializationStateGetter; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting.Basic; @@ -33,6 +34,8 @@ public class GlobalInfoEndpoint { private final AuthProviderService authProviderService; + private final InitializationStateGetter initializationStateGetter; + @ReadOperation public GlobalInfo globalInfo() { final var info = new GlobalInfo(); @@ -40,6 +43,10 @@ public GlobalInfo globalInfo() { info.setUseAbsolutePermalink(haloProperties.isUseAbsolutePermalink()); info.setLocale(Locale.getDefault()); info.setTimeZone(TimeZone.getDefault()); + info.setUserInitialized(initializationStateGetter.userInitialized() + .blockOptional().orElse(false)); + info.setDataInitialized(initializationStateGetter.dataInitialized() + .blockOptional().orElse(false)); handleSocialAuthProvider(info); systemConfigFetcher.ifAvailable(fetcher -> fetcher.getConfigMapBlocking() .ifPresent(configMap -> { @@ -70,6 +77,10 @@ public static class GlobalInfo { private String favicon; + private boolean userInitialized; + + private boolean dataInitialized; + private List socialAuthProviders; } diff --git a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index ab614192a0..c0a9654253 100644 --- a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -6,7 +6,6 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -27,13 +26,11 @@ import org.springframework.web.reactive.function.server.ServerResponse; import run.halo.app.core.extension.service.RoleService; import run.halo.app.core.extension.service.UserService; -import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.properties.HaloProperties; import run.halo.app.plugin.extensionpoint.ExtensionGetter; import run.halo.app.security.DefaultUserDetailService; import run.halo.app.security.DynamicMatcherSecurityWebFilterChain; -import run.halo.app.security.SuperAdminInitializer; import run.halo.app.security.authentication.SecurityConfigurer; import run.halo.app.security.authentication.login.CryptoService; import run.halo.app.security.authentication.login.PublicKeyRouteBuilder; @@ -127,16 +124,6 @@ PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } - @Bean - @ConditionalOnProperty(name = "halo.security.initializer.disabled", - havingValue = "false", - matchIfMissing = true) - SuperAdminInitializer superAdminInitializer(ReactiveExtensionClient client, - HaloProperties halo) { - return new SuperAdminInitializer(client, passwordEncoder(), - halo.getSecurity().getInitializer()); - } - @Bean RouterFunction publicKeyRoute(CryptoService cryptoService) { return new PublicKeyRouteBuilder(cryptoService).build(); diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpoint.java new file mode 100644 index 0000000000..d56a5a9f51 --- /dev/null +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpoint.java @@ -0,0 +1,136 @@ +package run.halo.app.core.extension.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 java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.http.HttpStatus; +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.ResponseStatusException; +import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.InitializationStateGetter; +import run.halo.app.infra.SystemSetting; +import run.halo.app.infra.ValidationUtils; +import run.halo.app.infra.exception.UnsatisfiedAttributeValueException; +import run.halo.app.infra.utils.JsonUtils; +import run.halo.app.security.SuperAdminInitializer; + +/** + * System initialization endpoint. + * + * @author guqing + * @since 2.9.0 + */ +@Component +@RequiredArgsConstructor +public class SystemInitializationEndpoint implements CustomEndpoint { + + private final ReactiveExtensionClient client; + private final SuperAdminInitializer superAdminInitializer; + private final InitializationStateGetter initializationStateSupplier; + + @Override + public RouterFunction endpoint() { + var tag = "api.console.halo.run/v1alpha1/System"; + // define a non-resource api + return SpringdocRouteBuilder.route() + .POST("/system/initialize", this::initialize, + builder -> builder.operationId("initialize") + .description("Initialize system") + .tag(tag) + .requestBody(requestBodyBuilder() + .implementation(SystemInitializationRequest.class)) + .response(responseBuilder().implementation(Boolean.class)) + ) + .build(); + } + + private Mono initialize(ServerRequest request) { + return request.bodyToMono(SystemInitializationRequest.class) + .switchIfEmpty( + Mono.error(new ServerWebInputException("Request body must not be empty")) + ) + .doOnNext(requestBody -> { + if (!ValidationUtils.validateName(requestBody.getUsername())) { + throw new UnsatisfiedAttributeValueException( + "The username does not meet the specifications", + "problemDetail.user.username.unsatisfied", null); + } + if (StringUtils.isBlank(requestBody.getPassword())) { + throw new UnsatisfiedAttributeValueException( + "The password does not meet the specifications", + "problemDetail.user.password.unsatisfied", null); + } + }) + .flatMap(requestBody -> initializationStateSupplier.userInitialized() + .flatMap(result -> { + if (result) { + return Mono.error(new ResponseStatusException(HttpStatus.CONFLICT, + "System has been initialized")); + } + return initializeSystem(requestBody); + }) + ) + .then(ServerResponse.ok().bodyValue(true)); + } + + private Mono initializeSystem(SystemInitializationRequest requestBody) { + Mono initializeAdminUser = superAdminInitializer.initialize( + SuperAdminInitializer.InitializationParam.builder() + .username(requestBody.getUsername()) + .password(requestBody.getPassword()) + .email(requestBody.getEmail()) + .build()); + + Mono siteSetting = + Mono.defer(() -> client.get(ConfigMap.class, SystemSetting.SYSTEM_CONFIG) + .flatMap(config -> { + Map data = config.getData(); + if (data == null) { + data = new LinkedHashMap<>(); + config.setData(data); + } + String basic = data.getOrDefault(SystemSetting.Basic.GROUP, "{}"); + SystemSetting.Basic basicSetting = + JsonUtils.jsonToObject(basic, SystemSetting.Basic.class); + basicSetting.setTitle(requestBody.getSiteTitle()); + data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting)); + return client.update(config); + })) + .retryWhen(Retry.backoff(5, Duration.ofMillis(100)) + .filter(t -> t instanceof OptimisticLockingFailureException) + ) + .then(); + return Mono.when(initializeAdminUser, siteSetting); + } + + @Data + public static class SystemInitializationRequest { + + @Schema(requiredMode = REQUIRED, minLength = 1) + private String username; + + @Schema(requiredMode = REQUIRED, minLength = 3) + private String password; + + private String email; + + private String siteTitle; + } +} diff --git a/application/src/main/java/run/halo/app/infra/DefaultInitializationStateGetter.java b/application/src/main/java/run/halo/app/infra/DefaultInitializationStateGetter.java new file mode 100644 index 0000000000..fea5c5a700 --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/DefaultInitializationStateGetter.java @@ -0,0 +1,65 @@ +package run.halo.app.infra; + +import static org.apache.commons.lang3.BooleanUtils.isTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.MetadataUtil; +import run.halo.app.extension.ReactiveExtensionClient; + +/** + *

A cache that caches system setup state.

+ * when setUp state changed, the cache will be updated. + * + * @author guqing + * @since 2.5.2 + */ +@Component +@RequiredArgsConstructor +public class DefaultInitializationStateGetter implements InitializationStateGetter { + private final ReactiveExtensionClient client; + private final AtomicBoolean userInitialized = new AtomicBoolean(false); + private final AtomicBoolean dataInitialized = new AtomicBoolean(false); + + @Override + public Mono userInitialized() { + // If user is initialized, return true directly. + if (userInitialized.get()) { + return Mono.just(true); + } + return hasUser() + .doOnNext(userInitialized::set); + } + + @Override + public Mono dataInitialized() { + if (dataInitialized.get()) { + return Mono.just(true); + } + return client.fetch(ConfigMap.class, SystemState.SYSTEM_STATES_CONFIGMAP) + .map(config -> { + SystemState systemState = SystemState.deserialize(config); + return isTrue(systemState.getIsSetup()); + }) + .defaultIfEmpty(false) + .doOnNext(dataInitialized::set); + } + + private Mono hasUser() { + return client.list(User.class, + user -> { + var labels = MetadataUtil.nullSafeLabels(user); + return isNotTrue(labels.get("halo.run/hidden-user")); + }, null, 1, 10) + .map(result -> result.getTotal() > 0) + .defaultIfEmpty(false); + } + + static boolean isNotTrue(String value) { + return !Boolean.parseBoolean(value); + } +} diff --git a/application/src/main/java/run/halo/app/infra/InitializationStateGetter.java b/application/src/main/java/run/halo/app/infra/InitializationStateGetter.java new file mode 100644 index 0000000000..87a65fd0f5 --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/InitializationStateGetter.java @@ -0,0 +1,26 @@ +package run.halo.app.infra; + +import reactor.core.publisher.Mono; + +/** + *

A interface that get system initialization state.

+ * + * @author guqing + * @since 2.9.0 + */ +public interface InitializationStateGetter { + + /** + * Check if system user is initialized. + * + * @return true if system user is initialized, false otherwise. + */ + Mono userInitialized(); + + /** + * Check if system basic data is initialized. + * + * @return true if system basic data is initialized, false otherwise. + */ + Mono dataInitialized(); +} diff --git a/application/src/main/java/run/halo/app/infra/SetupStateCache.java b/application/src/main/java/run/halo/app/infra/SetupStateCache.java deleted file mode 100644 index b989953433..0000000000 --- a/application/src/main/java/run/halo/app/infra/SetupStateCache.java +++ /dev/null @@ -1,96 +0,0 @@ -package run.halo.app.infra; - -import io.micrometer.common.util.StringUtils; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; -import lombok.Data; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import run.halo.app.extension.ConfigMap; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.controller.Controller; -import run.halo.app.extension.controller.ControllerBuilder; -import run.halo.app.extension.controller.Reconciler; -import run.halo.app.infra.utils.JsonUtils; - -/** - *

A cache that caches system setup state.

- * when setUp state changed, the cache will be updated. - * - * @author guqing - * @since 2.5.2 - */ -@Component -public class SetupStateCache implements Reconciler, Supplier { - public static final String SYSTEM_STATES_CONFIGMAP = "system-states"; - private final ExtensionClient client; - - private final InternalValueCache valueCache = new InternalValueCache(); - - public SetupStateCache(ExtensionClient client) { - this.client = client; - } - - /** - *

Gets system setup state.

- * Never return null. - * - * @return true if system is initialized, false otherwise. - */ - @NonNull - @Override - public Boolean get() { - return valueCache.get(); - } - - @Override - public Result reconcile(Request request) { - if (!SYSTEM_STATES_CONFIGMAP.equals(request.name())) { - return Result.doNotRetry(); - } - valueCache.cache(isInitialized()); - return Result.doNotRetry(); - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return builder - .extension(new ConfigMap()) - .build(); - } - - /** - * Check if system is initialized. - * - * @return true if system is initialized, false otherwise. - */ - private boolean isInitialized() { - return client.fetch(ConfigMap.class, SYSTEM_STATES_CONFIGMAP) - .filter(configMap -> configMap.getData() != null) - .map(ConfigMap::getData) - .flatMap(map -> Optional.ofNullable(map.get(SystemStates.GROUP)) - .filter(StringUtils::isNotBlank) - .map(value -> JsonUtils.jsonToObject(value, SystemStates.class).getIsSetup()) - ) - .orElse(false); - } - - @Data - static class SystemStates { - static final String GROUP = "states"; - Boolean isSetup; - } - - static class InternalValueCache { - private final AtomicBoolean value = new AtomicBoolean(false); - - public boolean cache(boolean newValue) { - return value.getAndSet(newValue); - } - - public boolean get() { - return value.get(); - } - } -} diff --git a/application/src/main/java/run/halo/app/infra/SystemState.java b/application/src/main/java/run/halo/app/infra/SystemState.java new file mode 100644 index 0000000000..1403a15ccb --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/SystemState.java @@ -0,0 +1,74 @@ +package run.halo.app.infra; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonpatch.JsonPatchException; +import com.github.fge.jsonpatch.mergepatch.JsonMergePatch; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.Data; +import org.springframework.lang.NonNull; +import run.halo.app.extension.ConfigMap; +import run.halo.app.infra.utils.JsonParseException; +import run.halo.app.infra.utils.JsonUtils; + +/** + * A model for system state deserialize from {@link run.halo.app.extension.ConfigMap} + * named {@code system-states}. + * + * @author guqing + * @since 2.8.0 + */ +@Data +public class SystemState { + public static final String SYSTEM_STATES_CONFIGMAP = "system-states"; + + static final String GROUP = "states"; + + private Boolean isSetup; + + /** + * Deserialize from {@link ConfigMap}. + * + * @return config map + */ + public static SystemState deserialize(@NonNull ConfigMap configMap) { + Map data = configMap.getData(); + if (data == null) { + return new SystemState(); + } + return JsonUtils.jsonToObject(data.getOrDefault(GROUP, emptyJsonObject()), + SystemState.class); + } + + /** + * Update modified system state to config map. + * + * @param systemState modified system state + * @param configMap config map + */ + public static void update(@NonNull SystemState systemState, @NonNull ConfigMap configMap) { + Map data = configMap.getData(); + if (data == null) { + data = new LinkedHashMap<>(); + configMap.setData(data); + } + JsonNode modifiedJson = JsonUtils.mapper() + .convertValue(systemState, JsonNode.class); + // original + JsonNode sourceJson = + JsonUtils.jsonToObject(data.getOrDefault(GROUP, emptyJsonObject()), JsonNode.class); + try { + // patch + JsonMergePatch jsonMergePatch = JsonMergePatch.fromJson(modifiedJson); + // apply patch to original + JsonNode patchedNode = jsonMergePatch.apply(sourceJson); + data.put(GROUP, JsonUtils.objectToJson(patchedNode)); + } catch (JsonPatchException e) { + throw new JsonParseException(e); + } + } + + private static String emptyJsonObject() { + return "{}"; + } +} diff --git a/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java b/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java index fe263c2659..5a29cf2316 100644 --- a/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java @@ -75,7 +75,5 @@ public void validate(Object target, Errors errors) { errors.rejectValue("externalUrl", "external-url.required.when-using-absolute-permalink", "External URL is required when property `use-absolute-permalink` is set to true."); } - SecurityProperties.Initializer.validateUsername(props.getSecurity().getInitializer(), - errors); } } diff --git a/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java b/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java index 54fa4e40f4..b33e323d17 100644 --- a/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java @@ -3,17 +3,12 @@ import static org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN; import lombok.Data; -import org.springframework.lang.NonNull; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy; import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode; -import org.springframework.validation.Errors; -import run.halo.app.infra.ValidationUtils; @Data public class SecurityProperties { - private final Initializer initializer = new Initializer(); - private final FrameOptions frameOptions = new FrameOptions(); private final ReferrerOptions referrerOptions = new ReferrerOptions(); @@ -32,24 +27,4 @@ public static class ReferrerOptions { private ReferrerPolicy policy = STRICT_ORIGIN_WHEN_CROSS_ORIGIN; } - - @Data - public static class Initializer { - - private boolean disabled; - - private String superAdminUsername = "admin"; - - private String superAdminPassword; - - static void validateUsername(@NonNull Initializer initializer, @NonNull Errors errors) { - if (initializer.isDisabled() || ValidationUtils.validateName( - initializer.getSuperAdminUsername())) { - return; - } - errors.rejectValue("security.initializer.superAdminUsername", - "initializer.superAdminUsername.invalid", - ValidationUtils.NAME_VALIDATION_MESSAGE); - } - } } diff --git a/application/src/main/java/run/halo/app/security/DefaultSuperAdminInitializer.java b/application/src/main/java/run/halo/app/security/DefaultSuperAdminInitializer.java new file mode 100644 index 0000000000..ec98893d29 --- /dev/null +++ b/application/src/main/java/run/halo/app/security/DefaultSuperAdminInitializer.java @@ -0,0 +1,83 @@ +package run.halo.app.security; + +import java.time.Instant; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.Role; +import run.halo.app.core.extension.RoleBinding; +import run.halo.app.core.extension.RoleBinding.RoleRef; +import run.halo.app.core.extension.RoleBinding.Subject; +import run.halo.app.core.extension.User; +import run.halo.app.core.extension.User.UserSpec; +import run.halo.app.extension.Metadata; +import run.halo.app.extension.ReactiveExtensionClient; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DefaultSuperAdminInitializer implements SuperAdminInitializer { + + private static final String SUPER_ROLE_NAME = "super-role"; + + private final ReactiveExtensionClient client; + private final PasswordEncoder passwordEncoder; + + @Override + public Mono initialize(InitializationParam param) { + return client.fetch(User.class, param.getUsername()) + .switchIfEmpty(Mono.defer(() -> client.create( + createAdmin(param.getUsername(), param.getPassword(), param.getEmail()))) + .flatMap(admin -> { + var binding = bindAdminAndSuperRole(admin); + return client.create(binding).thenReturn(admin); + }) + ) + .then(); + } + + RoleBinding bindAdminAndSuperRole(User admin) { + String adminUserName = admin.getMetadata().getName(); + var metadata = new Metadata(); + String name = + String.join("-", adminUserName, SUPER_ROLE_NAME, "binding"); + metadata.setName(name); + var roleRef = new RoleRef(); + roleRef.setName(SUPER_ROLE_NAME); + roleRef.setApiGroup(Role.GROUP); + roleRef.setKind(Role.KIND); + + var subject = new Subject(); + subject.setName(adminUserName); + subject.setApiGroup(admin.groupVersionKind().group()); + subject.setKind(admin.groupVersionKind().kind()); + + var roleBinding = new RoleBinding(); + roleBinding.setMetadata(metadata); + roleBinding.setRoleRef(roleRef); + roleBinding.setSubjects(List.of(subject)); + + return roleBinding; + } + + User createAdmin(String username, String password, String email) { + var metadata = new Metadata(); + metadata.setName(username); + + var spec = new UserSpec(); + spec.setDisplayName("Administrator"); + spec.setDisabled(false); + spec.setRegisteredAt(Instant.now()); + spec.setTwoFactorAuthEnabled(false); + spec.setEmail(email); + spec.setPassword(passwordEncoder.encode(password)); + + var user = new User(); + user.setMetadata(metadata); + user.setSpec(spec); + return user; + } +} diff --git a/application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java b/application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java index e7f9b83864..08f2e373c2 100644 --- a/application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java +++ b/application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java @@ -1,6 +1,7 @@ package run.halo.app.security; import java.net.URI; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpMethod; import org.springframework.lang.NonNull; @@ -14,7 +15,7 @@ import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; -import run.halo.app.infra.SetupStateCache; +import run.halo.app.infra.InitializationStateGetter; /** * A web filter that will redirect user to set up page if system is not initialized. @@ -29,8 +30,9 @@ public class InitializeRedirectionWebFilter implements WebFilter { private final ServerWebExchangeMatcher redirectMatcher = new PathPatternParserServerWebExchangeMatcher("/", HttpMethod.GET); - private final SetupStateCache setupStateCache; + private final InitializationStateGetter initializationStateGetter; + @Getter private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); @Override @@ -38,18 +40,21 @@ public class InitializeRedirectionWebFilter implements WebFilter { public Mono filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) { return redirectMatcher.matches(exchange) .flatMap(matched -> { - if (!matched.isMatch() || setupStateCache.get()) { + if (!matched.isMatch()) { return chain.filter(exchange); } - // Redirect to set up page if system is not initialized. - return redirectStrategy.sendRedirect(exchange, location); + return initializationStateGetter.userInitialized() + .defaultIfEmpty(false) + .flatMap(initialized -> { + if (initialized) { + return chain.filter(exchange); + } + // Redirect to set up page if system is not initialized. + return redirectStrategy.sendRedirect(exchange, location); + }); }); } - public ServerRedirectStrategy getRedirectStrategy() { - return redirectStrategy; - } - public void setRedirectStrategy(ServerRedirectStrategy redirectStrategy) { Assert.notNull(redirectStrategy, "redirectStrategy cannot be null"); this.redirectStrategy = redirectStrategy; diff --git a/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java b/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java index 9cf3466925..935ed02310 100644 --- a/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java +++ b/application/src/main/java/run/halo/app/security/SuperAdminInitializer.java @@ -1,99 +1,29 @@ package run.halo.app.security; -import java.time.Instant; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.util.StringUtils; +import lombok.Builder; +import lombok.Data; import reactor.core.publisher.Mono; -import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding; -import run.halo.app.core.extension.RoleBinding.RoleRef; -import run.halo.app.core.extension.RoleBinding.Subject; -import run.halo.app.core.extension.User; -import run.halo.app.core.extension.User.UserSpec; -import run.halo.app.extension.Metadata; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.properties.SecurityProperties.Initializer; -@Slf4j -public class SuperAdminInitializer { - - private static final String SUPER_ROLE_NAME = "super-role"; - - private final ReactiveExtensionClient client; - private final PasswordEncoder passwordEncoder; - - private final Initializer initializer; - - public SuperAdminInitializer(ReactiveExtensionClient client, PasswordEncoder passwordEncoder, - Initializer initializer) { - this.client = client; - this.passwordEncoder = passwordEncoder; - this.initializer = initializer; - } - - @EventListener - public Mono initialize(ApplicationReadyEvent readyEvent) { - return client.fetch(User.class, initializer.getSuperAdminUsername()) - .switchIfEmpty(Mono.defer(() -> client.create(createAdmin())).flatMap(admin -> { - var binding = bindAdminAndSuperRole(admin); - return client.create(binding).thenReturn(admin); - })).then(); - } - - RoleBinding bindAdminAndSuperRole(User admin) { - var metadata = new Metadata(); - String name = - String.join("-", initializer.getSuperAdminUsername(), SUPER_ROLE_NAME, "binding"); - metadata.setName(name); - var roleRef = new RoleRef(); - roleRef.setName(SUPER_ROLE_NAME); - roleRef.setApiGroup(Role.GROUP); - roleRef.setKind(Role.KIND); - - var subject = new Subject(); - subject.setName(admin.getMetadata().getName()); - subject.setApiGroup(admin.groupVersionKind().group()); - subject.setKind(admin.groupVersionKind().kind()); - - var roleBinding = new RoleBinding(); - roleBinding.setMetadata(metadata); - roleBinding.setRoleRef(roleRef); - roleBinding.setSubjects(List.of(subject)); - - return roleBinding; - } - - User createAdmin() { - var metadata = new Metadata(); - metadata.setName(initializer.getSuperAdminUsername()); - - var spec = new UserSpec(); - spec.setDisplayName("Administrator"); - spec.setDisabled(false); - spec.setRegisteredAt(Instant.now()); - spec.setTwoFactorAuthEnabled(false); - spec.setEmail("admin@halo.run"); - spec.setPassword(passwordEncoder.encode(getPassword())); - - var user = new User(); - user.setMetadata(metadata); - user.setSpec(spec); - return user; - } - - private String getPassword() { - var password = this.initializer.getSuperAdminPassword(); - if (!StringUtils.hasText(password)) { - // generate password - password = RandomStringUtils.randomAlphanumeric(16); - log.info("=== Generated random password: {} for super administrator: {} ===", - password, this.initializer.getSuperAdminUsername()); - } - return password; +/** + * Super admin initializer. + * + * @author guqing + * @since 2.9.0 + */ +public interface SuperAdminInitializer { + + /** + * Initialize super admin. + * + * @param param super admin initialization param + */ + Mono initialize(InitializationParam param); + + @Data + @Builder + class InitializationParam { + private String username; + private String password; + private String email; } } diff --git a/application/src/main/resources/application-dev.yaml b/application/src/main/resources/application-dev.yaml index 4d31488a83..fe6250e8ed 100644 --- a/application/src/main/resources/application-dev.yaml +++ b/application/src/main/resources/application-dev.yaml @@ -19,10 +19,6 @@ halo: proxy: endpoint: http://localhost:3000/ enabled: true - security: - initializer: - super-admin-username: admin - super-admin-password: admin plugin: runtime-mode: development # development, deployment work-dir: ${user.home}/halo2-dev diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index 25893afcd7..542077fb1f 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -39,6 +39,8 @@ problemDetail.run.halo.app.infra.exception.DuplicateNameException=Duplicate name problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin {0} already exists. problemDetail.run.halo.app.infra.exception.RateLimitExceededException=API rate limit exceeded, please try again later. +problemDetail.user.password.unsatisfied=The password does not meet the specifications. +problemDetail.user.username.unsatisfied=The username does not meet the specifications. 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 401ae08e23..3dff2acca6 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -16,6 +16,8 @@ problemDetail.run.halo.app.infra.exception.DuplicateNameException=检测到有 problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=插件 {0} 已经存。 problemDetail.run.halo.app.infra.exception.RateLimitExceededException=请求过于频繁,请稍候再试。 +problemDetail.user.password.unsatisfied=密码不符合规范。 +problemDetail.user.username.unsatisfied=用户名不符合规范。 problemDetail.user.signUpFailed.disallowed=系统不允许注册新用户。 problemDetail.user.duplicateName=用户名 {0} 已存在,请更换用户名后重试。 problemDetail.plugin.version.unsatisfied.requires=插件要求一个最小的系统版本为 {0}, 但当前版本为 {1}。 diff --git a/application/src/main/resources/extensions/role-template-anonymous.yaml b/application/src/main/resources/extensions/role-template-anonymous.yaml index 713576488d..ff5b78a742 100644 --- a/application/src/main/resources/extensions/role-template-anonymous.yaml +++ b/application/src/main/resources/extensions/role-template-anonymous.yaml @@ -23,6 +23,8 @@ rules: verbs: [ "create" ] - nonResourceURLs: [ "/actuator/globalinfo", "/actuator/health", "/actuator/health/*", "/login/public-key" ] verbs: [ "get" ] + - nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/system/initialize" ] + verbs: [ "create" ] --- apiVersion: v1alpha1 kind: "Role" diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpointTest.java new file mode 100644 index 0000000000..e6bb881237 --- /dev/null +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/SystemInitializationEndpointTest.java @@ -0,0 +1,39 @@ +package run.halo.app.core.extension.endpoint; + +import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction; + +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.junit.jupiter.MockitoExtension; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Tests for {@link SystemInitializationEndpoint}. + * + * @author guqing + * @since 2.9.0 + */ +@ExtendWith(MockitoExtension.class) +class SystemInitializationEndpointTest { + + @InjectMocks + SystemInitializationEndpoint initializationEndpoint; + + WebTestClient webTestClient; + + @BeforeEach + void setUp() { + webTestClient = bindToRouterFunction(initializationEndpoint.endpoint()).build(); + } + + @Test + void initialize() { + webTestClient.post() + .uri("/system/initialize") + .exchange() + .expectStatus() + .isBadRequest(); + } +} diff --git a/application/src/test/java/run/halo/app/infra/InitializationStateGetterTest.java b/application/src/test/java/run/halo/app/infra/InitializationStateGetterTest.java new file mode 100644 index 0000000000..b5dc381cd4 --- /dev/null +++ b/application/src/test/java/run/halo/app/infra/InitializationStateGetterTest.java @@ -0,0 +1,84 @@ +package run.halo.app.infra; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; +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 reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import run.halo.app.core.extension.User; +import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ListResult; +import run.halo.app.extension.Metadata; +import run.halo.app.extension.ReactiveExtensionClient; + +/** + * Tests for {@link InitializationStateGetter}. + * + * @author guqing + * @since 2.9.0 + */ +@ExtendWith(MockitoExtension.class) +class InitializationStateGetterTest { + @Mock + private ReactiveExtensionClient client; + + @InjectMocks + private DefaultInitializationStateGetter initializationStateGetter; + + @Test + void userInitialized() { + when(client.list(eq(User.class), any(), any(), anyInt(), anyInt())) + .thenReturn(Mono.empty()); + initializationStateGetter.userInitialized() + .as(StepVerifier::create) + .expectNext(false) + .verifyComplete(); + + User user = new User(); + user.setMetadata(new Metadata()); + user.getMetadata().setName("fake-hidden-user"); + user.getMetadata().setLabels(Map.of("halo.run/hidden-user", "true")); + user.setSpec(new User.UserSpec()); + user.getSpec().setDisplayName("fake-hidden-user"); + ListResult listResult = new ListResult<>(List.of(user)); + + when(client.list(eq(User.class), any(), any(), anyInt(), anyInt())) + .thenReturn(Mono.just(listResult)); + initializationStateGetter.userInitialized() + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + + @Test + void dataInitialized() { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setName(SystemState.SYSTEM_STATES_CONFIGMAP); + configMap.setData(Map.of("states", "{\"isSetup\":true}")); + when(client.fetch(eq(ConfigMap.class), eq(SystemState.SYSTEM_STATES_CONFIGMAP))) + .thenReturn(Mono.just(configMap)); + initializationStateGetter.dataInitialized() + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + + // call again + initializationStateGetter.dataInitialized() + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + // execute only once + verify(client).fetch(eq(ConfigMap.class), eq(SystemState.SYSTEM_STATES_CONFIGMAP)); + } +} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/infra/SystemStateTest.java b/application/src/test/java/run/halo/app/infra/SystemStateTest.java new file mode 100644 index 0000000000..cafce5c604 --- /dev/null +++ b/application/src/test/java/run/halo/app/infra/SystemStateTest.java @@ -0,0 +1,51 @@ +package run.halo.app.infra; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import run.halo.app.extension.ConfigMap; + +/** + * Tests for {@link SystemState}. + * + * @author guqing + * @since 2.8.0 + */ +class SystemStateTest { + + @Test + void deserialize() { + ConfigMap configMap = new ConfigMap(); + SystemState systemState = SystemState.deserialize(configMap); + assertThat(systemState).isNotNull(); + + configMap.setData(Map.of(SystemState.GROUP, "{\"isSetup\":true}")); + systemState = SystemState.deserialize(configMap); + assertThat(systemState.getIsSetup()).isTrue(); + } + + @Test + void update() { + SystemState newSystemState = new SystemState(); + newSystemState.setIsSetup(true); + + ConfigMap configMap = new ConfigMap(); + SystemState.update(newSystemState, configMap); + assertThat(configMap.getData().get(SystemState.GROUP)).isEqualTo("{\"isSetup\":true}"); + + var data = new LinkedHashMap(); + configMap.setData(data); + data.put(SystemState.GROUP, "{\"isSetup\":false}"); + SystemState.update(newSystemState, configMap); + assertThat(configMap.getData().get(SystemState.GROUP)).isEqualTo("{\"isSetup\":true}"); + + data.clear(); + data.put(SystemState.GROUP, "{\"isSetup\":true, \"foo\":\"bar\"}"); + newSystemState.setIsSetup(false); + SystemState.update(newSystemState, configMap); + assertThat(configMap.getData().get(SystemState.GROUP)) + .isEqualTo("{\"isSetup\":false,\"foo\":\"bar\"}"); + } +} diff --git a/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java b/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java index 121c8f0ee1..9d7a1d81bd 100644 --- a/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java +++ b/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java @@ -20,7 +20,7 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import run.halo.app.infra.SetupStateCache; +import run.halo.app.infra.InitializationStateGetter; /** * Tests for {@link InitializeRedirectionWebFilter}. @@ -32,7 +32,7 @@ class InitializeRedirectionWebFilterTest { @Mock - private SetupStateCache setupStateCache; + private InitializationStateGetter initializationStateGetter; @Mock private ServerRedirectStrategy serverRedirectStrategy; @@ -47,7 +47,7 @@ void setUp() { @Test void shouldRedirectWhenSystemNotInitialized() { - when(setupStateCache.get()).thenReturn(false); + when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(false)); WebFilterChain chain = mock(WebFilterChain.class); @@ -69,7 +69,7 @@ void shouldRedirectWhenSystemNotInitialized() { @Test void shouldNotRedirectWhenSystemInitialized() { - when(setupStateCache.get()).thenReturn(true); + when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true)); WebFilterChain chain = mock(WebFilterChain.class); diff --git a/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java b/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java index 2ce586c917..dc63f6b44d 100644 --- a/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java +++ b/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java @@ -24,7 +24,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; -import run.halo.app.infra.SetupStateCache; +import run.halo.app.infra.InitializationStateGetter; import run.halo.app.theme.ThemeContext; import run.halo.app.theme.ThemeResolver; @@ -46,14 +46,14 @@ public class ThemeMessageResolverIntegrationTest { private URL otherThemeUrl; @SpyBean - private SetupStateCache setupStateCache; + private InitializationStateGetter initializationStateGetter; @Autowired private WebTestClient webTestClient; @BeforeEach void setUp() throws FileNotFoundException, URISyntaxException { - when(setupStateCache.get()).thenReturn(true); + when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true)); defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default"); otherThemeUrl = ResourceUtils.getURL("classpath:themes/other"); diff --git a/console/packages/api-client/src/.openapi-generator/FILES b/console/packages/api-client/src/.openapi-generator/FILES index d37628998d..c0989d7452 100644 --- a/console/packages/api-client/src/.openapi-generator/FILES +++ b/console/packages/api-client/src/.openapi-generator/FILES @@ -10,6 +10,7 @@ api/api-console-halo-run-v1alpha1-post-api.ts api/api-console-halo-run-v1alpha1-reply-api.ts api/api-console-halo-run-v1alpha1-single-page-api.ts api/api-console-halo-run-v1alpha1-stats-api.ts +api/api-console-halo-run-v1alpha1-system-api.ts api/api-console-halo-run-v1alpha1-theme-api.ts api/api-console-halo-run-v1alpha1-user-api.ts api/api-console-migration-halo-run-v1alpha1-migration-api.ts @@ -219,6 +220,7 @@ models/snapshot.ts models/stats-vo.ts models/stats.ts models/subject.ts +models/system-initialization-request.ts models/tag-list.ts models/tag-spec.ts models/tag-status.ts diff --git a/console/packages/api-client/src/api.ts b/console/packages/api-client/src/api.ts index 2566a3e8d8..e7f9b9a08c 100644 --- a/console/packages/api-client/src/api.ts +++ b/console/packages/api-client/src/api.ts @@ -21,6 +21,7 @@ export * from "./api/api-console-halo-run-v1alpha1-post-api"; export * from "./api/api-console-halo-run-v1alpha1-reply-api"; export * from "./api/api-console-halo-run-v1alpha1-single-page-api"; export * from "./api/api-console-halo-run-v1alpha1-stats-api"; +export * from "./api/api-console-halo-run-v1alpha1-system-api"; export * from "./api/api-console-halo-run-v1alpha1-theme-api"; export * from "./api/api-console-halo-run-v1alpha1-user-api"; export * from "./api/api-console-migration-halo-run-v1alpha1-migration-api"; diff --git a/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-system-api.ts b/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-system-api.ts new file mode 100644 index 0000000000..730eb34a7c --- /dev/null +++ b/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-system-api.ts @@ -0,0 +1,208 @@ +/* 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 { SystemInitializationRequest } from "../models"; +/** + * ApiConsoleHaloRunV1alpha1SystemApi - axios parameter creator + * @export + */ +export const ApiConsoleHaloRunV1alpha1SystemApiAxiosParamCreator = function ( + configuration?: Configuration +) { + return { + /** + * Initialize system + * @param {SystemInitializationRequest} [systemInitializationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + initialize: async ( + systemInitializationRequest?: SystemInitializationRequest, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/apis/api.console.halo.run/v1alpha1/system/initialize`; + // 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( + systemInitializationRequest, + localVarRequestOptions, + configuration + ); + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +/** + * ApiConsoleHaloRunV1alpha1SystemApi - functional programming interface + * @export + */ +export const ApiConsoleHaloRunV1alpha1SystemApiFp = function ( + configuration?: Configuration +) { + const localVarAxiosParamCreator = + ApiConsoleHaloRunV1alpha1SystemApiAxiosParamCreator(configuration); + return { + /** + * Initialize system + * @param {SystemInitializationRequest} [systemInitializationRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async initialize( + systemInitializationRequest?: SystemInitializationRequest, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.initialize( + systemInitializationRequest, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, + }; +}; + +/** + * ApiConsoleHaloRunV1alpha1SystemApi - factory interface + * @export + */ +export const ApiConsoleHaloRunV1alpha1SystemApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance +) { + const localVarFp = ApiConsoleHaloRunV1alpha1SystemApiFp(configuration); + return { + /** + * Initialize system + * @param {ApiConsoleHaloRunV1alpha1SystemApiInitializeRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + initialize( + requestParameters: ApiConsoleHaloRunV1alpha1SystemApiInitializeRequest = {}, + options?: AxiosRequestConfig + ): AxiosPromise { + return localVarFp + .initialize(requestParameters.systemInitializationRequest, options) + .then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for initialize operation in ApiConsoleHaloRunV1alpha1SystemApi. + * @export + * @interface ApiConsoleHaloRunV1alpha1SystemApiInitializeRequest + */ +export interface ApiConsoleHaloRunV1alpha1SystemApiInitializeRequest { + /** + * + * @type {SystemInitializationRequest} + * @memberof ApiConsoleHaloRunV1alpha1SystemApiInitialize + */ + readonly systemInitializationRequest?: SystemInitializationRequest; +} + +/** + * ApiConsoleHaloRunV1alpha1SystemApi - object-oriented interface + * @export + * @class ApiConsoleHaloRunV1alpha1SystemApi + * @extends {BaseAPI} + */ +export class ApiConsoleHaloRunV1alpha1SystemApi extends BaseAPI { + /** + * Initialize system + * @param {ApiConsoleHaloRunV1alpha1SystemApiInitializeRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiConsoleHaloRunV1alpha1SystemApi + */ + public initialize( + requestParameters: ApiConsoleHaloRunV1alpha1SystemApiInitializeRequest = {}, + options?: AxiosRequestConfig + ) { + return ApiConsoleHaloRunV1alpha1SystemApiFp(this.configuration) + .initialize(requestParameters.systemInitializationRequest, 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 7c1aba3e74..6e100e14f8 100644 --- a/console/packages/api-client/src/models/index.ts +++ b/console/packages/api-client/src/models/index.ts @@ -154,6 +154,7 @@ export * from "./snapshot-list"; export * from "./stats"; export * from "./stats-vo"; export * from "./subject"; +export * from "./system-initialization-request"; export * from "./tag"; export * from "./tag-list"; export * from "./tag-spec"; diff --git a/console/packages/api-client/src/models/system-initialization-request.ts b/console/packages/api-client/src/models/system-initialization-request.ts new file mode 100644 index 0000000000..c19ad63c6e --- /dev/null +++ b/console/packages/api-client/src/models/system-initialization-request.ts @@ -0,0 +1,45 @@ +/* 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 SystemInitializationRequest + */ +export interface SystemInitializationRequest { + /** + * + * @type {string} + * @memberof SystemInitializationRequest + */ + email?: string; + /** + * + * @type {string} + * @memberof SystemInitializationRequest + */ + password: string; + /** + * + * @type {string} + * @memberof SystemInitializationRequest + */ + siteTitle?: string; + /** + * + * @type {string} + * @memberof SystemInitializationRequest + */ + username: string; +} diff --git a/console/src/App.vue b/console/src/App.vue index 5772dd5efc..2405a8e9b1 100644 --- a/console/src/App.vue +++ b/console/src/App.vue @@ -1,11 +1,10 @@ + + diff --git a/console/src/components/login/SocialAuthProviderItem.vue b/console/src/components/login/SocialAuthProviderItem.vue index 828af334d4..a2062583db 100644 --- a/console/src/components/login/SocialAuthProviderItem.vue +++ b/console/src/components/login/SocialAuthProviderItem.vue @@ -1,5 +1,5 @@ diff --git a/console/src/router/guards/check-states.ts b/console/src/router/guards/check-states.ts index 1efd9b4f4a..442c24d834 100644 --- a/console/src/router/guards/check-states.ts +++ b/console/src/router/guards/check-states.ts @@ -1,22 +1,37 @@ -import { useSystemStatesStore } from "@/stores/system-states"; +import { useGlobalInfoStore } from "@/stores/global-info"; +import { useUserStore } from "@/stores/user"; import type { Router } from "vue-router"; -const whiteList = ["Setup", "Login", "Binding"]; - export function setupCheckStatesGuard(router: Router) { router.beforeEach(async (to, from, next) => { - if (whiteList.includes(to.name as string)) { - next(); + const userStore = useUserStore(); + const { globalInfo } = useGlobalInfoStore(); + const { userInitialized, dataInitialized } = globalInfo || {}; + + if (to.name === "Setup" && userInitialized) { + next({ name: "Dashboard" }); return; } - const systemStateStore = useSystemStatesStore(); + if (to.name === "SetupInitialData" && dataInitialized) { + next({ name: "Dashboard" }); + return; + } - if (!systemStateStore.states.isSetup) { + if (userInitialized === false && to.name !== "Setup") { next({ name: "Setup" }); return; } + if ( + dataInitialized === false && + !userStore.isAnonymous && + to.name !== "SetupInitialData" + ) { + next({ name: "SetupInitialData" }); + return; + } + next(); }); } diff --git a/console/src/router/index.ts b/console/src/router/index.ts index e7789b0a78..20d397cb05 100644 --- a/console/src/router/index.ts +++ b/console/src/router/index.ts @@ -22,8 +22,8 @@ const router = createRouter({ }, }); +setupCheckStatesGuard(router); setupAuthCheckGuard(router); setupPermissionGuard(router); -setupCheckStatesGuard(router); export default router; diff --git a/console/src/router/routes.config.ts b/console/src/router/routes.config.ts index 1af7f09bf4..18704b7920 100644 --- a/console/src/router/routes.config.ts +++ b/console/src/router/routes.config.ts @@ -5,6 +5,7 @@ import BasicLayout from "@/layouts/BasicLayout.vue"; import Setup from "@/views/system/Setup.vue"; import Redirect from "@/views/system/Redirect.vue"; import type { MenuGroupType } from "@halo-dev/console-shared"; +import SetupInitialData from "@/views/system/SetupInitialData.vue"; export const routes: Array = [ { @@ -31,6 +32,14 @@ export const routes: Array = [ title: "core.setup.title", }, }, + { + path: "/setup-initial-data", + name: "SetupInitialData", + component: SetupInitialData, + meta: { + title: "core.setup.title", + }, + }, { path: "/redirect", name: "Redirect", diff --git a/console/src/stores/global-info.ts b/console/src/stores/global-info.ts new file mode 100644 index 0000000000..5aa8d232bb --- /dev/null +++ b/console/src/stores/global-info.ts @@ -0,0 +1,20 @@ +import { defineStore } from "pinia"; +import type { GlobalInfo } from "@/types"; +import { ref } from "vue"; +import axios from "axios"; + +export const useGlobalInfoStore = defineStore("global-info", () => { + const globalInfo = ref(); + + async function fetchGlobalInfo() { + const { data } = await axios.get( + `${import.meta.env.VITE_API_URL}/actuator/globalinfo`, + { + withCredentials: true, + } + ); + globalInfo.value = data; + } + + return { globalInfo, fetchGlobalInfo }; +}); diff --git a/console/src/stores/system-states.ts b/console/src/stores/system-states.ts deleted file mode 100644 index 32b0fb19e1..0000000000 --- a/console/src/stores/system-states.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { defineStore } from "pinia"; -import { apiClient } from "@/utils/api-client"; - -interface SystemState { - isSetup: boolean; -} - -interface SystemStatesState { - states: SystemState; -} - -export const useSystemStatesStore = defineStore({ - id: "system-states", - state: (): SystemStatesState => ({ - states: { - isSetup: false, - }, - }), - actions: { - async fetchSystemStates() { - try { - const { data } = - await apiClient.extension.configMap.getv1alpha1ConfigMap( - { - name: "system-states", - }, - { mute: true } - ); - - if (data.data) { - this.states = JSON.parse(data.data["states"]); - return; - } - this.states.isSetup = false; - } catch (error) { - this.states.isSetup = false; - } - }, - }, -}); diff --git a/console/src/modules/system/actuator/types/index.ts b/console/src/types/actuator.ts similarity index 95% rename from console/src/modules/system/actuator/types/index.ts rename to console/src/types/actuator.ts index 911223da15..5f97987d27 100644 --- a/console/src/modules/system/actuator/types/index.ts +++ b/console/src/types/actuator.ts @@ -7,6 +7,9 @@ export interface GlobalInfo { allowRegistration: boolean; socialAuthProviders: SocialAuthProvider[]; useAbsolutePermalink: boolean; + userInitialized: boolean; + dataInitialized: boolean; + favicon?: string; } export interface Info { diff --git a/console/src/types/index.ts b/console/src/types/index.ts new file mode 100644 index 0000000000..9dd25b1f04 --- /dev/null +++ b/console/src/types/index.ts @@ -0,0 +1 @@ +export * from "./actuator"; diff --git a/console/src/utils/api-client.ts b/console/src/utils/api-client.ts index 62d830b20e..52eb57261f 100644 --- a/console/src/utils/api-client.ts +++ b/console/src/utils/api-client.ts @@ -10,6 +10,7 @@ import { ApiConsoleHaloRunV1alpha1AttachmentApi, ApiConsoleHaloRunV1alpha1IndicesApi, ApiConsoleHaloRunV1alpha1AuthProviderApi, + ApiConsoleHaloRunV1alpha1SystemApi, ContentHaloRunV1alpha1CategoryApi, ContentHaloRunV1alpha1CommentApi, ContentHaloRunV1alpha1PostApi, @@ -218,6 +219,7 @@ function setupApiClient(axios: AxiosInstance) { baseURL, axios ), + system: new ApiConsoleHaloRunV1alpha1SystemApi(undefined, baseURL, axios), }; } diff --git a/console/src/views/system/Setup.vue b/console/src/views/system/Setup.vue index c153422200..23ad3ff46d 100644 --- a/console/src/views/system/Setup.vue +++ b/console/src/views/system/Setup.vue @@ -1,201 +1,119 @@ diff --git a/console/src/views/system/SetupInitialData.vue b/console/src/views/system/SetupInitialData.vue new file mode 100644 index 0000000000..59561ea103 --- /dev/null +++ b/console/src/views/system/SetupInitialData.vue @@ -0,0 +1,165 @@ + + + diff --git a/console/tsconfig.app.json b/console/tsconfig.app.json index 3ee7770c14..9ea30c1f10 100644 --- a/console/tsconfig.app.json +++ b/console/tsconfig.app.json @@ -1,6 +1,6 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.json"], "exclude": ["src/**/__tests__/*", "packages/**/*"], "compilerOptions": { "baseUrl": ".",