feat: allow theme preview for theme admins when preview is disabled (#7277)

#### What type of PR is this?
/kind feature
/area core
/milestone 2.20.x

#### What this PR does / why we need it:
支持禁用主题预览功能,但拥有主题管理权限的用户不受此功能影响

#### Which issue(s) this PR fixes:

Fixes #7204

#### Does this PR introduce a user-facing change?

```release-note
支持禁用主题预览功能,但拥有主题管理权限的用户不受此功能影响
```
pull/7290/head
guqing 2025-03-12 16:39:04 +08:00 committed by GitHub
parent fed80f26f2
commit 6e6bb42778
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 6 deletions

View File

@ -32,6 +32,7 @@ public class SystemSetting {
public static class ThemeRouteRules {
public static final String GROUP = "routeRules";
private boolean disableThemePreview;
private String categories;
private String archives;
private String post;

View File

@ -28,6 +28,8 @@ public enum AuthorityUtils {
public static final String POST_CONTRIBUTOR_ROLE_NAME = "role-template-post-contributor";
public static final String THEME_MANAGEMENT_ROLE_NAME = "role-template-manage-themes";
/**
* Converts an array of GrantedAuthority objects to a role set.
*

View File

@ -1,14 +1,25 @@
package run.halo.app.theme;
import static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles;
import java.util.Collection;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.core.user.service.RoleService;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting.Theme;
import run.halo.app.infra.SystemSetting.ThemeRouteRules;
import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.security.authorization.AuthorityUtils;
/**
* @author johnniang
@ -21,6 +32,7 @@ public class ThemeResolver {
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
private final ThemeRootGetter themeRoot;
private final RoleService roleService;
public Mono<ThemeContext> getThemeContext(String themeName) {
Assert.hasText(themeName, "Theme name cannot be empty");
@ -39,17 +51,17 @@ public class ThemeResolver {
public Mono<ThemeContext> getTheme(ServerWebExchange exchange) {
return fetchThemeFromExchange(exchange)
.switchIfEmpty(Mono.defer(() -> environmentFetcher.fetch(Theme.GROUP, Theme.class)
.map(Theme::getActive)
.switchIfEmpty(
Mono.error(() -> new IllegalArgumentException("No theme activated")))
.map(activatedTheme -> {
.switchIfEmpty(Mono.defer(() -> fetchActivationState()
.map(themeState -> {
var activatedTheme = themeState.activatedTheme();
var builder = ThemeContext.builder();
var themeName = exchange.getRequest().getQueryParams()
.getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME);
if (StringUtils.isBlank(themeName)) {
if (StringUtils.isBlank(themeName) || !themeState.supportsPreviewTheme()) {
themeName = activatedTheme;
}
boolean active = StringUtils.equals(activatedTheme, themeName);
var path = themeRoot.get().resolve(themeName);
return builder.name(themeName)
@ -70,4 +82,44 @@ public class ThemeResolver {
.cast(ThemeContext.class);
}
private Mono<ThemeActivationState> fetchActivationState() {
var builder = ThemeActivationState.builder();
var activatedMono = environmentFetcher.fetch(Theme.GROUP, Theme.class)
.map(Theme::getActive)
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("No theme activated")))
.doOnNext(builder::activatedTheme);
var preivewDisabledMono = environmentFetcher.fetch(ThemeRouteRules.GROUP,
ThemeRouteRules.class)
.map(ThemeRouteRules::isDisableThemePreview)
.defaultIfEmpty(false)
.doOnNext(builder::previewDisabled);
var themeManageMono = ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(au -> !AnonymousUserConst.isAnonymousUser(au.getName()))
.flatMap(au -> supportsPreviewTheme(authoritiesToRoles(au.getAuthorities())))
.doOnNext(builder::hasThemeManagementRole);
return Mono.when(activatedMono, preivewDisabledMono, themeManageMono)
.then(Mono.fromSupplier(builder::build));
}
private Mono<Boolean> supportsPreviewTheme(Collection<String> authorities) {
return roleService.contains(authorities, Set.of(AuthorityUtils.THEME_MANAGEMENT_ROLE_NAME))
.defaultIfEmpty(false);
}
@Builder
record ThemeActivationState(String activatedTheme, boolean previewDisabled,
boolean hasThemeManagementRole) {
private boolean supportsPreviewTheme() {
if (hasThemeManagementRole) {
return true;
}
return !previewDisabled;
}
}
}

View File

@ -142,6 +142,11 @@ spec:
- group: routeRules
label: 主题路由设置
formSchema:
- $formkit: checkbox
label: "关闭主题预览"
value: false
name: disableThemePreview
help: "关闭后,未包含主题管理权限的用户将无法通过参数预览未激活的主题"
- $formkit: text
label: "分类页路由前缀"
value: "categories"