84 lines
3.2 KiB
Java
84 lines
3.2 KiB
Java
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('\\', '/');
|
||
}
|
||
}
|