Skip to content

Commit

Permalink
refactor: system initialization process to adapt to the new login method
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Sep 30, 2024
1 parent 875a804 commit a9d2bf2
Show file tree
Hide file tree
Showing 16 changed files with 630 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ public static class PostSpec {

@Data
public static class PostStatus {
@Schema(requiredMode = RequiredMode.REQUIRED)
private String phase;

@Schema
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package run.halo.app.core.endpoint;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.config.PlaceholderConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.util.InMemoryResource;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.StreamUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Unstructured;
import run.halo.app.infra.InitializationStateGetter;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.SystemState;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.YamlUnstructuredLoader;
import run.halo.app.plugin.PluginService;
import run.halo.app.security.SuperAdminInitializer;
import run.halo.app.theme.service.ThemeService;

@Component
@RequiredArgsConstructor
public class SystemSetupEndpoint {
static final String SETUP_TEMPLATE = "setup";
static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
new PropertyPlaceholderHelper(
PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_PREFIX,
PlaceholderConfigurerSupport.DEFAULT_PLACEHOLDER_SUFFIX
);

private final InitializationStateGetter initializationStateGetter;
private final SystemConfigurableEnvironmentFetcher systemConfigFetcher;
private final SuperAdminInitializer superAdminInitializer;
private final ReactiveExtensionClient client;
private final PluginService pluginService;
private final ThemeService themeService;
private final Validator validator;

@Bean
RouterFunction<ServerResponse> setupPageRouter() {
return RouterFunctions.route()
.GET(path("/system/setup").and(accept(MediaType.TEXT_HTML)), this::setupPage)
.POST("/system/setup", contentType(MediaType.APPLICATION_FORM_URLENCODED), this::setup)
.build();
}

private Mono<ServerResponse> setup(ServerRequest request) {
return request.formData()
.map(SetupRequest::new)
.flatMap(body -> {
var bindingResult = body.toBindingResult();
validator.validate(body, bindingResult);
if (bindingResult.hasErrors()) {
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.render(SETUP_TEMPLATE, bindingResult.getModel());
}
return doInitialization(body)
.then(ServerResponse.temporaryRedirect(URI.create("/console")).build());
});
}

private Mono<Void> doInitialization(SetupRequest body) {
var superUserMono = superAdminInitializer.initialize(
SuperAdminInitializer.InitializationParam.builder()
.username(body.getUsername())
.password(body.getPassword())
.email(body.getEmail())
.build()
)
.subscribeOn(Schedulers.boundedElastic());

var basicConfigMono = Mono.defer(() -> systemConfigFetcher.getConfigMap()
.flatMap(configMap -> {
mergeToBasicConfig(body, configMap);
return client.update(configMap);
})
)
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
.filter(t -> t instanceof OptimisticLockingFailureException)
)
.subscribeOn(Schedulers.boundedElastic())
.then();
return Mono.when(superUserMono, basicConfigMono,
initializeNecessaryData(body.getUsername()),
pluginService.installPresetPlugins(),
themeService.installPresetTheme()
)
.then(SystemState.upsetSystemState(client, state -> state.setIsSetup(true)));
}

private Mono<Void> initializeNecessaryData(String username) {
return loadPresetExtensions(username)
.concatMap(client::create)
.subscribeOn(Schedulers.boundedElastic())
.then();
}

private static void mergeToBasicConfig(SetupRequest body, ConfigMap configMap) {
Map<String, String> data = configMap.getData();
if (data == null) {
data = new LinkedHashMap<>();
configMap.setData(data);
}
String basic = data.getOrDefault(SystemSetting.Basic.GROUP, "{}");
var basicSetting = JsonUtils.jsonToObject(basic, SystemSetting.Basic.class);
basicSetting.setTitle(body.getSiteTitle());
data.put(SystemSetting.Basic.GROUP, JsonUtils.objectToJson(basicSetting));
}

private Mono<ServerResponse> setupPage(ServerRequest request) {
return initializationStateGetter.userInitialized()
.flatMap(initialized -> {
if (initialized) {
return ServerResponse.temporaryRedirect(URI.create("/console")).build();
}
var body = new SetupRequest(new LinkedMultiValueMap<>());
var bindingResult = body.toBindingResult();
return ServerResponse.ok().render(SETUP_TEMPLATE, bindingResult.getModel());
});
}

record SetupRequest(MultiValueMap<String, String> formData) {

@Schema(requiredMode = REQUIRED, minLength = 1, maxLength = 50)
@NotBlank
@Size(min = 1, max = 50)
public String getUsername() {
return formData.getFirst("username");
}

@Schema(requiredMode = REQUIRED, minLength = 3, maxLength = 200)
@NotBlank
@Size(min = 3, max = 200)
public String getPassword() {
return formData.getFirst("password");
}

@Email
public String getEmail() {
return formData.getFirst("email");
}

public String getSiteTitle() {
return formData.getFirst("siteTitle");
}

public BindingResult toBindingResult() {
return new BeanPropertyBindingResult(this, "form");
}
}

Flux<Unstructured> loadPresetExtensions(String username) {
return Mono.fromCallable(
() -> {
// read initial-data.yaml to string
var classPathResource = new ClassPathResource("initial-data.yaml");
String rawContent = StreamUtils.copyToString(classPathResource.getInputStream(),
StandardCharsets.UTF_8);
// build properties
var properties = new Properties();
properties.setProperty("username", username);
properties.setProperty("timestamp", Instant.now().toString());
// replace placeholders
var processedContent =
PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(rawContent, properties);
// load yaml to unstructured
var stringResource = new InMemoryResource(processedContent);
var loader = new YamlUnstructuredLoader(stringResource);
return loader.load();
})
.flatMapMany(Flux::fromIterable)
.subscribeOn(Schedulers.boundedElastic());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
import run.halo.app.infra.utils.SettingUtils;
import run.halo.app.plugin.PluginNotFoundException;
import run.halo.app.plugin.PluginService;

@Slf4j
Expand Down Expand Up @@ -298,12 +297,6 @@ public RouterFunction<ServerResponse> endpoint() {
.response(responseBuilder()
.implementation(ObjectNode.class))
)
.GET("plugin-presets", this::listPresets,
builder -> builder.operationId("ListPluginPresets")
.description("List all plugin presets in the system.")
.tag(tag)
.response(responseBuilder().implementationArray(Plugin.class))
)
.GET("plugins/-/bundle.js", this::fetchJsBundle,
builder -> builder.operationId("fetchJsBundle")
.description("Merge all JS bundles of enabled plugins into one.")
Expand Down Expand Up @@ -472,10 +465,6 @@ private Mono<ServerResponse> reload(ServerRequest serverRequest) {
return ServerResponse.ok().body(pluginService.reload(name), Plugin.class);
}

private Mono<ServerResponse> listPresets(ServerRequest request) {
return ServerResponse.ok().body(pluginService.getPresets(), Plugin.class);
}

private Mono<ServerResponse> fetchPluginConfig(ServerRequest request) {
final var name = request.pathVariable("name");
return client.fetch(Plugin.class, name)
Expand Down Expand Up @@ -564,10 +553,6 @@ private Mono<ServerResponse> install(ServerRequest request) {
if (InstallSource.FILE.equals(source)) {
return installFromFile(installRequest.getFile(), pluginService::install);
}
if (InstallSource.PRESET.equals(source)) {
return installFromPreset(installRequest.getPresetName(),
pluginService::install);
}
return Mono.error(
new UnsupportedOperationException("Unsupported install source " + source));
}))
Expand All @@ -586,10 +571,6 @@ private Mono<ServerResponse> upgrade(ServerRequest request) {
return installFromFile(installRequest.getFile(),
path -> pluginService.upgrade(pluginName, path));
}
if (InstallSource.PRESET.equals(source)) {
return installFromPreset(installRequest.getPresetName(),
path -> pluginService.upgrade(pluginName, path));
}
return Mono.error(
new UnsupportedOperationException("Unsupported install source " + source));
}))
Expand All @@ -606,16 +587,6 @@ private Mono<Plugin> installFromFile(FilePart filePart,
this::deleteFileIfExists);
}

private Mono<Plugin> installFromPreset(Mono<String> presetNameMono,
Function<Path, Mono<Plugin>> resourceClosure) {
return presetNameMono.flatMap(pluginService::getPreset)
.switchIfEmpty(
Mono.error(() -> new PluginNotFoundException("Plugin preset was not found.")))
.map(pluginPreset -> pluginPreset.getStatus().getLoadLocation())
.map(Path::of)
.flatMap(resourceClosure);
}

public static class ListRequest extends SortableRequest {

public ListRequest(ServerRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.http.HttpHeaders;
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;
Expand All @@ -41,7 +40,7 @@
* @author guqing
* @since 2.9.0
*/
@Component
// @Component
@RequiredArgsConstructor
public class SystemInitializationEndpoint implements CustomEndpoint {

Expand Down

This file was deleted.

Loading

0 comments on commit a9d2bf2

Please sign in to comment.