refactor(admin): удалить мёртвый updater/config-write API и его UI

Маршруты, операциями которых продукт не владеет, отвечали заглушкой
"managed by orchestrator" или пустым списком:

  POST /hysteria2ChangeVersion
  GET  /listRelease
  POST /config/updateHysteria2Config
  POST /config/importHysteria2Config
  POST /config/restartServer
  POST /config/uploadCertFile
  GET  /config/hysteria2AcmePath   (не имел потребителя вовсе)

Они удалены, а не оставлены заглушками. Причины две. API-контракт не
должен обещать updater, которого у продукта принципиально нет:
маршрут, всегда возвращающий отказ, вводит в заблуждение. И это лишняя
attack surface плюс технический мусор от прежней архитектуры.

Вместе с маршрутами убраны мёртвые сервисы (StartHysteria2,
StopHysteria2, RestartHysteria2, SetHysteria2Config,
UpdateHysteria2Config, GetAuthHttpUrl, Hysteria2AcmePath), неиспользуемые
типы и клиентские функции фронтенда.

Отдельно - кнопки. "Перезапустить панель" и загрузка сертификатов
обращались к заглушкам, то есть гарантированно возвращали ошибку.
Кнопка, которая всегда падает, - не точка расширения на будущее, а
дефект UX. Удалены вместе со строками i18n.

