mirror of https://github.com/halo-dev/halo
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
parent
fed80f26f2
commit
6e6bb42778
|
@ -32,6 +32,7 @@ public class SystemSetting {
|
||||||
public static class ThemeRouteRules {
|
public static class ThemeRouteRules {
|
||||||
public static final String GROUP = "routeRules";
|
public static final String GROUP = "routeRules";
|
||||||
|
|
||||||
|
private boolean disableThemePreview;
|
||||||
private String categories;
|
private String categories;
|
||||||
private String archives;
|
private String archives;
|
||||||
private String post;
|
private String post;
|
||||||
|
|
|
@ -28,6 +28,8 @@ public enum AuthorityUtils {
|
||||||
|
|
||||||
public static final String POST_CONTRIBUTOR_ROLE_NAME = "role-template-post-contributor";
|
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.
|
* Converts an array of GrantedAuthority objects to a role set.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
package run.halo.app.theme;
|
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.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
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.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting.Theme;
|
import run.halo.app.infra.SystemSetting.Theme;
|
||||||
|
import run.halo.app.infra.SystemSetting.ThemeRouteRules;
|
||||||
import run.halo.app.infra.ThemeRootGetter;
|
import run.halo.app.infra.ThemeRootGetter;
|
||||||
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
|
@ -21,6 +32,7 @@ public class ThemeResolver {
|
||||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
private final ThemeRootGetter themeRoot;
|
private final ThemeRootGetter themeRoot;
|
||||||
|
private final RoleService roleService;
|
||||||
|
|
||||||
public Mono<ThemeContext> getThemeContext(String themeName) {
|
public Mono<ThemeContext> getThemeContext(String themeName) {
|
||||||
Assert.hasText(themeName, "Theme name cannot be empty");
|
Assert.hasText(themeName, "Theme name cannot be empty");
|
||||||
|
@ -39,17 +51,17 @@ public class ThemeResolver {
|
||||||
|
|
||||||
public Mono<ThemeContext> getTheme(ServerWebExchange exchange) {
|
public Mono<ThemeContext> getTheme(ServerWebExchange exchange) {
|
||||||
return fetchThemeFromExchange(exchange)
|
return fetchThemeFromExchange(exchange)
|
||||||
.switchIfEmpty(Mono.defer(() -> environmentFetcher.fetch(Theme.GROUP, Theme.class)
|
.switchIfEmpty(Mono.defer(() -> fetchActivationState()
|
||||||
.map(Theme::getActive)
|
.map(themeState -> {
|
||||||
.switchIfEmpty(
|
var activatedTheme = themeState.activatedTheme();
|
||||||
Mono.error(() -> new IllegalArgumentException("No theme activated")))
|
|
||||||
.map(activatedTheme -> {
|
|
||||||
var builder = ThemeContext.builder();
|
var builder = ThemeContext.builder();
|
||||||
var themeName = exchange.getRequest().getQueryParams()
|
var themeName = exchange.getRequest().getQueryParams()
|
||||||
.getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME);
|
.getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME);
|
||||||
if (StringUtils.isBlank(themeName)) {
|
|
||||||
|
if (StringUtils.isBlank(themeName) || !themeState.supportsPreviewTheme()) {
|
||||||
themeName = activatedTheme;
|
themeName = activatedTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean active = StringUtils.equals(activatedTheme, themeName);
|
boolean active = StringUtils.equals(activatedTheme, themeName);
|
||||||
var path = themeRoot.get().resolve(themeName);
|
var path = themeRoot.get().resolve(themeName);
|
||||||
return builder.name(themeName)
|
return builder.name(themeName)
|
||||||
|
@ -70,4 +82,44 @@ public class ThemeResolver {
|
||||||
.cast(ThemeContext.class);
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,11 @@ spec:
|
||||||
- group: routeRules
|
- group: routeRules
|
||||||
label: 主题路由设置
|
label: 主题路由设置
|
||||||
formSchema:
|
formSchema:
|
||||||
|
- $formkit: checkbox
|
||||||
|
label: "关闭主题预览"
|
||||||
|
value: false
|
||||||
|
name: disableThemePreview
|
||||||
|
help: "关闭后,未包含主题管理权限的用户将无法通过参数预览未激活的主题"
|
||||||
- $formkit: text
|
- $formkit: text
|
||||||
label: "分类页路由前缀"
|
label: "分类页路由前缀"
|
||||||
value: "categories"
|
value: "categories"
|
||||||
|
|
Loading…
Reference in New Issue