Files
YschcmsService/src/main/java/com/yunhai/cms/controller/api/ThemeController.java
T
2026-09-07 09:09:39 +08:00

84 lines
3.2 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.cms.controller.api;
import com.yunhai.cms.service.impl.ThemeService;
import com.yunhai.cms.theme.ThemeRegistry;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.List;
/**
* 主题管理接口(方案 §5.2,路径对齐 /api/{domain} 约定):
* /api/theme/list 主题列表;/api/theme/{name}/asset/** 主题资产代理
* (screenshot 与预览资产共用,防穿越同 PortalAssetController,name 先过 Registry 白名单)。
* 走 /api/* CAS 鉴权(现状边界)。
*/
@RestController
@RequestMapping("/api/theme")
@RequiredArgsConstructor
public class ThemeController {
private static final Duration ASSET_CACHE = Duration.ofHours(1);
private final ThemeService themeService;
private final ThemeRegistry themeRegistry;
@GetMapping("/list")
public List<com.yunhai.cms.model.vo.ThemeVo> list() {
return themeService.list();
}
@GetMapping("/{name}/asset/**")
public ResponseEntity<Resource> asset(@PathVariable("name") String name,
HttpServletRequest request) {
if (!themeRegistry.contains(name)) {
return ResponseEntity.notFound().build();
}
String path = extractRelativePath(request, name);
if (path == null || path.isEmpty() || path.contains("..")) {
return ResponseEntity.badRequest().build();
}
Resource resource = new ClassPathResource("templates/themes/" + name + "/assets/" + path);
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
MediaType mediaType = MediaTypeFactory.getMediaType(path)
.orElse(MediaType.APPLICATION_OCTET_STREAM);
return ResponseEntity.ok()
.contentType(mediaType)
.cacheControl(CacheControl.maxAge(ASSET_CACHE).cachePublic())
.body(resource);
}
/**
* 从请求 URI 截取 /asset/ 之后的相对路径(同 PortalAssetController:
* 直接解析 URL-decode 后的 URI,兼容两种路径匹配器语义)。
*/
private String extractRelativePath(HttpServletRequest request, String name) {
String uri = request.getRequestURI();
String prefix = request.getContextPath() + "/api/theme/" + name + "/asset/";
int index = uri.indexOf(prefix);
if (index < 0) {
return null;
}
String relative = uri.substring(index + prefix.length());
try {
relative = java.net.URLDecoder.decode(relative, "UTF-8");
} catch (java.io.UnsupportedEncodingException e) {
return null;
}
return relative.replace('\\', '/');
}
}