2023-02-09 19:18:06 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
_ "embed"
|
2023-04-18 16:51:37 +00:00
|
|
|
"encoding/json"
|
2023-02-09 19:18:06 +00:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-02-14 21:49:19 +00:00
|
|
|
"net"
|
2026-04-02 08:15:07 +00:00
|
|
|
"os"
|
2023-02-09 19:18:06 +00:00
|
|
|
"reflect"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
2024-03-10 21:31:24 +00:00
|
|
|
|
2026-04-02 08:15:07 +00:00
|
|
|
"github.com/mhsanaei/3x-ui/v2/config"
|
2025-09-19 08:05:43 +00:00
|
|
|
"github.com/mhsanaei/3x-ui/v2/database"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/util/reflect_util"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
2026-04-06 18:12:02 +00:00
|
|
|
"gorm.io/gorm/clause"
|
2023-02-09 19:18:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//go:embed config.json
|
|
|
|
|
var xrayTemplateConfig string
|
|
|
|
|
|
|
|
|
|
var defaultValueMap = map[string]string{
|
2026-04-25 12:35:10 +00:00
|
|
|
"xrayTemplateConfig": xrayTemplateConfig,
|
|
|
|
|
"webListen": "",
|
|
|
|
|
"webDomain": "",
|
|
|
|
|
"webPort": "2053",
|
|
|
|
|
"webCertFile": "",
|
|
|
|
|
"webKeyFile": "",
|
|
|
|
|
"secret": random.Seq(32),
|
|
|
|
|
"webBasePath": "/",
|
|
|
|
|
"sessionMaxAge": "360",
|
|
|
|
|
"pageSize": "25",
|
|
|
|
|
"expireDiff": "0",
|
|
|
|
|
"trafficDiff": "0",
|
|
|
|
|
"remarkModel": "-ieo",
|
|
|
|
|
"timeLocation": "Local",
|
|
|
|
|
"tgBotEnable": "false",
|
|
|
|
|
"tgBotToken": "",
|
|
|
|
|
"tgBotProxy": "",
|
|
|
|
|
"tgBotAPIServer": "",
|
|
|
|
|
"tgBotChatId": "",
|
|
|
|
|
"tgRunTime": "@daily",
|
|
|
|
|
"tgBotBackup": "false",
|
|
|
|
|
"tgBotLoginNotify": "true",
|
|
|
|
|
"tgCpu": "80",
|
|
|
|
|
"tgLang": "en-US",
|
|
|
|
|
"twoFactorEnable": "false",
|
|
|
|
|
"twoFactorToken": "",
|
|
|
|
|
"subEnable": "true",
|
|
|
|
|
"subJsonEnable": "false",
|
|
|
|
|
"subTitle": "",
|
|
|
|
|
"subSupportUrl": "",
|
|
|
|
|
"subProfileUrl": "",
|
|
|
|
|
"subAnnounce": "",
|
|
|
|
|
"subEnableRouting": "true",
|
|
|
|
|
"subRoutingRules": "",
|
|
|
|
|
"subListen": "",
|
|
|
|
|
"subPort": "2096",
|
|
|
|
|
"subPath": "/sub/",
|
|
|
|
|
"subDomain": "",
|
|
|
|
|
"subCertFile": "",
|
|
|
|
|
"subKeyFile": "",
|
|
|
|
|
"subUpdates": "12",
|
|
|
|
|
"subEncrypt": "true",
|
|
|
|
|
"subShowInfo": "true",
|
|
|
|
|
"subURI": "",
|
|
|
|
|
"subJsonPath": "/json/",
|
|
|
|
|
"subJsonURI": "",
|
|
|
|
|
"subJsonFragment": "",
|
|
|
|
|
"subJsonNoises": "",
|
|
|
|
|
"subJsonMux": "",
|
|
|
|
|
"subJsonRules": "",
|
|
|
|
|
"subClashEnable": "false",
|
|
|
|
|
"subClashPath": "/clash/",
|
|
|
|
|
"subClashURI": "",
|
|
|
|
|
"subClashTemplate": "",
|
2025-02-22 13:31:08 +00:00
|
|
|
"datepicker": "gregorian",
|
|
|
|
|
"warp": "",
|
|
|
|
|
"externalTrafficInformEnable": "false",
|
|
|
|
|
"externalTrafficInformURI": "",
|
2026-02-09 20:43:17 +00:00
|
|
|
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
|
|
|
|
|
2025-09-28 19:00:16 +00:00
|
|
|
// LDAP defaults
|
2025-10-21 11:02:55 +00:00
|
|
|
"ldapEnable": "false",
|
|
|
|
|
"ldapHost": "",
|
|
|
|
|
"ldapPort": "389",
|
|
|
|
|
"ldapUseTLS": "false",
|
|
|
|
|
"ldapBindDN": "",
|
|
|
|
|
"ldapPassword": "",
|
|
|
|
|
"ldapBaseDN": "",
|
|
|
|
|
"ldapUserFilter": "(objectClass=person)",
|
|
|
|
|
"ldapUserAttr": "mail",
|
|
|
|
|
"ldapVlessField": "vless_enabled",
|
|
|
|
|
"ldapSyncCron": "@every 1m",
|
|
|
|
|
"ldapFlagField": "",
|
|
|
|
|
"ldapTruthyValues": "true,1,yes,on",
|
|
|
|
|
"ldapInvertFlag": "false",
|
|
|
|
|
"ldapInboundTags": "",
|
|
|
|
|
"ldapAutoCreate": "false",
|
|
|
|
|
"ldapAutoDelete": "false",
|
|
|
|
|
"ldapDefaultTotalGB": "0",
|
|
|
|
|
"ldapDefaultExpiryDays": "0",
|
|
|
|
|
"ldapDefaultLimitIP": "0",
|
2026-04-02 12:18:48 +00:00
|
|
|
|
|
|
|
|
// Registration settings
|
2026-04-02 16:01:07 +00:00
|
|
|
"turnstileSiteKey": "",
|
2026-04-02 15:49:30 +00:00
|
|
|
"turnstileSecretKey": "",
|
2026-04-03 01:24:18 +00:00
|
|
|
|
|
|
|
|
// Database settings
|
|
|
|
|
"dbType": "sqlite",
|
|
|
|
|
"dbHost": "127.0.0.1",
|
|
|
|
|
"dbPort": "3306",
|
|
|
|
|
"dbUser": "",
|
|
|
|
|
"dbPassword": "",
|
|
|
|
|
"dbName": "3xui",
|
2026-04-24 12:57:12 +00:00
|
|
|
|
|
|
|
|
// Node settings
|
|
|
|
|
"nodeRole": "master",
|
|
|
|
|
"nodeId": "",
|
|
|
|
|
"syncInterval": "30",
|
|
|
|
|
"trafficFlushInterval": "10",
|
2026-04-26 11:10:25 +00:00
|
|
|
|
|
|
|
|
// Backup settings
|
|
|
|
|
"backupEnabled": "false",
|
|
|
|
|
"backupFrequency": "daily",
|
|
|
|
|
"backupHour": "3",
|
|
|
|
|
"backupMaxCount": "10",
|
2026-04-02 15:49:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// settingGroups defines the nested JSON structure for on-disk settings.
|
|
|
|
|
// Each group maps nested keys to their flat equivalents in defaultValueMap.
|
|
|
|
|
var settingGroups = map[string]map[string]string{
|
2026-04-04 06:46:24 +00:00
|
|
|
"panelNetwork": {
|
|
|
|
|
"listen": "webListen",
|
|
|
|
|
"domain": "webDomain",
|
|
|
|
|
"port": "webPort",
|
|
|
|
|
"basePath": "webBasePath",
|
|
|
|
|
},
|
|
|
|
|
"panelTLS": {
|
|
|
|
|
"certFile": "webCertFile",
|
|
|
|
|
"keyFile": "webKeyFile",
|
|
|
|
|
},
|
|
|
|
|
"panelSecurity": {
|
|
|
|
|
"sessionMaxAge": "sessionMaxAge",
|
|
|
|
|
"twoFactorEnable": "twoFactorEnable",
|
|
|
|
|
"twoFactorToken": "twoFactorToken",
|
|
|
|
|
"turnstileSiteKey": "turnstileSiteKey",
|
|
|
|
|
"turnstileSecretKey": "turnstileSecretKey",
|
|
|
|
|
"secret": "secret",
|
|
|
|
|
},
|
|
|
|
|
"panelUX": {
|
|
|
|
|
"timeLocation": "timeLocation",
|
|
|
|
|
"datepicker": "datepicker",
|
|
|
|
|
"pageSize": "pageSize",
|
|
|
|
|
"expireDiff": "expireDiff",
|
|
|
|
|
"trafficDiff": "trafficDiff",
|
|
|
|
|
"remarkModel": "remarkModel",
|
|
|
|
|
},
|
|
|
|
|
"telegramBot": {
|
|
|
|
|
"enable": "tgBotEnable",
|
|
|
|
|
"token": "tgBotToken",
|
|
|
|
|
"proxy": "tgBotProxy",
|
|
|
|
|
"apiServer": "tgBotAPIServer",
|
|
|
|
|
"chatId": "tgBotChatId",
|
|
|
|
|
"runTime": "tgRunTime",
|
|
|
|
|
"backup": "tgBotBackup",
|
|
|
|
|
"loginNotify": "tgBotLoginNotify",
|
|
|
|
|
"cpu": "tgCpu",
|
|
|
|
|
"lang": "tgLang",
|
|
|
|
|
},
|
|
|
|
|
"subscriptionNetwork": {
|
2026-04-25 16:43:09 +00:00
|
|
|
"enable": "subEnable",
|
|
|
|
|
"jsonEnable": "subJsonEnable",
|
|
|
|
|
"clashEnable": "subClashEnable",
|
|
|
|
|
"listen": "subListen",
|
|
|
|
|
"port": "subPort",
|
|
|
|
|
"path": "subPath",
|
|
|
|
|
"jsonPath": "subJsonPath",
|
|
|
|
|
"clashPath": "subClashPath",
|
|
|
|
|
"domain": "subDomain",
|
|
|
|
|
"certFile": "subCertFile",
|
|
|
|
|
"keyFile": "subKeyFile",
|
|
|
|
|
"updates": "subUpdates",
|
|
|
|
|
"encrypt": "subEncrypt",
|
|
|
|
|
"showInfo": "subShowInfo",
|
|
|
|
|
"uri": "subURI",
|
|
|
|
|
"jsonURI": "subJsonURI",
|
|
|
|
|
"clashURI": "subClashURI",
|
2026-04-04 06:46:24 +00:00
|
|
|
},
|
|
|
|
|
"subscriptionBranding": {
|
|
|
|
|
"title": "subTitle",
|
|
|
|
|
"supportUrl": "subSupportUrl",
|
|
|
|
|
"profileUrl": "subProfileUrl",
|
|
|
|
|
"announce": "subAnnounce",
|
|
|
|
|
},
|
|
|
|
|
"subscriptionRouting": {
|
|
|
|
|
"enableRouting": "subEnableRouting",
|
|
|
|
|
"routingRules": "subRoutingRules",
|
|
|
|
|
"jsonFragment": "subJsonFragment",
|
|
|
|
|
"jsonNoises": "subJsonNoises",
|
|
|
|
|
"jsonMux": "subJsonMux",
|
|
|
|
|
"jsonRules": "subJsonRules",
|
|
|
|
|
},
|
|
|
|
|
"ldapConnection": {
|
|
|
|
|
"enable": "ldapEnable",
|
|
|
|
|
"host": "ldapHost",
|
|
|
|
|
"port": "ldapPort",
|
|
|
|
|
"useTLS": "ldapUseTLS",
|
|
|
|
|
"bindDN": "ldapBindDN",
|
|
|
|
|
"password": "ldapPassword",
|
|
|
|
|
"baseDN": "ldapBaseDN",
|
|
|
|
|
"userFilter": "ldapUserFilter",
|
|
|
|
|
"userAttr": "ldapUserAttr",
|
|
|
|
|
"vlessField": "ldapVlessField",
|
|
|
|
|
},
|
|
|
|
|
"ldapSync": {
|
|
|
|
|
"syncCron": "ldapSyncCron",
|
|
|
|
|
"flagField": "ldapFlagField",
|
|
|
|
|
"truthyValues": "ldapTruthyValues",
|
|
|
|
|
"invertFlag": "ldapInvertFlag",
|
|
|
|
|
"inboundTags": "ldapInboundTags",
|
|
|
|
|
"autoCreate": "ldapAutoCreate",
|
|
|
|
|
"autoDelete": "ldapAutoDelete",
|
|
|
|
|
"defaultTotalGB": "ldapDefaultTotalGB",
|
|
|
|
|
"defaultExpiryDays": "ldapDefaultExpiryDays",
|
|
|
|
|
"defaultLimitIP": "ldapDefaultLimitIP",
|
|
|
|
|
},
|
|
|
|
|
"systemIntegration": {
|
|
|
|
|
"externalTrafficInformEnable": "externalTrafficInformEnable",
|
|
|
|
|
"externalTrafficInformURI": "externalTrafficInformURI",
|
|
|
|
|
"warp": "warp",
|
|
|
|
|
"xrayOutboundTestUrl": "xrayOutboundTestUrl",
|
|
|
|
|
},
|
|
|
|
|
"databaseConnection": {
|
|
|
|
|
"dbType": "dbType",
|
|
|
|
|
"dbHost": "dbHost",
|
|
|
|
|
"dbPort": "dbPort",
|
|
|
|
|
"dbUser": "dbUser",
|
|
|
|
|
"dbPassword": "dbPassword",
|
|
|
|
|
"dbName": "dbName",
|
|
|
|
|
},
|
2026-04-24 12:57:12 +00:00
|
|
|
"node": {
|
|
|
|
|
"nodeRole": "nodeRole",
|
|
|
|
|
"nodeId": "nodeId",
|
|
|
|
|
"syncInterval": "syncInterval",
|
|
|
|
|
"trafficFlushInterval": "trafficFlushInterval",
|
|
|
|
|
},
|
2026-04-26 11:10:25 +00:00
|
|
|
"backup": {
|
|
|
|
|
"enabled": "backupEnabled",
|
|
|
|
|
"frequency": "backupFrequency",
|
|
|
|
|
"hour": "backupHour",
|
|
|
|
|
"maxCount": "backupMaxCount",
|
|
|
|
|
},
|
2026-04-04 06:46:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var legacySettingGroups = map[string]map[string]string{
|
2026-04-02 15:49:30 +00:00
|
|
|
"web": {
|
|
|
|
|
"listen": "webListen",
|
|
|
|
|
"domain": "webDomain",
|
|
|
|
|
"port": "webPort",
|
|
|
|
|
"certFile": "webCertFile",
|
|
|
|
|
"keyFile": "webKeyFile",
|
|
|
|
|
"basePath": "webBasePath",
|
|
|
|
|
"sessionMaxAge": "sessionMaxAge",
|
|
|
|
|
},
|
|
|
|
|
"tgBot": {
|
|
|
|
|
"enable": "tgBotEnable",
|
|
|
|
|
"token": "tgBotToken",
|
|
|
|
|
"proxy": "tgBotProxy",
|
|
|
|
|
"apiServer": "tgBotAPIServer",
|
|
|
|
|
"chatId": "tgBotChatId",
|
|
|
|
|
"runTime": "tgRunTime",
|
|
|
|
|
"backup": "tgBotBackup",
|
|
|
|
|
"loginNotify": "tgBotLoginNotify",
|
|
|
|
|
"cpu": "tgCpu",
|
|
|
|
|
"lang": "tgLang",
|
|
|
|
|
},
|
|
|
|
|
"sub": {
|
|
|
|
|
"enable": "subEnable",
|
|
|
|
|
"jsonEnable": "subJsonEnable",
|
2026-04-25 16:43:09 +00:00
|
|
|
"clashEnable": "subClashEnable",
|
2026-04-02 15:49:30 +00:00
|
|
|
"title": "subTitle",
|
|
|
|
|
"supportUrl": "subSupportUrl",
|
|
|
|
|
"profileUrl": "subProfileUrl",
|
|
|
|
|
"announce": "subAnnounce",
|
|
|
|
|
"enableRouting": "subEnableRouting",
|
|
|
|
|
"routingRules": "subRoutingRules",
|
|
|
|
|
"listen": "subListen",
|
|
|
|
|
"port": "subPort",
|
|
|
|
|
"path": "subPath",
|
2026-04-25 16:43:09 +00:00
|
|
|
"jsonPath": "subJsonPath",
|
|
|
|
|
"clashPath": "subClashPath",
|
2026-04-02 15:49:30 +00:00
|
|
|
"domain": "subDomain",
|
|
|
|
|
"certFile": "subCertFile",
|
|
|
|
|
"keyFile": "subKeyFile",
|
|
|
|
|
"updates": "subUpdates",
|
|
|
|
|
"encrypt": "subEncrypt",
|
|
|
|
|
"showInfo": "subShowInfo",
|
|
|
|
|
"uri": "subURI",
|
|
|
|
|
"jsonURI": "subJsonURI",
|
2026-04-25 16:43:09 +00:00
|
|
|
"clashURI": "subClashURI",
|
2026-04-02 15:49:30 +00:00
|
|
|
"jsonFragment": "subJsonFragment",
|
|
|
|
|
"jsonNoises": "subJsonNoises",
|
|
|
|
|
"jsonMux": "subJsonMux",
|
|
|
|
|
"jsonRules": "subJsonRules",
|
|
|
|
|
},
|
|
|
|
|
"ldap": {
|
|
|
|
|
"enable": "ldapEnable",
|
|
|
|
|
"host": "ldapHost",
|
|
|
|
|
"port": "ldapPort",
|
|
|
|
|
"useTLS": "ldapUseTLS",
|
|
|
|
|
"bindDN": "ldapBindDN",
|
|
|
|
|
"password": "ldapPassword",
|
|
|
|
|
"baseDN": "ldapBaseDN",
|
|
|
|
|
"userFilter": "ldapUserFilter",
|
|
|
|
|
"userAttr": "ldapUserAttr",
|
|
|
|
|
"vlessField": "ldapVlessField",
|
|
|
|
|
"syncCron": "ldapSyncCron",
|
|
|
|
|
"flagField": "ldapFlagField",
|
|
|
|
|
"truthyValues": "ldapTruthyValues",
|
|
|
|
|
"invertFlag": "ldapInvertFlag",
|
|
|
|
|
"inboundTags": "ldapInboundTags",
|
|
|
|
|
"autoCreate": "ldapAutoCreate",
|
|
|
|
|
"autoDelete": "ldapAutoDelete",
|
|
|
|
|
"defaultTotalGB": "ldapDefaultTotalGB",
|
|
|
|
|
"defaultExpiryDays": "ldapDefaultExpiryDays",
|
|
|
|
|
"defaultLimitIP": "ldapDefaultLimitIP",
|
|
|
|
|
},
|
|
|
|
|
"other": {
|
|
|
|
|
"timeLocation": "timeLocation",
|
|
|
|
|
"twoFactorEnable": "twoFactorEnable",
|
|
|
|
|
"twoFactorToken": "twoFactorToken",
|
|
|
|
|
"externalTrafficInformEnable": "externalTrafficInformEnable",
|
|
|
|
|
"externalTrafficInformURI": "externalTrafficInformURI",
|
|
|
|
|
"turnstileSiteKey": "turnstileSiteKey",
|
|
|
|
|
"turnstileSecretKey": "turnstileSecretKey",
|
|
|
|
|
"datepicker": "datepicker",
|
|
|
|
|
"pageSize": "pageSize",
|
|
|
|
|
"expireDiff": "expireDiff",
|
|
|
|
|
"trafficDiff": "trafficDiff",
|
|
|
|
|
"remarkModel": "remarkModel",
|
|
|
|
|
"secret": "secret",
|
|
|
|
|
"warp": "warp",
|
|
|
|
|
"xrayOutboundTestUrl": "xrayOutboundTestUrl",
|
2026-04-03 01:24:18 +00:00
|
|
|
"dbType": "dbType",
|
|
|
|
|
"dbHost": "dbHost",
|
|
|
|
|
"dbPort": "dbPort",
|
|
|
|
|
"dbUser": "dbUser",
|
|
|
|
|
"dbPassword": "dbPassword",
|
|
|
|
|
"dbName": "dbName",
|
2026-04-02 15:49:30 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 06:46:24 +00:00
|
|
|
func settingsLayoutMeta() map[string]string {
|
|
|
|
|
return map[string]string{
|
|
|
|
|
"layout": "按模块-用途来归类",
|
|
|
|
|
"schema": "module-purpose-v1",
|
|
|
|
|
"description": "Top-level groups are organized by module and purpose for easier maintenance and development.",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:49:30 +00:00
|
|
|
// flatToNestedKey maps flat keys to their [group, nestedKey] pair.
|
|
|
|
|
var flatToNestedKey map[string][2]string
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
flatToNestedKey = make(map[string][2]string)
|
|
|
|
|
for group, keys := range settingGroups {
|
|
|
|
|
for nestedKey, flatKey := range keys {
|
|
|
|
|
flatToNestedKey[flatKey] = [2]string{group, nestedKey}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// expandToNested converts a flat map[string]string to nested map[string]any
|
|
|
|
|
// using the settingGroups mapping. Keys not in any group are placed at the top level.
|
|
|
|
|
func expandToNested(flat map[string]string) map[string]any {
|
|
|
|
|
result := make(map[string]any)
|
2026-04-04 06:46:24 +00:00
|
|
|
result["_meta"] = settingsLayoutMeta()
|
2026-04-02 15:49:30 +00:00
|
|
|
|
|
|
|
|
// Initialize all groups
|
|
|
|
|
for group := range settingGroups {
|
|
|
|
|
result[group] = make(map[string]string)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Place each flat key into its group
|
|
|
|
|
for flatKey, value := range flat {
|
|
|
|
|
if pair, ok := flatToNestedKey[flatKey]; ok {
|
|
|
|
|
group, nestedKey := pair[0], pair[1]
|
|
|
|
|
result[group].(map[string]string)[nestedKey] = value
|
|
|
|
|
} else {
|
|
|
|
|
// Ungrouped keys go to top level
|
|
|
|
|
result[flatKey] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove empty groups
|
|
|
|
|
for group := range result {
|
|
|
|
|
if m, ok := result[group].(map[string]string); ok && len(m) == 0 {
|
|
|
|
|
delete(result, group)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// flattenNested converts a nested map[string]any (from JSON) to a flat map[string]string.
|
|
|
|
|
// It uses settingGroups to map nested keys back to flat keys.
|
|
|
|
|
func flattenNested(nested map[string]any) map[string]string {
|
|
|
|
|
result := make(map[string]string)
|
|
|
|
|
|
|
|
|
|
for key, val := range nested {
|
|
|
|
|
switch v := val.(type) {
|
|
|
|
|
case map[string]any:
|
|
|
|
|
// This is a group
|
|
|
|
|
if groupKeys, ok := settingGroups[key]; ok {
|
|
|
|
|
for nestedKey, flatKey := range groupKeys {
|
|
|
|
|
if strVal, exists := v[nestedKey]; exists {
|
|
|
|
|
result[flatKey] = fmt.Sprint(strVal)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 06:46:24 +00:00
|
|
|
} else if groupKeys, ok := legacySettingGroups[key]; ok {
|
|
|
|
|
for nestedKey, flatKey := range groupKeys {
|
|
|
|
|
if strVal, exists := v[nestedKey]; exists {
|
|
|
|
|
result[flatKey] = fmt.Sprint(strVal)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-02 15:49:30 +00:00
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
// Top-level value (ungrouped key)
|
|
|
|
|
result[key] = fmt.Sprint(val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tryParseNested detects whether the JSON is nested or flat format and returns a flat map.
|
|
|
|
|
func tryParseNested(data []byte) (map[string]string, error) {
|
|
|
|
|
// First try to detect if it's nested by checking for object values
|
|
|
|
|
var probe map[string]any
|
|
|
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if any value is a nested object (map[string]any) — indicates nested format
|
|
|
|
|
for _, v := range probe {
|
|
|
|
|
if _, isNested := v.(map[string]any); isNested {
|
|
|
|
|
return flattenNested(probe), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flat format — all values are strings
|
|
|
|
|
result := make(map[string]string, len(probe))
|
|
|
|
|
for k, v := range probe {
|
|
|
|
|
result[k] = fmt.Sprint(v)
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:15:07 +00:00
|
|
|
// loadSettings reads the JSON settings file into a map.
|
|
|
|
|
// If the file doesn't exist, it creates one from defaultValueMap (excluding xrayTemplateConfig).
|
2026-04-02 14:25:50 +00:00
|
|
|
// If the file exists, missing keys from defaultValueMap are merged in (supports new fields added after install).
|
2026-04-02 08:15:07 +00:00
|
|
|
func loadSettings() (map[string]string, error) {
|
|
|
|
|
path := config.GetSettingPath()
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
settings := make(map[string]string)
|
|
|
|
|
for k, v := range defaultValueMap {
|
|
|
|
|
if k == "xrayTemplateConfig" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
settings[k] = v
|
|
|
|
|
}
|
|
|
|
|
return settings, saveSettings(settings)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-02 15:49:30 +00:00
|
|
|
// Detect format: try nested first, fall back to flat
|
|
|
|
|
settings, err := tryParseNested(data)
|
|
|
|
|
if err != nil {
|
2026-04-02 08:15:07 +00:00
|
|
|
return nil, fmt.Errorf("failed to parse settings file %s: %w", path, err)
|
|
|
|
|
}
|
2026-04-02 14:25:50 +00:00
|
|
|
// Merge missing keys from defaults so new fields are picked up on upgrade
|
|
|
|
|
needsSave := false
|
|
|
|
|
for k, v := range defaultValueMap {
|
|
|
|
|
if k == "xrayTemplateConfig" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, exists := settings[k]; !exists {
|
|
|
|
|
settings[k] = v
|
|
|
|
|
needsSave = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if needsSave {
|
|
|
|
|
if err := saveSettings(settings); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to save merged settings: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
return settings, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:49:30 +00:00
|
|
|
// saveSettings writes the settings map to the JSON file in nested format.
|
2026-04-02 08:15:07 +00:00
|
|
|
func saveSettings(settings map[string]string) error {
|
2026-04-02 15:49:30 +00:00
|
|
|
nested := expandToNested(settings)
|
|
|
|
|
data, err := json.MarshalIndent(nested, "", " ")
|
2026-04-02 08:15:07 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return os.WriteFile(config.GetSettingPath(), data, 0644)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getXrayTemplateConfigFromDB reads xrayTemplateConfig directly from the database.
|
|
|
|
|
func getXrayTemplateConfigFromDB() (string, error) {
|
|
|
|
|
db := database.GetDB()
|
|
|
|
|
setting := &model.Setting{}
|
2026-04-06 03:04:43 +00:00
|
|
|
err := db.Model(model.Setting{}).Where("`key` = ?", "xrayTemplateConfig").First(setting).Error
|
2026-04-02 08:15:07 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return setting.Value, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// saveXrayTemplateConfigToDB writes xrayTemplateConfig directly to the database.
|
|
|
|
|
func saveXrayTemplateConfigToDB(value string) error {
|
|
|
|
|
db := database.GetDB()
|
2026-04-06 18:12:02 +00:00
|
|
|
return db.Clauses(clause.OnConflict{
|
|
|
|
|
Columns: []clause.Column{{Name: "key"}},
|
|
|
|
|
DoUpdates: clause.Assignments(map[string]any{"value": value}),
|
|
|
|
|
}).Create(&model.Setting{
|
|
|
|
|
Key: "xrayTemplateConfig",
|
|
|
|
|
Value: value,
|
|
|
|
|
}).Error
|
2026-04-02 08:15:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
// SettingService provides business logic for application settings management.
|
|
|
|
|
// It handles configuration storage, retrieval, and validation for all system settings.
|
2024-03-10 21:31:24 +00:00
|
|
|
type SettingService struct{}
|
2023-02-09 19:18:06 +00:00
|
|
|
|
2026-03-04 12:05:29 +00:00
|
|
|
func (s *SettingService) GetDefaultJSONConfig() (any, error) {
|
2025-03-12 19:13:51 +00:00
|
|
|
var jsonData any
|
2023-04-18 16:51:37 +00:00
|
|
|
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return jsonData, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
2026-04-02 08:15:07 +00:00
|
|
|
settings, err := loadSettings()
|
2023-02-09 19:18:06 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
allSetting := &entity.AllSetting{}
|
2026-03-04 12:05:29 +00:00
|
|
|
t := reflect.TypeFor[entity.AllSetting]()
|
2023-02-09 19:18:06 +00:00
|
|
|
v := reflect.ValueOf(allSetting).Elem()
|
|
|
|
|
fields := reflect_util.GetFields(t)
|
|
|
|
|
|
|
|
|
|
setSetting := func(key, value string) (err error) {
|
|
|
|
|
defer func() {
|
|
|
|
|
panicErr := recover()
|
|
|
|
|
if panicErr != nil {
|
|
|
|
|
err = errors.New(fmt.Sprint(panicErr))
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
var found bool
|
|
|
|
|
var field reflect.StructField
|
|
|
|
|
for _, f := range fields {
|
|
|
|
|
if f.Tag.Get("json") == key {
|
|
|
|
|
field = f
|
|
|
|
|
found = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fieldV := v.FieldByName(field.Name)
|
|
|
|
|
switch t := fieldV.Interface().(type) {
|
|
|
|
|
case int:
|
|
|
|
|
n, err := strconv.ParseInt(value, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fieldV.SetInt(n)
|
|
|
|
|
case string:
|
|
|
|
|
fieldV.SetString(value)
|
|
|
|
|
case bool:
|
|
|
|
|
fieldV.SetBool(value == "true")
|
|
|
|
|
default:
|
|
|
|
|
return common.NewErrorf("unknown field %v type %v", key, t)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keyMap := map[string]bool{}
|
2026-04-02 08:15:07 +00:00
|
|
|
for key, value := range settings {
|
|
|
|
|
err := setSetting(key, value)
|
2023-02-09 19:18:06 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
keyMap[key] = true
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key, value := range defaultValueMap {
|
2026-04-02 08:15:07 +00:00
|
|
|
if key == "xrayTemplateConfig" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2023-02-09 19:18:06 +00:00
|
|
|
if keyMap[key] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
err := setSetting(key, value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return allSetting, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) ResetSettings() error {
|
2026-04-02 08:15:07 +00:00
|
|
|
// Delete the JSON settings file
|
|
|
|
|
err := os.Remove(config.GetSettingPath())
|
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
2023-04-21 15:30:14 +00:00
|
|
|
return err
|
|
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
// Reset certificate settings to empty
|
|
|
|
|
if err := s.SetCertFile(""); err != nil {
|
|
|
|
|
return err
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
return s.SetKeyFile("")
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) saveSetting(key string, value string) error {
|
2026-04-02 08:15:07 +00:00
|
|
|
settings, err := loadSettings()
|
|
|
|
|
if err != nil {
|
2023-02-09 19:18:06 +00:00
|
|
|
return err
|
|
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
settings[key] = value
|
|
|
|
|
return saveSettings(settings)
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) getString(key string) (string, error) {
|
2026-04-02 08:15:07 +00:00
|
|
|
settings, err := loadSettings()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
value, ok := settings[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
defaultValue, hasDefault := defaultValueMap[key]
|
|
|
|
|
if !hasDefault {
|
2023-02-09 19:18:06 +00:00
|
|
|
return "", common.NewErrorf("key <%v> not in defaultValueMap", key)
|
|
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
return defaultValue, nil
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
return value, nil
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) setString(key string, value string) error {
|
|
|
|
|
return s.saveSetting(key, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) getBool(key string) (bool, error) {
|
|
|
|
|
str, err := s.getString(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return strconv.ParseBool(str)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) setBool(key string, value bool) error {
|
|
|
|
|
return s.setString(key, strconv.FormatBool(value))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) getInt(key string) (int, error) {
|
|
|
|
|
str, err := s.getString(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return strconv.Atoi(str)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) setInt(key string, value int) error {
|
|
|
|
|
return s.setString(key, strconv.Itoa(value))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetXrayConfigTemplate() (string, error) {
|
2026-04-02 08:15:07 +00:00
|
|
|
config, err := getXrayTemplateConfigFromDB()
|
|
|
|
|
if err != nil {
|
|
|
|
|
// If not in DB, return the embedded default
|
|
|
|
|
return xrayTemplateConfig, nil
|
|
|
|
|
}
|
|
|
|
|
return config, nil
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 20:43:17 +00:00
|
|
|
func (s *SettingService) GetXrayOutboundTestUrl() (string, error) {
|
|
|
|
|
return s.getString("xrayOutboundTestUrl")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetXrayOutboundTestUrl(url string) error {
|
|
|
|
|
return s.setString("xrayOutboundTestUrl", url)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetListen() (string, error) {
|
|
|
|
|
return s.getString("webListen")
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-30 15:35:36 +00:00
|
|
|
func (s *SettingService) SetListen(ip string) error {
|
|
|
|
|
return s.setString("webListen", ip)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-30 20:51:14 +00:00
|
|
|
func (s *SettingService) GetWebDomain() (string, error) {
|
|
|
|
|
return s.getString("webDomain")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:39:39 +00:00
|
|
|
func (s *SettingService) SetWebDomain(domain string) error {
|
|
|
|
|
return s.setString("webDomain", domain)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetTgBotToken() (string, error) {
|
|
|
|
|
return s.getString("tgBotToken")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetTgBotToken(token string) error {
|
|
|
|
|
return s.setString("tgBotToken", token)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-02 09:42:07 +00:00
|
|
|
func (s *SettingService) GetTgBotProxy() (string, error) {
|
|
|
|
|
return s.getString("tgBotProxy")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetTgBotProxy(token string) error {
|
|
|
|
|
return s.setString("tgBotProxy", token)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-17 08:59:42 +00:00
|
|
|
func (s *SettingService) GetTgBotAPIServer() (string, error) {
|
|
|
|
|
return s.getString("tgBotAPIServer")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetTgBotAPIServer(token string) error {
|
|
|
|
|
return s.setString("tgBotAPIServer", token)
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 16:07:49 +00:00
|
|
|
func (s *SettingService) GetTgBotChatId() (string, error) {
|
|
|
|
|
return s.getString("tgBotChatId")
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-17 16:07:49 +00:00
|
|
|
func (s *SettingService) SetTgBotChatId(chatIds string) error {
|
|
|
|
|
return s.setString("tgBotChatId", chatIds)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-07 09:55:59 +00:00
|
|
|
func (s *SettingService) GetTgbotEnabled() (bool, error) {
|
2023-03-17 16:07:49 +00:00
|
|
|
return s.getBool("tgBotEnable")
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
2024-07-07 09:55:59 +00:00
|
|
|
func (s *SettingService) SetTgbotEnabled(value bool) error {
|
2023-02-09 19:18:06 +00:00
|
|
|
return s.setBool("tgBotEnable", value)
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 16:07:49 +00:00
|
|
|
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
|
|
|
|
return s.getString("tgRunTime")
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetTgbotRuntime(time string) error {
|
|
|
|
|
return s.setString("tgRunTime", time)
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 16:07:49 +00:00
|
|
|
func (s *SettingService) GetTgBotBackup() (bool, error) {
|
|
|
|
|
return s.getBool("tgBotBackup")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-17 15:41:16 +00:00
|
|
|
func (s *SettingService) GetTgBotLoginNotify() (bool, error) {
|
|
|
|
|
return s.getBool("tgBotLoginNotify")
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-17 16:07:49 +00:00
|
|
|
func (s *SettingService) GetTgCpu() (int, error) {
|
|
|
|
|
return s.getInt("tgCpu")
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-20 14:45:20 +00:00
|
|
|
func (s *SettingService) GetTgLang() (string, error) {
|
|
|
|
|
return s.getString("tgLang")
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
func (s *SettingService) GetTwoFactorEnable() (bool, error) {
|
|
|
|
|
return s.getBool("twoFactorEnable")
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 09:25:25 +00:00
|
|
|
func (s *SettingService) SetTwoFactorEnable(value bool) error {
|
|
|
|
|
return s.setBool("twoFactorEnable", value)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 14:20:58 +00:00
|
|
|
func (s *SettingService) GetTwoFactorToken() (string, error) {
|
|
|
|
|
return s.getString("twoFactorToken")
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 09:25:25 +00:00
|
|
|
func (s *SettingService) SetTwoFactorToken(value string) error {
|
|
|
|
|
return s.setString("twoFactorToken", value)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetPort() (int, error) {
|
|
|
|
|
return s.getInt("webPort")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetPort(port int) error {
|
|
|
|
|
return s.setInt("webPort", port)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-25 09:35:27 +00:00
|
|
|
func (s *SettingService) SetCertFile(webCertFile string) error {
|
|
|
|
|
return s.setString("webCertFile", webCertFile)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetCertFile() (string, error) {
|
|
|
|
|
return s.getString("webCertFile")
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-25 09:35:27 +00:00
|
|
|
func (s *SettingService) SetKeyFile(webKeyFile string) error {
|
|
|
|
|
return s.setString("webKeyFile", webKeyFile)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetKeyFile() (string, error) {
|
|
|
|
|
return s.getString("webKeyFile")
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-09 19:43:18 +00:00
|
|
|
func (s *SettingService) GetExpireDiff() (int, error) {
|
|
|
|
|
return s.getInt("expireDiff")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetTrafficDiff() (int, error) {
|
|
|
|
|
return s.getInt("trafficDiff")
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-25 11:30:21 +00:00
|
|
|
func (s *SettingService) GetSessionMaxAge() (int, error) {
|
|
|
|
|
return s.getInt("sessionMaxAge")
|
2023-04-09 19:43:18 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-08 19:31:17 +00:00
|
|
|
func (s *SettingService) GetRemarkModel() (string, error) {
|
|
|
|
|
return s.getString("remarkModel")
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetSecret() ([]byte, error) {
|
|
|
|
|
secret, err := s.getString("secret")
|
|
|
|
|
if secret == defaultValueMap["secret"] {
|
|
|
|
|
err := s.saveSetting("secret", secret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warning("save secret failed:", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return []byte(secret), err
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-25 08:15:06 +00:00
|
|
|
func (s *SettingService) SetBasePath(basePath string) error {
|
|
|
|
|
if !strings.HasPrefix(basePath, "/") {
|
|
|
|
|
basePath = "/" + basePath
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasSuffix(basePath, "/") {
|
|
|
|
|
basePath += "/"
|
|
|
|
|
}
|
|
|
|
|
return s.setString("webBasePath", basePath)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
func (s *SettingService) GetBasePath() (string, error) {
|
|
|
|
|
basePath, err := s.getString("webBasePath")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(basePath, "/") {
|
|
|
|
|
basePath = "/" + basePath
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasSuffix(basePath, "/") {
|
|
|
|
|
basePath += "/"
|
|
|
|
|
}
|
|
|
|
|
return basePath, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetTimeLocation() (*time.Location, error) {
|
|
|
|
|
l, err := s.getString("timeLocation")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
location, err := time.LoadLocation(l)
|
|
|
|
|
if err != nil {
|
|
|
|
|
defaultLocation := defaultValueMap["timeLocation"]
|
|
|
|
|
logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation)
|
|
|
|
|
return time.LoadLocation(defaultLocation)
|
|
|
|
|
}
|
|
|
|
|
return location, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-22 14:36:34 +00:00
|
|
|
func (s *SettingService) GetSubEnable() (bool, error) {
|
|
|
|
|
return s.getBool("subEnable")
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 11:56:04 +00:00
|
|
|
func (s *SettingService) GetSubJsonEnable() (bool, error) {
|
|
|
|
|
return s.getBool("subJsonEnable")
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-15 07:16:59 +00:00
|
|
|
func (s *SettingService) GetSubTitle() (string, error) {
|
|
|
|
|
return s.getString("subTitle")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 22:06:01 +00:00
|
|
|
func (s *SettingService) GetSubSupportUrl() (string, error) {
|
|
|
|
|
return s.getString("subSupportUrl")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubProfileUrl() (string, error) {
|
|
|
|
|
return s.getString("subProfileUrl")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubAnnounce() (string, error) {
|
|
|
|
|
return s.getString("subAnnounce")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubEnableRouting() (bool, error) {
|
|
|
|
|
return s.getBool("subEnableRouting")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubRoutingRules() (string, error) {
|
|
|
|
|
return s.getString("subRoutingRules")
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-22 14:36:34 +00:00
|
|
|
func (s *SettingService) GetSubListen() (string, error) {
|
|
|
|
|
return s.getString("subListen")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubPort() (int, error) {
|
|
|
|
|
return s.getInt("subPort")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubPath() (string, error) {
|
2024-02-21 10:47:52 +00:00
|
|
|
return s.getString("subPath")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubJsonPath() (string, error) {
|
|
|
|
|
return s.getString("subJsonPath")
|
2023-05-22 14:36:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubDomain() (string, error) {
|
|
|
|
|
return s.getString("subDomain")
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 12:10:27 +00:00
|
|
|
func (s *SettingService) SetSubCertFile(subCertFile string) error {
|
|
|
|
|
return s.setString("subCertFile", subCertFile)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-22 14:36:34 +00:00
|
|
|
func (s *SettingService) GetSubCertFile() (string, error) {
|
|
|
|
|
return s.getString("subCertFile")
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 12:10:27 +00:00
|
|
|
func (s *SettingService) SetSubKeyFile(subKeyFile string) error {
|
|
|
|
|
return s.setString("subKeyFile", subKeyFile)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-22 14:36:34 +00:00
|
|
|
func (s *SettingService) GetSubKeyFile() (string, error) {
|
|
|
|
|
return s.getString("subKeyFile")
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 10:47:52 +00:00
|
|
|
func (s *SettingService) GetSubUpdates() (string, error) {
|
|
|
|
|
return s.getString("subUpdates")
|
2023-05-22 14:36:34 +00:00
|
|
|
}
|
|
|
|
|
|
2023-08-26 13:24:01 +00:00
|
|
|
func (s *SettingService) GetSubEncrypt() (bool, error) {
|
|
|
|
|
return s.getBool("subEncrypt")
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-26 11:41:12 +00:00
|
|
|
func (s *SettingService) GetSubShowInfo() (bool, error) {
|
|
|
|
|
return s.getBool("subShowInfo")
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 06:27:21 +00:00
|
|
|
func (s *SettingService) GetPageSize() (int, error) {
|
|
|
|
|
return s.getInt("pageSize")
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-08 16:18:51 +00:00
|
|
|
func (s *SettingService) GetSubURI() (string, error) {
|
|
|
|
|
return s.getString("subURI")
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 10:47:52 +00:00
|
|
|
func (s *SettingService) GetSubJsonURI() (string, error) {
|
|
|
|
|
return s.getString("subJsonURI")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubJsonFragment() (string, error) {
|
|
|
|
|
return s.getString("subJsonFragment")
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-17 07:33:44 +00:00
|
|
|
func (s *SettingService) GetSubJsonNoises() (string, error) {
|
|
|
|
|
return s.getString("subJsonNoises")
|
2024-08-29 09:27:43 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-12 16:14:51 +00:00
|
|
|
func (s *SettingService) GetSubJsonMux() (string, error) {
|
|
|
|
|
return s.getString("subJsonMux")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubJsonRules() (string, error) {
|
|
|
|
|
return s.getString("subJsonRules")
|
|
|
|
|
}
|
|
|
|
|
|
feat: add Clash YAML subscription endpoint with template injection
Add /clash/:subid endpoint that returns complete Clash YAML config.
User provides full template (DNS, routing, proxy-groups, rules) in
settings, panel generates proxies from inbound/client data and injects
via proxies: [] placeholder replacement.
- New SubClashService reads template, generates vmess/vless/trojan/ss
proxy entries with transport (ws/grpc/h2/tcp/httpupgrade), TLS, and
Reality support
- Settings: subClashEnable, subClashPath, subClashURI, subClashTemplate
- UI: Clash settings tab, QR code on subpage, Desktop dropdown with
clash-verge:// deep link preferring Clash URL
- Version bump to v1.5.2-beta
2026-04-24 03:25:10 +00:00
|
|
|
func (s *SettingService) GetSubClashEnable() (bool, error) {
|
|
|
|
|
return s.getBool("subClashEnable")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubClashPath() (string, error) {
|
|
|
|
|
return s.getString("subClashPath")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubClashURI() (string, error) {
|
|
|
|
|
return s.getString("subClashURI")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetSubClashTemplate() (string, error) {
|
|
|
|
|
return s.getString("subClashTemplate")
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-02 08:32:21 +00:00
|
|
|
func (s *SettingService) GetDatepicker() (string, error) {
|
|
|
|
|
return s.getString("datepicker")
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 06:27:21 +00:00
|
|
|
func (s *SettingService) GetWarp() (string, error) {
|
|
|
|
|
return s.getString("warp")
|
|
|
|
|
}
|
2024-03-10 21:31:24 +00:00
|
|
|
|
2024-01-11 06:27:21 +00:00
|
|
|
func (s *SettingService) SetWarp(data string) error {
|
|
|
|
|
return s.setString("warp", data)
|
2023-12-08 16:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-22 09:45:14 +00:00
|
|
|
func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) {
|
|
|
|
|
return s.getBool("externalTrafficInformEnable")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetExternalTrafficInformEnable(value bool) error {
|
|
|
|
|
return s.setBool("externalTrafficInformEnable", value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetExternalTrafficInformURI() (string, error) {
|
|
|
|
|
return s.getString("externalTrafficInformURI")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error {
|
|
|
|
|
return s.setString("externalTrafficInformURI", InformURI)
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-12 20:53:32 +00:00
|
|
|
func (s *SettingService) GetIpLimitEnable() (bool, error) {
|
2024-03-13 07:54:41 +00:00
|
|
|
accessLogPath, err := xray.GetAccessLogPath()
|
2024-03-12 20:53:32 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
2024-03-13 07:54:41 +00:00
|
|
|
return (accessLogPath != "none" && accessLogPath != ""), nil
|
2024-03-12 20:53:32 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 12:05:29 +00:00
|
|
|
// GetLdapEnable returns whether LDAP is enabled.
|
2025-09-28 19:00:16 +00:00
|
|
|
func (s *SettingService) GetLdapEnable() (bool, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getBool("ldapEnable")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapHost() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapHost")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapPort() (int, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getInt("ldapPort")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapUseTLS() (bool, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getBool("ldapUseTLS")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapBindDN() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapBindDN")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapPassword() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapPassword")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapBaseDN() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapBaseDN")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapUserFilter() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapUserFilter")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapUserAttr() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapUserAttr")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapVlessField() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapVlessField")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapSyncCron() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapSyncCron")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapFlagField() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapFlagField")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapTruthyValues() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapTruthyValues")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapInvertFlag() (bool, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getBool("ldapInvertFlag")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapInboundTags() (string, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getString("ldapInboundTags")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapAutoCreate() (bool, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getBool("ldapAutoCreate")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapAutoDelete() (bool, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getBool("ldapAutoDelete")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapDefaultTotalGB() (int, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getInt("ldapDefaultTotalGB")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getInt("ldapDefaultExpiryDays")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
|
2025-10-21 11:02:55 +00:00
|
|
|
return s.getInt("ldapDefaultLimitIP")
|
2025-09-28 19:00:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 12:18:48 +00:00
|
|
|
func (s *SettingService) GetTurnstileSiteKey() (string, error) {
|
|
|
|
|
return s.getString("turnstileSiteKey")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 15:49:30 +00:00
|
|
|
func (s *SettingService) GetTurnstileSecretKey() (string, error) {
|
|
|
|
|
return s.getString("turnstileSecretKey")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 18:02:25 +00:00
|
|
|
func (s *SettingService) SetTurnstileSecretKey(value string) error {
|
|
|
|
|
return s.setString("turnstileSecretKey", value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 11:17:33 +00:00
|
|
|
func (s *SettingService) GetBackupEnabled() (bool, error) {
|
|
|
|
|
return s.getBool("backupEnabled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetBackupFrequency() (string, error) {
|
|
|
|
|
return s.getString("backupFrequency")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetBackupHour() (int, error) {
|
|
|
|
|
return s.getInt("backupHour")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetBackupMaxCount() (int, error) {
|
|
|
|
|
return s.getInt("backupMaxCount")
|
|
|
|
|
}
|
2026-04-26 11:10:25 +00:00
|
|
|
|
2026-04-04 06:45:31 +00:00
|
|
|
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting, presentKeys map[string]struct{}) error {
|
2023-02-09 19:18:06 +00:00
|
|
|
if err := allSetting.CheckValid(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 08:15:07 +00:00
|
|
|
settings, err := loadSettings()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 19:18:06 +00:00
|
|
|
v := reflect.ValueOf(allSetting).Elem()
|
2026-03-04 12:05:29 +00:00
|
|
|
t := reflect.TypeFor[entity.AllSetting]()
|
2023-02-09 19:18:06 +00:00
|
|
|
fields := reflect_util.GetFields(t)
|
|
|
|
|
for _, field := range fields {
|
|
|
|
|
key := field.Tag.Get("json")
|
2026-04-02 18:02:25 +00:00
|
|
|
if key == "-" || key == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-04-04 06:45:31 +00:00
|
|
|
if presentKeys != nil {
|
|
|
|
|
if _, ok := presentKeys[key]; !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-09 19:18:06 +00:00
|
|
|
fieldV := v.FieldByName(field.Name)
|
2026-04-02 08:15:07 +00:00
|
|
|
settings[key] = fmt.Sprint(fieldV.Interface())
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
2026-04-03 01:53:20 +00:00
|
|
|
// DBPassword uses json:"-" to avoid leaking to frontend, handle it via form tag
|
|
|
|
|
if allSetting.DBPassword != "" {
|
|
|
|
|
settings["dbPassword"] = allSetting.DBPassword
|
|
|
|
|
}
|
2026-04-02 08:15:07 +00:00
|
|
|
return saveSettings(settings)
|
2023-02-09 19:18:06 +00:00
|
|
|
}
|
2023-12-04 18:20:46 +00:00
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
func (s *SettingService) GetDefaultXrayConfig() (any, error) {
|
|
|
|
|
var jsonData any
|
2023-12-04 18:20:46 +00:00
|
|
|
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return jsonData, nil
|
|
|
|
|
}
|
2023-12-08 16:18:51 +00:00
|
|
|
|
2026-02-11 14:33:53 +00:00
|
|
|
func extractHostname(host string) string {
|
2026-02-14 21:49:19 +00:00
|
|
|
h, _, err := net.SplitHostPort(host)
|
|
|
|
|
// Err is not nil means host does not contain port
|
|
|
|
|
if err != nil {
|
|
|
|
|
h = host
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ip := net.ParseIP(h)
|
|
|
|
|
// If it's not an IP, return as is
|
|
|
|
|
if ip == nil {
|
|
|
|
|
return h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If it's an IPv4, return as is
|
|
|
|
|
if ip.To4() != nil {
|
|
|
|
|
return h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IPv6 needs bracketing
|
|
|
|
|
return "[" + h + "]"
|
2026-02-11 14:33:53 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
|
|
|
|
type settingFunc func() (any, error)
|
2023-12-08 16:18:51 +00:00
|
|
|
settings := map[string]settingFunc{
|
2026-04-24 06:43:44 +00:00
|
|
|
"expireDiff": func() (any, error) { return s.GetExpireDiff() },
|
|
|
|
|
"trafficDiff": func() (any, error) { return s.GetTrafficDiff() },
|
|
|
|
|
"pageSize": func() (any, error) { return s.GetPageSize() },
|
|
|
|
|
"defaultCert": func() (any, error) { return s.GetCertFile() },
|
|
|
|
|
"defaultKey": func() (any, error) { return s.GetKeyFile() },
|
|
|
|
|
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
|
feat: add Clash YAML subscription endpoint with template injection
Add /clash/:subid endpoint that returns complete Clash YAML config.
User provides full template (DNS, routing, proxy-groups, rules) in
settings, panel generates proxies from inbound/client data and injects
via proxies: [] placeholder replacement.
- New SubClashService reads template, generates vmess/vless/trojan/ss
proxy entries with transport (ws/grpc/h2/tcp/httpupgrade), TLS, and
Reality support
- Settings: subClashEnable, subClashPath, subClashURI, subClashTemplate
- UI: Clash settings tab, QR code on subpage, Desktop dropdown with
clash-verge:// deep link preferring Clash URL
- Version bump to v1.5.2-beta
2026-04-24 03:25:10 +00:00
|
|
|
"subEnable": func() (any, error) { return s.GetSubEnable() },
|
|
|
|
|
"subJsonEnable": func() (any, error) { return s.GetSubJsonEnable() },
|
|
|
|
|
"subClashEnable": func() (any, error) { return s.GetSubClashEnable() },
|
|
|
|
|
"subTitle": func() (any, error) { return s.GetSubTitle() },
|
|
|
|
|
"subURI": func() (any, error) { return s.GetSubURI() },
|
|
|
|
|
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
|
|
|
|
|
"subClashURI": func() (any, error) { return s.GetSubClashURI() },
|
2026-04-24 06:43:44 +00:00
|
|
|
"remarkModel": func() (any, error) { return s.GetRemarkModel() },
|
|
|
|
|
"datepicker": func() (any, error) { return s.GetDatepicker() },
|
|
|
|
|
"ipLimitEnable": func() (any, error) { return s.GetIpLimitEnable() },
|
2023-12-08 16:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-12 19:13:51 +00:00
|
|
|
result := make(map[string]any)
|
2023-12-08 16:18:51 +00:00
|
|
|
|
|
|
|
|
for key, fn := range settings {
|
|
|
|
|
value, err := fn()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
result[key] = value
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 11:56:04 +00:00
|
|
|
subEnable := result["subEnable"].(bool)
|
|
|
|
|
subJsonEnable := false
|
|
|
|
|
if v, ok := result["subJsonEnable"]; ok {
|
|
|
|
|
if b, ok2 := v.(bool); ok2 {
|
|
|
|
|
subJsonEnable = b
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: add Clash YAML subscription endpoint with template injection
Add /clash/:subid endpoint that returns complete Clash YAML config.
User provides full template (DNS, routing, proxy-groups, rules) in
settings, panel generates proxies from inbound/client data and injects
via proxies: [] placeholder replacement.
- New SubClashService reads template, generates vmess/vless/trojan/ss
proxy entries with transport (ws/grpc/h2/tcp/httpupgrade), TLS, and
Reality support
- Settings: subClashEnable, subClashPath, subClashURI, subClashTemplate
- UI: Clash settings tab, QR code on subpage, Desktop dropdown with
clash-verge:// deep link preferring Clash URL
- Version bump to v1.5.2-beta
2026-04-24 03:25:10 +00:00
|
|
|
subClashEnable := false
|
|
|
|
|
if v, ok := result["subClashEnable"]; ok {
|
|
|
|
|
if b, ok2 := v.(bool); ok2 {
|
|
|
|
|
subClashEnable = b
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (subEnable && result["subURI"].(string) == "") || (subJsonEnable && result["subJsonURI"].(string) == "") || (subClashEnable && result["subClashURI"].(string) == "") {
|
2023-12-08 16:18:51 +00:00
|
|
|
subURI := ""
|
2025-03-15 07:16:59 +00:00
|
|
|
subTitle, _ := s.GetSubTitle()
|
2023-12-08 16:18:51 +00:00
|
|
|
subPort, _ := s.GetSubPort()
|
|
|
|
|
subPath, _ := s.GetSubPath()
|
2024-02-21 10:47:52 +00:00
|
|
|
subJsonPath, _ := s.GetSubJsonPath()
|
feat: add Clash YAML subscription endpoint with template injection
Add /clash/:subid endpoint that returns complete Clash YAML config.
User provides full template (DNS, routing, proxy-groups, rules) in
settings, panel generates proxies from inbound/client data and injects
via proxies: [] placeholder replacement.
- New SubClashService reads template, generates vmess/vless/trojan/ss
proxy entries with transport (ws/grpc/h2/tcp/httpupgrade), TLS, and
Reality support
- Settings: subClashEnable, subClashPath, subClashURI, subClashTemplate
- UI: Clash settings tab, QR code on subpage, Desktop dropdown with
clash-verge:// deep link preferring Clash URL
- Version bump to v1.5.2-beta
2026-04-24 03:25:10 +00:00
|
|
|
subClashPath, _ := s.GetSubClashPath()
|
2023-12-08 16:18:51 +00:00
|
|
|
subDomain, _ := s.GetSubDomain()
|
|
|
|
|
subKeyFile, _ := s.GetSubKeyFile()
|
|
|
|
|
subCertFile, _ := s.GetSubCertFile()
|
|
|
|
|
subTLS := false
|
|
|
|
|
if subKeyFile != "" && subCertFile != "" {
|
|
|
|
|
subTLS = true
|
|
|
|
|
}
|
|
|
|
|
if subDomain == "" {
|
2026-02-11 14:33:53 +00:00
|
|
|
subDomain = extractHostname(host)
|
2023-12-08 16:18:51 +00:00
|
|
|
}
|
|
|
|
|
if subTLS {
|
|
|
|
|
subURI = "https://"
|
|
|
|
|
} else {
|
|
|
|
|
subURI = "http://"
|
|
|
|
|
}
|
|
|
|
|
if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) {
|
|
|
|
|
subURI += subDomain
|
|
|
|
|
} else {
|
|
|
|
|
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
|
|
|
|
}
|
2025-09-18 11:56:04 +00:00
|
|
|
if subEnable && result["subURI"].(string) == "" {
|
2024-02-21 10:47:52 +00:00
|
|
|
result["subURI"] = subURI + subPath
|
|
|
|
|
}
|
2025-03-15 07:16:59 +00:00
|
|
|
if result["subTitle"].(string) == "" {
|
|
|
|
|
result["subTitle"] = subTitle
|
|
|
|
|
}
|
2025-09-18 11:56:04 +00:00
|
|
|
if subJsonEnable && result["subJsonURI"].(string) == "" {
|
2024-02-21 10:47:52 +00:00
|
|
|
result["subJsonURI"] = subURI + subJsonPath
|
2023-12-08 16:18:51 +00:00
|
|
|
}
|
feat: add Clash YAML subscription endpoint with template injection
Add /clash/:subid endpoint that returns complete Clash YAML config.
User provides full template (DNS, routing, proxy-groups, rules) in
settings, panel generates proxies from inbound/client data and injects
via proxies: [] placeholder replacement.
- New SubClashService reads template, generates vmess/vless/trojan/ss
proxy entries with transport (ws/grpc/h2/tcp/httpupgrade), TLS, and
Reality support
- Settings: subClashEnable, subClashPath, subClashURI, subClashTemplate
- UI: Clash settings tab, QR code on subpage, Desktop dropdown with
clash-verge:// deep link preferring Clash URL
- Version bump to v1.5.2-beta
2026-04-24 03:25:10 +00:00
|
|
|
if subClashEnable && result["subClashURI"].(string) == "" {
|
|
|
|
|
result["subClashURI"] = subURI + subClashPath
|
|
|
|
|
}
|
2023-12-08 16:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
2026-04-03 01:24:18 +00:00
|
|
|
|
|
|
|
|
func (s *SettingService) GetDBType() (string, error) {
|
|
|
|
|
return s.getString("dbType")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetDBType(value string) error {
|
|
|
|
|
return s.setString("dbType", value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetDBHost() (string, error) {
|
|
|
|
|
return s.getString("dbHost")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetDBHost(value string) error {
|
|
|
|
|
return s.setString("dbHost", value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetDBPort() (string, error) {
|
|
|
|
|
return s.getString("dbPort")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetDBPort(value string) error {
|
|
|
|
|
return s.setString("dbPort", value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetDBUser() (string, error) {
|
|
|
|
|
return s.getString("dbUser")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetDBUser(value string) error {
|
|
|
|
|
return s.setString("dbUser", value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetDBPassword() (string, error) {
|
|
|
|
|
return s.getString("dbPassword")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetDBPassword(value string) error {
|
|
|
|
|
return s.setString("dbPassword", value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) GetDBName() (string, error) {
|
|
|
|
|
return s.getString("dbName")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *SettingService) SetDBName(value string) error {
|
|
|
|
|
return s.setString("dbName", value)
|
|
|
|
|
}
|