Skip to content

Commit

Permalink
refactor: improve the system initialization process (halo-dev#4306)
Browse files Browse the repository at this point in the history
* refactor: improve the system initialization process

* Sync api-client

Signed-off-by: Ryan Wang <[email protected]>

* feat: add initialized state to global info

* Refine setup page ui

Signed-off-by: Ryan Wang <[email protected]>

* refactor: improve the system initialization process

* Refine setup page ui

Signed-off-by: Ryan Wang <[email protected]>

* Refine setup page ui

Signed-off-by: Ryan Wang <[email protected]>

* fix: update with initialize state

* Refactor setup

Signed-off-by: Ryan Wang <[email protected]>

* refactor: initialization state

* Refactor router guards

Signed-off-by: Ryan Wang <[email protected]>

* Refine i18n

Signed-off-by: Ryan Wang <[email protected]>

* Refactor init data

Signed-off-by: Ryan Wang <[email protected]>

* Refactor init data

Signed-off-by: Ryan Wang <[email protected]>

* Update console/src/views/system/Setup.vue

Co-authored-by: Takagi <[email protected]>

* refactor: initialization interface

---------

Signed-off-by: Ryan Wang <[email protected]>
Co-authored-by: Ryan Wang <[email protected]>
Co-authored-by: Takagi <[email protected]>
  • Loading branch information
3 people authored Aug 11, 2023
1 parent 1172f4a commit 5690de3
Show file tree
Hide file tree
Showing 48 changed files with 1,272 additions and 521 deletions.
4 changes: 4 additions & 0 deletions api/src/main/java/run/halo/app/infra/utils/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,13 +34,19 @@ public class GlobalInfoEndpoint {

private final AuthProviderService authProviderService;

private final InitializationStateGetter initializationStateGetter;

@ReadOperation
public GlobalInfo globalInfo() {
final var info = new GlobalInfo();
info.setExternalUrl(haloProperties.getExternalUrl());
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 -> {
Expand Down Expand Up @@ -70,6 +77,10 @@ public static class GlobalInfo {

private String favicon;

private boolean userInitialized;

private boolean dataInitialized;

private List<SocialAuthProvider> socialAuthProviders;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<ServerResponse> publicKeyRoute(CryptoService cryptoService) {
return new PublicKeyRouteBuilder(cryptoService).build();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ServerResponse> 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<ServerResponse> 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<Void> initializeSystem(SystemInitializationRequest requestBody) {
Mono<Void> initializeAdminUser = superAdminInitializer.initialize(
SuperAdminInitializer.InitializationParam.builder()
.username(requestBody.getUsername())
.password(requestBody.getPassword())
.email(requestBody.getEmail())
.build());

Mono<Void> siteSetting =
Mono.defer(() -> client.get(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
.flatMap(config -> {
Map<String, String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>A cache that caches system setup state.</p>
* 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<Boolean> userInitialized() {
// If user is initialized, return true directly.
if (userInitialized.get()) {
return Mono.just(true);
}
return hasUser()
.doOnNext(userInitialized::set);
}

@Override
public Mono<Boolean> 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<Boolean> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package run.halo.app.infra;

import reactor.core.publisher.Mono;

/**
* <p>A interface that get system initialization state.</p>
*
* @author guqing
* @since 2.9.0
*/
public interface InitializationStateGetter {

/**
* Check if system user is initialized.
*
* @return <code>true</code> if system user is initialized, <code>false</code> otherwise.
*/
Mono<Boolean> userInitialized();

/**
* Check if system basic data is initialized.
*
* @return <code>true</code> if system basic data is initialized, <code>false</code> otherwise.
*/
Mono<Boolean> dataInitialized();
}
Loading

0 comments on commit 5690de3

Please sign in to comment.