Конфигурация Hysteria остаётся доступной панели на чтение и на
выгрузку: getHysteria2Config и exportHysteria2Config.
This commit is contained in:
2026-08-27 12:15:53 +05:00
parent 10d2c48cf0
commit 19ffc80130
15 changed files with 20 additions and 357 deletions
-25
View File
@@ -176,10 +176,6 @@ func GetHysteria2Config(c *gin.Context) {
vo.Success(config, c)
}
func UpdateHysteria2Config(c *gin.Context) {
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
}
// ExportHysteria2Config отдаёт оператору фактический серверный конфиг.
//
// Экспорт работает от исходного YAML, а не от типизированной модели: поля,
@@ -200,10 +196,6 @@ func ExportHysteria2Config(c *gin.Context) {
c.Data(200, "application/octet-stream", sanitized)
}
func ImportHysteria2Config(c *gin.Context) {
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
}
func ExportConfig(c *gin.Context) {
configs, err := service.ListConfigNotIn([]string{constant.Hysteria2Config})
if err != nil {
@@ -267,20 +259,3 @@ func ImportConfig(c *gin.Context) {
}()
vo.Success(nil, c)
}
func Hysteria2AcmePath(c *gin.Context) {
hysteria2AcmePathVo, err := service.Hysteria2AcmePath()
if err != nil {
vo.Fail(err.Error(), c)
return
}
vo.Success(hysteria2AcmePathVo, c)
}
func RestartServer(c *gin.Context) {
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
}
func UploadCertFile(c *gin.Context) {
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
}
-8
View File
@@ -32,11 +32,3 @@ func Hysteria2Auth(c *gin.Context) {
}
vo.Hysteria2AuthSuccess(username, c)
}
func Hysteria2ChangeVersion(c *gin.Context) {
vo.Fail("Смена версии Hysteria2 отключена: runtime управляется install-оркестратором HY2XS", c)
}
func ListRelease(c *gin.Context) {
vo.Success([]string{}, c)
}
+4 -44
View File
@@ -5,10 +5,13 @@ import {
ConfigsDto,
ConfigUpdateDto,
ConfigVo,
Hysteria2AcmePathVo,
Hysteria2ServerConfig,
} from "@/api/config/types";
// Серверный конфиг Hysteria доступен панели только на чтение и на выгрузку:
// им владеет install-оркестратор. Клиентов записи, импорта, перезапуска и
// загрузки сертификатов здесь нет — соответствующих маршрутов не существует.
export function getHysteria2ConfigApi(): AxiosPromise<Hysteria2ServerConfig> {
return request({
url: "/config/getHysteria2Config",
@@ -16,16 +19,6 @@ export function getHysteria2ConfigApi(): AxiosPromise<Hysteria2ServerConfig> {
});
}
export function updateHysteria2ConfigApi(
data: Hysteria2ServerConfig
): AxiosPromise {
return request({
url: "/config/updateHysteria2Config",
method: "post",
data: data,
});
}
export function getConfigApi(data: ConfigDto): AxiosPromise<ConfigVo> {
return request({
url: "/config/getConfig",
@@ -77,36 +70,3 @@ export function exportHysteria2ConfigApi(): AxiosPromise {
});
}
export function importHysteria2ConfigApi(data: FormData): AxiosPromise {
return request({
url: "/config/importHysteria2Config",
method: "post",
headers: {
"Content-Type": "multipart/form-data",
},
data: data,
});
}
export function hysteria2AcmePathApi(): AxiosPromise<Hysteria2AcmePathVo> {
return request({
url: "/config/hysteria2AcmePath",
method: "get",
});
}
export function restartServerApi(): AxiosPromise {
return request({
url: "/config/restartServer",
method: "post",
});
}
export function uploadCertFileApi(data: FormData): AxiosPromise<string> {
return request({
url: "/config/uploadCertFile",
method: "post",
data,
headers: { "Content-Type": "multipart/form-data" },
});
}
-5
View File
@@ -340,8 +340,3 @@ export interface Tab {
name: string;
desc: string;
}
export interface Hysteria2AcmePathVo {
crtPath: string;
keyPath: string;
}
-5
View File
@@ -185,10 +185,6 @@ export default {
hysteria2TrafficTime: "Hysteria2 Traffic Time",
huiCrtPath: "HY2XS admin CRT File Path",
huiKeyPath: "HY2XS admin KEY File Path",
uploadCrtFile: "Upload CRT File",
uploadKeyFile: "Upload KEY File",
restartServer: "Restart Panel",
restartTip: "Restarting, please refresh",
useHysteria2Cert: "Use Hysteria2 cert",
huiHttps: "Open https on the panel",
resetTrafficCron: "Reset traffic schedule task",
@@ -237,7 +233,6 @@ export default {
addConfigItem: "Add Config Item",
hysteria2Version: "Hysteria2 Version",
hysteria2Running: "Hysteria2 Status",
hysteria2ChangeVersion: "Change",
addOutbound: "Add Outbound",
extension: "Extension",
listen: "Listen",
-5
View File
@@ -181,10 +181,6 @@ export default {
hysteria2TrafficTime: "Период учёта трафика Hysteria2",
huiCrtPath: "Путь к CRT HY2XS admin",
huiKeyPath: "Путь к KEY HY2XS admin",
uploadCrtFile: "Загрузить CRT",
uploadKeyFile: "Загрузить KEY",
restartServer: "Перезапустить панель",
restartTip: "Перезапуск, обновите страницу",
useHysteria2Cert: "Использовать сертификат Hysteria2",
huiHttps: "Включить HTTPS панели",
resetTrafficCron: "Расписание сброса трафика",
@@ -233,7 +229,6 @@ export default {
addConfigItem: "Добавить параметр",
hysteria2Version: "Версия Hysteria2",
hysteria2Running: "Статус Hysteria2",
hysteria2ChangeVersion: "Сменить",
addOutbound: "Добавить outbound",
extension: "Расширение",
listen: "Адрес прослушивания",
@@ -7,14 +7,6 @@
{{ $t("common.save") }}
</el-button>
</el-form-item>
<el-form-item>
<el-button @click="handleRestartServer">
<template #icon>
<i-ep-refreshRight />
</template>
{{ $t("config.restartServer") }}
</el-button>
</el-form-item>
<el-form-item>
<el-upload
v-model:file-list="fileList"
@@ -166,7 +158,6 @@ import {
exportConfigApi,
importConfigApi,
listConfigApi,
restartServerApi,
updateConfigsApi,
} from "@/api/config";
import { ConfigsUpdateDto } from "@/api/config/types";
@@ -352,21 +343,6 @@ const handleExport = async () => {
}
};
const handleRestartServer = async () => {
try {
ElMessageBox.confirm(t("config.restartTip"), t("common.warning"), {
confirmButtonText: t("common.confirm"),
cancelButtonText: t("common.cancel"),
type: "warning",
}).then(() => {
restartServerApi();
ElMessage.success(t("config.restartTip"));
});
} catch (e) {
/* empty */
}
};
onMounted(() => {
setConfig();
if (route.query.focus === "huiHttps") {
@@ -95,28 +95,9 @@
<el-form-item label="cert" prop="tls.cert">
<el-input
v-model="dataForm.tls.cert"
style="width: 50%"
style="width: 100%"
clearable
/>
<el-upload
style="height: 32px"
ref="uploadCrtFile"
action=""
:file-list="crtFileList"
:http-request="uploadCertFile"
accept=".crt"
:before-upload="
() => {
crtFileList = [];
}
"
:show-file-list="false"
:limit="1"
>
<template #trigger>
<el-button>{{ t("config.uploadCrtFile") }}</el-button>
</template>
</el-upload>
</el-form-item>
</el-tooltip>
<el-tooltip
@@ -127,28 +108,9 @@
<el-form-item label="key" prop="tls.key">
<el-input
v-model="dataForm.tls.key"
style="width: 50%"
style="width: 100%"
clearable
/>
<el-upload
style="height: 32px"
ref="uploadKeyFile"
action=""
:file-list="keyFileList"
:http-request="uploadCertFile"
accept=".key"
:before-upload="
() => {
keyFileList = [];
}
"
:show-file-list="false"
:limit="1"
>
<template #trigger>
<el-button>{{ t("config.uploadKeyFile") }}</el-button>
</template>
</el-upload>
</el-form-item>
</el-tooltip>
<el-tooltip
@@ -1104,13 +1066,10 @@ import {
exportHysteria2ConfigApi,
getHysteria2ConfigApi,
listConfigApi,
uploadCertFileApi,
} from "@/api/config";
import { useI18n } from "vue-i18n";
import { assignWith, deepCopy } from "@/utils/copy";
import { UploadRequestOptions } from "element-plus";
import { dashboardSummaryApi } from "@/api/dashboard";
import { UploadUserFile } from "element-plus";
const { t } = useI18n();
@@ -1181,8 +1140,6 @@ const state = reactive({
version: "",
running: false,
},
crtFileList: [] as UploadUserFile[],
keyFileList: [] as UploadUserFile[],
});
const {
@@ -1203,8 +1160,6 @@ const {
outbounds,
masquerade,
hysteria2Monitor,
crtFileList,
keyFileList,
} = toRefs(state);
const handleExport = async () => {
@@ -1287,33 +1242,6 @@ const setHysteria2Monitor = async () => {
state.hysteria2Monitor.running = data.hysteria.running;
};
const uploadCertFile = async (params: UploadRequestOptions) => {
try {
if (!state.dataForm.tls) {
return;
}
if (
!params.file.name.endsWith(".crt") &&
!params.file.name.endsWith(".key")
) {
ElMessage.error(t("common.fileFormatUnsupported"));
}
if (params.file.size > 1024 * 1024) {
ElMessage.error(t("common.fileTooLarge"));
}
let formData = new FormData();
formData.append("file", params.file);
const { data } = await uploadCertFileApi(formData);
if (params.file.name.endsWith(".crt")) {
state.dataForm.tls.cert = data;
} else if (params.file.name.endsWith(".key")) {
state.dataForm.tls.key = data;
}
} catch (e) {
/* empty */
}
};
onMounted(() => {
setConfig();
setHysteria2Monitor();
-4
View File
@@ -5,7 +5,3 @@ type Hysteria2AuthDto struct {
Auth *string `json:"auth" form:"auth" validate:"required"`
Tx *int64 `json:"tx" form:"tx" validate:"required"`
}
type Hysteria2VersionDto struct {
Version *string `json:"version" form:"version" validate:"required,min=1,max=10"`
}
-10
View File
@@ -30,13 +30,3 @@ func Hysteria2AuthBadRequest(c *gin.Context) {
Id: "",
})
}
type Hysteria2UrlVo struct {
Url string `json:"url"`
QrCode []byte `json:"qrCode"`
}
type Hysteria2AcmePathVo struct {
CrtPath string `json:"crtPath"`
KeyPath string `json:"keyPath"`
}
+5 -5
View File
@@ -5,6 +5,11 @@ import (
"hy2xs-admin/controller"
)
// Серверный конфиг Hysteria в HY2XS доступен только на чтение и на выгрузку:
// его владелец — install-оркестратор. Маршруты записи (updateHysteria2Config,
// importHysteria2Config), перезапуска панели и загрузки сертификатов удалены,
// а не оставлены заглушками: маршрут, который всегда отвечает «feature
// disabled», вводит в заблуждение и остаётся точкой входа.
func initConfigRouter(configApi *gin.RouterGroup) {
config := configApi.Group("/config")
{
@@ -12,13 +17,8 @@ func initConfigRouter(configApi *gin.RouterGroup) {
config.GET("/getConfig", controller.GetConfig)
config.POST("/listConfig", controller.ListConfig)
config.GET("/getHysteria2Config", controller.GetHysteria2Config)
config.POST("/updateHysteria2Config", controller.UpdateHysteria2Config)
config.POST("/exportHysteria2Config", controller.ExportHysteria2Config)
config.POST("/importHysteria2Config", controller.ImportHysteria2Config)
config.POST("/exportConfig", controller.ExportConfig)
config.POST("/importConfig", controller.ImportConfig)
config.GET("/hysteria2AcmePath", controller.Hysteria2AcmePath)
config.POST("/restartServer", controller.RestartServer)
config.POST("/uploadCertFile", controller.UploadCertFile)
}
}
+4 -8
View File
@@ -5,17 +5,13 @@ import (
"hy2xs-admin/controller"
)
// Панель HY2XS не управляет жизненным циклом Hysteria: установка, обновление
// и перезапуск выполняются install-оркестратором. Маршрутов updater'а здесь
// нет намеренно — API-контракт не должен обещать операцию, которой у продукта
// принципиально не существует, и не должен нести лишнюю attack surface.
func initHysteria2MachineAuthRouter(hysteria2Api *gin.RouterGroup) {
hysteria2 := hysteria2Api.Group("/hysteria2")
{
hysteria2.POST("/auth", controller.Hysteria2Auth)
}
}
func initHysteria2Router(hysteria2Api *gin.RouterGroup) {
hysteria2 := hysteria2Api.Group("/hysteria2")
{
hysteria2.POST("/hysteria2ChangeVersion", controller.Hysteria2ChangeVersion)
hysteria2.GET("/listRelease", controller.ListRelease)
}
}
-1
View File
@@ -65,6 +65,5 @@ func Router(router *gin.Engine, huiWebContext *string) {
initDashboardRouter(huiAdminApi)
initPeerRouter(huiAdminApi)
initConfigRouter(huiAdminApi)
initHysteria2Router(huiAdminApi)
initLogRouter(huiAdminApi)
}
-85
View File
@@ -10,7 +10,6 @@ import (
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"net"
"net/url"
"os"
"strconv"
"strings"
@@ -54,62 +53,6 @@ func GetHysteria2Config() (bo.Hysteria2ServerConfig, error) {
return serverConfig, nil
}
func UpdateHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error {
// Значения по умолчанию
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.Hysteria2TrafficStatsSecret})
if err != nil {
return err
}
var hUIWebPort string
var trafficStatsSecret string
for _, item := range config {
if *item.Key == constant.HUIWebPort {
hUIWebPort = *item.Value
} else if *item.Key == constant.Hysteria2TrafficStatsSecret {
trafficStatsSecret = *item.Value
}
}
if hUIWebPort == "" || trafficStatsSecret == "" {
logrus.Errorf("hUIWebPort or trafficStatsSecret is nil")
return errors.New(constant.SysError)
}
authHttpUrl, err := GetAuthHttpUrl()
if err != nil {
return err
}
authType := "http"
authHttpInsecure := true
var auth bo.ServerConfigAuth
auth.Type = &authType
var http bo.ServerConfigAuthHTTP
http.URL = &authHttpUrl
http.Insecure = &authHttpInsecure
auth.HTTP = &http
hysteria2ServerConfig.Auth = &auth
if hysteria2ServerConfig.TrafficStats == nil {
hysteria2ServerConfig.TrafficStats = &bo.ServerConfigTrafficStats{}
}
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
yamlConfig, err := yaml.Marshal(&hysteria2ServerConfig)
if err != nil {
return err
}
return dao.UpdateConfig([]string{constant.Hysteria2Config}, map[string]interface{}{"value": string(yamlConfig)})
}
func SetHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error {
config, err := yaml.Marshal(&hysteria2ServerConfig)
if err != nil {
return err
}
return dao.UpdateConfig([]string{constant.Hysteria2Config}, map[string]interface{}{"value": string(config)})
}
func UpsertConfig(configs []entity.Config) error {
return dao.UpsertConfig(configs)
}
@@ -185,31 +128,3 @@ func GetPortAndCert() (int64, string, string, error) {
return portInt, crtPath, keyPath, nil
}
func GetAuthHttpUrl() (string, error) {
port, crtPath, keyPath, err := GetPortAndCert()
if err != nil {
return "", err
}
protocol := "http"
if crtPath != "" && keyPath != "" {
protocol = "https"
}
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
if err != nil {
return "", err
}
webContext := ""
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
webContext = *config.Value
}
trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret)
if err != nil {
return "", err
}
authURL := fmt.Sprintf("%s://127.0.0.1:%d%s/hui/hysteria2/auth", protocol, port, webContext)
if trafficSecretConfig.Value != nil && strings.TrimSpace(*trafficSecretConfig.Value) != "" {
authURL = fmt.Sprintf("%s?access_token=%s", authURL, url.QueryEscape(strings.TrimSpace(*trafficSecretConfig.Value)))
}
return authURL, nil
}
+5 -54
View File
@@ -2,14 +2,17 @@ package service
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"hy2xs-admin/dao"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/vo"
"hy2xs-admin/util"
)
// Жизненный цикл Hysteria принадлежит systemd и install-оркестратору.
// Панель умеет только наблюдать состояние: функций Start/Stop/Restart и
// смены версии здесь нет намеренно, а не «временно отключены».
func InitHysteria2() error {
if !util.Exists(util.GetHysteria2BinPath()) {
return errors.New("systemd-managed hysteria binary not found")
@@ -31,58 +34,6 @@ func Hysteria2IsRunning() bool {
return err == nil
}
func StartHysteria2() error {
return errors.New("managed by orchestrator: use hy2xs-orchestrator reconfigure")
}
func StopHysteria2() error {
return errors.New("managed by orchestrator: use hy2xs-orchestrator reconfigure")
}
func RestartHysteria2() error {
return errors.New("managed by orchestrator: use hy2xs-orchestrator reconfigure")
}
func ReleaseHysteria2() error {
return nil
}
func Hysteria2AcmePath() (vo.Hysteria2AcmePathVo, error) {
hysteria2AcmePathVo := vo.Hysteria2AcmePathVo{}
hysteria2Config, err := GetHysteria2Config()
if err != nil {
return hysteria2AcmePathVo, err
}
if hysteria2Config.TLS != nil &&
hysteria2Config.TLS.Cert != nil && *hysteria2Config.TLS.Cert != "" &&
hysteria2Config.TLS.Key != nil && *hysteria2Config.TLS.Key != "" {
if util.Exists(*hysteria2Config.TLS.Cert) && util.Exists(*hysteria2Config.TLS.Key) {
hysteria2AcmePathVo.CrtPath = *hysteria2Config.TLS.Cert
hysteria2AcmePathVo.KeyPath = *hysteria2Config.TLS.Key
return hysteria2AcmePathVo, nil
}
return hysteria2AcmePathVo, errors.New("cert not found")
} else if hysteria2Config.ACME != nil &&
hysteria2Config.ACME.Domains != nil &&
len(hysteria2Config.ACME.Domains) > 0 &&
hysteria2Config.ACME.CA != nil &&
*hysteria2Config.ACME.CA != "" &&
hysteria2Config.ACME.Dir != nil &&
*hysteria2Config.ACME.Dir != "" {
acmeDir := *hysteria2Config.ACME.Dir
for _, domain := range hysteria2Config.ACME.Domains {
crtPath, err := util.FindFile(acmeDir, fmt.Sprintf("%s.crt", domain))
if err != nil {
continue
}
keyPath, err := util.FindFile(acmeDir, fmt.Sprintf("%s.key", domain))
if err != nil {
continue
}
hysteria2AcmePathVo.CrtPath = crtPath
hysteria2AcmePathVo.KeyPath = keyPath
return hysteria2AcmePathVo, nil
}
}
return vo.Hysteria2AcmePathVo{}, errors.New("cert not found")
}