diff --git a/README.md b/README.md index bb1e7f1..b3ed4bc 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ - `application.yml` 和 `application-druid.yml` 中配置数据库与上传路径 - 配置 Redis 地址与密码 - `endless-ui/vue.config.js` 中配置代理 API 地址 + - 启动后可访问 http://localhost:8080/setup.html 在页面中编辑外置配置(默认仅本机访问,可设置 `setup.allow-remote=true` 放开远程) 4. 启动服务: @@ -459,4 +460,3 @@ --- 💡 **打造更专业的 Minecraft 运维体验,从这里开始!** - diff --git a/endless-admin/src/main/java/cc/endmc/EndlessApplication.java b/endless-admin/src/main/java/cc/endmc/EndlessApplication.java index 778f284..49f0023 100644 --- a/endless-admin/src/main/java/cc/endmc/EndlessApplication.java +++ b/endless-admin/src/main/java/cc/endmc/EndlessApplication.java @@ -14,6 +14,8 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableScheduling; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 启动程序 @@ -23,6 +25,8 @@ @EnableScheduling @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}, scanBasePackages = "cc.endmc") public class EndlessApplication { + private static final Logger log = LoggerFactory.getLogger(EndlessApplication.class); + public static void main(String[] args) { // 初始化配置文件 try { @@ -40,6 +44,14 @@ public static void main(String[] args) { // 获取版本信息 String version = context.getEnvironment().getProperty("endless.version", "Unknown"); + String serverPort = context.getEnvironment().getProperty("server.port", "8080"); + String contextPath = context.getEnvironment().getProperty("server.servlet.context-path", ""); + if (contextPath.isEmpty() || "/".equals(contextPath)) + { + contextPath = ""; + } + String setupUrl = String.format("http://localhost:%s%s/setup.html", serverPort, contextPath); + log.info("🔧 配置向导地址: {} (仅本机访问,可设置 setup.allow-remote=true)", setupUrl); // 收集初始化数据 int serverCount = context.getBean(IServerInfoService.class).selectServerInfoList(new ServerInfo()).size(); diff --git a/endless-admin/src/main/java/cc/endmc/web/controller/setup/SetupConfigController.java b/endless-admin/src/main/java/cc/endmc/web/controller/setup/SetupConfigController.java new file mode 100644 index 0000000..1df627e --- /dev/null +++ b/endless-admin/src/main/java/cc/endmc/web/controller/setup/SetupConfigController.java @@ -0,0 +1,167 @@ +package cc.endmc.web.controller.setup; + +import cc.endmc.common.annotation.Anonymous; +import cc.endmc.common.core.domain.AjaxResult; +import cc.endmc.common.utils.StringUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.Set; + +/** + * 配置向导接口 + */ +@Anonymous +@RestController +@RequestMapping("/setup") +public class SetupConfigController +{ + private static final Logger log = LoggerFactory.getLogger(SetupConfigController.class); + private static final Path CONFIG_DIR = Paths.get("config"); + private static final Set ALLOWED_FILES = Set.of("application.yml", "application-druid.yml"); + private static final Yaml SAFE_YAML = new Yaml(new SafeConstructor(new LoaderOptions())); + + @Value("${setup.allow-remote:false}") + private boolean allowRemote; + + @GetMapping("/config") + public AjaxResult getConfig(@RequestParam(value = "file", defaultValue = "application.yml") String file, + HttpServletRequest request) + { + if (!isRequestAllowed(request)) + { + return AjaxResult.error("仅允许本机访问配置向导,如需远程访问请设置 setup.allow-remote=true"); + } + Path configPath = resolveConfigPath(file); + if (configPath == null) + { + return AjaxResult.error("不支持的配置文件: " + file); + } + if (!Files.exists(configPath)) + { + return AjaxResult.error("配置文件不存在: " + file); + } + try + { + String content = Files.readString(configPath, StandardCharsets.UTF_8); + HashMap data = new HashMap<>(); + data.put("file", file); + data.put("content", content); + return AjaxResult.success(data); + } + catch (IOException e) + { + log.error("读取配置文件失败: {}", configPath.toAbsolutePath(), e); + return AjaxResult.error("读取配置文件失败"); + } + } + + @PostMapping(value = "/config", consumes = MediaType.TEXT_PLAIN_VALUE) + public AjaxResult saveConfig(@RequestParam(value = "file", defaultValue = "application.yml") String file, + @RequestBody(required = false) String content, + HttpServletRequest request) + { + if (!isRequestAllowed(request)) + { + return AjaxResult.error("仅允许本机访问配置向导,如需远程访问请设置 setup.allow-remote=true"); + } + Path configPath = resolveConfigPath(file); + if (configPath == null) + { + return AjaxResult.error("不支持的配置文件: " + file); + } + if (StringUtils.isBlank(content)) + { + return AjaxResult.error("配置内容不能为空"); + } + if (!isValidYaml(content)) + { + return AjaxResult.error("配置内容不是有效的YAML格式"); + } + try + { + Files.createDirectories(CONFIG_DIR); + Files.writeString(configPath, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + log.info("配置文件已保存: {}", configPath.toAbsolutePath()); + return AjaxResult.success("保存成功,请重启服务使配置生效"); + } + catch (IOException e) + { + log.error("保存配置文件失败: {}", configPath.toAbsolutePath(), e); + return AjaxResult.error("保存配置文件失败"); + } + } + + private Path resolveConfigPath(String file) + { + if (!ALLOWED_FILES.contains(file)) + { + return null; + } + Path configDir = CONFIG_DIR.toAbsolutePath().normalize(); + Path normalized = configDir.resolve(file).normalize(); + if (!normalized.startsWith(configDir)) + { + return null; + } + return normalized; + } + + private boolean isRequestAllowed(HttpServletRequest request) + { + return allowRemote || isLocalAddress(request.getRemoteAddr()); + } + + private boolean isLocalAddress(String address) + { + if (address == null) + { + return false; + } + try + { + return InetAddress.getByName(address).isLoopbackAddress(); + } + catch (UnknownHostException ex) + { + return false; + } + } + + private boolean isValidYaml(String content) + { + try + { + SAFE_YAML.load(content); + return true; + } + catch (YAMLException ex) + { + log.warn("YAML格式校验失败", ex); + return false; + } + } +} diff --git a/endless-admin/src/main/resources/application.yml b/endless-admin/src/main/resources/application.yml index be64062..9b0f937 100644 --- a/endless-admin/src/main/resources/application.yml +++ b/endless-admin/src/main/resources/application.yml @@ -45,6 +45,11 @@ server: # Tomcat启动初始化的线程数,默认值10 min-spare: 10 +# 配置向导 +setup: + # 是否允许远程访问配置向导(默认仅本机访问) + allow-remote: false + # 日志配置 logging: level: diff --git a/endless-admin/src/main/resources/static/setup.html b/endless-admin/src/main/resources/static/setup.html new file mode 100644 index 0000000..853dbba --- /dev/null +++ b/endless-admin/src/main/resources/static/setup.html @@ -0,0 +1,227 @@ + + + + + + 配置向导 + + + +
+

配置向导

+

在页面中直接编辑外置配置文件,保存后重启服务使配置生效。

+ +
+ + + + +
+ + +
+
⚠️ 开启 setup.allow-remote=true 后任何可访问该地址的用户都能直接编辑配置且无需登录,请务必仅在受信任网络中使用。
+
保存配置后需要重启服务使新配置生效。
+
+ + + +