-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: system initialization process to adapt to the new login method
- Loading branch information
Showing
19 changed files
with
632 additions
and
377 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
application/src/main/java/run/halo/app/core/endpoint/SystemSetupEndpoint.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
|
||
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.