diff --git a/application/src/main/java/run/halo/app/infra/SetupStateCache.java b/application/src/main/java/run/halo/app/infra/SetupStateCache.java
new file mode 100644
index 000000000..b98995343
--- /dev/null
+++ b/application/src/main/java/run/halo/app/infra/SetupStateCache.java
@@ -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;
+
+/**
+ *
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/security/InitializeRedirectionWebFilter.java b/application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java
new file mode 100644
index 000000000..e7f9b8386
--- /dev/null
+++ b/application/src/main/java/run/halo/app/security/InitializeRedirectionWebFilter.java
@@ -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 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;
+ }
+}
diff --git a/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java b/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java
new file mode 100644
index 000000000..121c8f0ee
--- /dev/null
+++ b/application/src/test/java/run/halo/app/security/InitializeRedirectionWebFilterTest.java
@@ -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 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 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 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));
+ }
+}
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 c80d6a0cc..2ce586c91 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,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");