mirror of https://github.com/halo-dev/halo
refactor: add system initialization check and redirect to console if not initialized (#3892)
#### What type of PR is this? /kind improvement /area core /milestone 2.5.2 #### What this PR does / why we need it: 添加系统初始化检查,如果未初始化则重定向到控制台。 此检查只针对首页,当用户访问首页时检查到未初始化则跳转到 Console 让用户初始化以优化没有数据时的访问体验。 SetupStateCache 用于缓存系统初始化状态,当数据库状态改变时会更新缓存以优化性能,避免每次访问首页都查询数据。 #### Which issue(s) this PR fixes: A part of #3230 #### Does this PR introduce a user-facing change? ```release-note 添加系统初始化检查,如果未初始化则重定向到控制台 ```pull/3909/head
parent
0f039225ab
commit
a8250500fc
|
@ -0,0 +1,96 @@
|
|||
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;
|
||||
|
||||
/**
|
||||
* <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
|
||||
public class SetupStateCache implements Reconciler<Reconciler.Request>, Supplier<Boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets system setup state.</p>
|
||||
* Never return null.
|
||||
*
|
||||
* @return <code>true</code> if system is initialized, <code>false</code> 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 <code>true</code> if system is initialized, <code>false</code> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import java.net.URI;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SetupStateCache;
|
||||
|
||||
/**
|
||||
* A web filter that will redirect user to set up page if system is not initialized.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.5.2
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InitializeRedirectionWebFilter implements WebFilter {
|
||||
private final URI location = URI.create("/console");
|
||||
private final ServerWebExchangeMatcher redirectMatcher =
|
||||
new PathPatternParserServerWebExchangeMatcher("/", HttpMethod.GET);
|
||||
|
||||
private final SetupStateCache setupStateCache;
|
||||
|
||||
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||
return redirectMatcher.matches(exchange)
|
||||
.flatMap(matched -> {
|
||||
if (!matched.isMatch() || setupStateCache.get()) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.URI;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.infra.SetupStateCache;
|
||||
|
||||
/**
|
||||
* Tests for {@link InitializeRedirectionWebFilter}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.5.2
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InitializeRedirectionWebFilterTest {
|
||||
|
||||
@Mock
|
||||
private SetupStateCache setupStateCache;
|
||||
|
||||
@Mock
|
||||
private ServerRedirectStrategy serverRedirectStrategy;
|
||||
|
||||
@InjectMocks
|
||||
private InitializeRedirectionWebFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
filter.setRedirectStrategy(serverRedirectStrategy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectWhenSystemNotInitialized() {
|
||||
when(setupStateCache.get()).thenReturn(false);
|
||||
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
|
||||
when(serverRedirectStrategy.sendRedirect(any(), any())).thenReturn(Mono.empty().then());
|
||||
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNextCount(0)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
verify(serverRedirectStrategy).sendRedirect(eq(exchange), eq(URI.create("/console")));
|
||||
verify(chain, never()).filter(eq(exchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRedirectWhenSystemInitialized() {
|
||||
when(setupStateCache.get()).thenReturn(true);
|
||||
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty().then());
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNextCount(0)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange),
|
||||
eq(URI.create("/console")));
|
||||
verify(chain).filter(eq(exchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRedirectWhenNotHomePage() {
|
||||
WebFilterChain chain = mock(WebFilterChain.class);
|
||||
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||
when(chain.filter(any())).thenReturn(Mono.empty().then());
|
||||
Mono<Void> result = filter.filter(exchange, chain);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNextCount(0)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
verify(serverRedirectStrategy, never()).sendRedirect(eq(exchange),
|
||||
eq(URI.create("/console")));
|
||||
verify(chain).filter(eq(exchange));
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
|
|||
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.theme.ThemeContext;
|
||||
import run.halo.app.theme.ThemeResolver;
|
||||
|
||||
|
@ -44,11 +45,15 @@ public class ThemeMessageResolverIntegrationTest {
|
|||
|
||||
private URL otherThemeUrl;
|
||||
|
||||
@SpyBean
|
||||
private SetupStateCache setupStateCache;
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws FileNotFoundException, URISyntaxException {
|
||||
when(setupStateCache.get()).thenReturn(true);
|
||||
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
||||
otherThemeUrl = ResourceUtils.getURL("classpath:themes/other");
|
||||
|
||||
|
|
Loading…
Reference in New Issue