Merge branch 'main' into bash

This commit is contained in:
Sanaei 2026-05-21 16:34:09 +02:00 committed by GitHub
commit b5cb069a07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 419 additions and 158 deletions

View file

@ -57,6 +57,11 @@ func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true"
}
// IsSkipHSTS returns true if skipping HSTS mode is enabled via the XUI_SKIP_HSTS environment variable.
func IsSkipHSTS() bool {
return os.Getenv("XUI_SKIP_HSTS") == "true"
}
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER")

View file

@ -11,6 +11,7 @@ import (
"os"
"path"
"slices"
"strconv"
"strings"
"time"
@ -198,6 +199,36 @@ func runSeeders(isUsersEmpty bool) error {
return nil
}
// normalizeClientJSONFields coerces loosely-typed numeric fields in a raw
// settings.clients entry so json.Unmarshal into model.Client doesn't fail
// when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings
// drop the key so the field falls back to its zero value.
func normalizeClientJSONFields(obj map[string]any) {
normalizeInt := func(key string) {
raw, exists := obj[key]
if !exists {
return
}
s, ok := raw.(string)
if !ok {
return
}
trimmed := strings.ReplaceAll(strings.TrimSpace(s), " ", "")
if trimmed == "" {
delete(obj, key)
return
}
if n, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
obj[key] = n
} else {
delete(obj, key)
}
}
for _, k := range []string{"tgId", "limitIp", "totalGB", "expiryTime", "reset", "created_at", "updated_at"} {
normalizeInt(k)
}
}
func seedClientsFromInboundJSON() error {
var inbounds []model.Inbound
if err := db.Find(&inbounds).Error; err != nil {
@ -226,12 +257,15 @@ func seedClientsFromInboundJSON() error {
if !ok {
continue
}
normalizeClientJSONFields(obj)
blob, err := json.Marshal(obj)
if err != nil {
continue
}
var c model.Client
if err := json.Unmarshal(blob, &c); err != nil {
log.Printf("ClientsTable seed: skip client in inbound %d (unmarshal failed): %v; payload=%s",
inbound.Id, err, string(blob))
continue
}
email := strings.TrimSpace(c.Email)

View file

@ -27,7 +27,7 @@
"eslint": "^10.3.0",
"eslint-plugin-vue": "^10.9.1",
"globals": "^17.6.0",
"vite": "^8.0.11",
"vite": "^8.0.14",
"vue-eslint-parser": "^10.4.0"
},
"engines": {
@ -610,9 +610,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"version": "0.132.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"dev": true,
"license": "MIT",
"funding": {
@ -620,9 +620,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"cpu": [
"arm64"
],
@ -637,9 +637,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"cpu": [
"arm64"
],
@ -654,9 +654,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"cpu": [
"x64"
],
@ -671,9 +671,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"cpu": [
"x64"
],
@ -688,9 +688,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"cpu": [
"arm"
],
@ -705,16 +705,13 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -725,16 +722,13 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -745,16 +739,13 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -765,16 +756,13 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -785,16 +773,13 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -805,16 +790,13 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -825,9 +807,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"cpu": [
"arm64"
],
@ -842,9 +824,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"cpu": [
"wasm32"
],
@ -861,9 +843,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"cpu": [
"arm64"
],
@ -878,9 +860,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"cpu": [
"x64"
],
@ -2202,9 +2184,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2226,9 +2205,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2250,9 +2226,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2274,9 +2247,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2623,9 +2593,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
@ -2642,7 +2612,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@ -2715,13 +2685,13 @@
"license": "MIT"
},
"node_modules/rolldown": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.130.0",
"@oxc-project/types": "=0.132.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@ -2731,21 +2701,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.1"
"@rolldown/binding-android-arm64": "1.0.2",
"@rolldown/binding-darwin-arm64": "1.0.2",
"@rolldown/binding-darwin-x64": "1.0.2",
"@rolldown/binding-freebsd-x64": "1.0.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
"@rolldown/binding-linux-arm64-musl": "1.0.2",
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
"@rolldown/binding-linux-x64-gnu": "1.0.2",
"@rolldown/binding-linux-x64-musl": "1.0.2",
"@rolldown/binding-openharmony-arm64": "1.0.2",
"@rolldown/binding-wasm32-wasi": "1.0.2",
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
"@rolldown/binding-win32-x64-msvc": "1.0.2"
}
},
"node_modules/scroll-into-view-if-needed": {
@ -2957,16 +2927,16 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.14",
"rolldown": "1.0.1",
"postcss": "^8.5.15",
"rolldown": "1.0.2",
"tinyglobby": "^0.2.16"
},
"bin": {

View file

@ -34,7 +34,7 @@
"eslint": "^10.3.0",
"eslint-plugin-vue": "^10.9.1",
"globals": "^17.6.0",
"vite": "^8.0.11",
"vite": "^8.0.14",
"vue-eslint-parser": "^10.4.0"
},
"overrides": {

View file

@ -748,6 +748,9 @@ export class SockoptStreamSettings extends CommonClass {
penetrate = false,
addressPortStrategy = Address_Port_Strategy.NONE,
trustedXForwardedFor = [],
mark = 0,
interfaceName = "",
) {
super();
this.dialerProxy = dialerProxy;
@ -757,6 +760,9 @@ export class SockoptStreamSettings extends CommonClass {
this.penetrate = penetrate;
this.addressPortStrategy = addressPortStrategy;
this.trustedXForwardedFor = trustedXForwardedFor;
this.mark = mark;
this.interfaceName = interfaceName;
}
static fromJson(json = {}) {
@ -768,7 +774,9 @@ export class SockoptStreamSettings extends CommonClass {
json.tcpMptcp,
json.penetrate,
json.addressPortStrategy,
json.trustedXForwardedFor || []
json.trustedXForwardedFor || [],
json.mark ?? 0,
json.interface ?? "",
);
}
@ -779,7 +787,9 @@ export class SockoptStreamSettings extends CommonClass {
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
tcpMptcp: this.tcpMptcp,
penetrate: this.penetrate,
addressPortStrategy: this.addressPortStrategy
addressPortStrategy: this.addressPortStrategy,
mark: this.mark,
interface: this.interfaceName,
};
if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
result.trustedXForwardedFor = this.trustedXForwardedFor;
@ -1138,8 +1148,12 @@ export class StreamSettings extends CommonClass {
}
static fromJson(json = {}) {
// Xray-core supports both "xhttpSettings" and "splithttpSettings" (backward-compat alias)
const xhttpJson = json.xhttpSettings ?? json.splithttpSettings;
// Normalize "splithttp" network name to "xhttp" for internal consistency
const network = json.network === 'splithttp' ? 'xhttp' : json.network;
return new StreamSettings(
json.network,
network,
json.security,
TlsStreamSettings.fromJson(json.tlsSettings),
RealityStreamSettings.fromJson(json.realitySettings),
@ -1148,7 +1162,7 @@ export class StreamSettings extends CommonClass {
WsStreamSettings.fromJson(json.wsSettings),
GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
xHTTPStreamSettings.fromJson(xhttpJson),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
FinalMaskStreamSettings.fromJson(json.finalmask),
SockoptStreamSettings.fromJson(json.sockopt),
@ -1379,12 +1393,28 @@ export class Outbound extends CommonClass {
} else if (network === 'httpupgrade') {
stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host);
} else if (network === 'xhttp') {
// xHTTPStreamSettings positional args are (path, host, headers, ..., mode);
// passing `json.mode` as the 3rd argument used to land in the `headers`
// slot, dropping the mode on the floor. Build the object and set mode
// explicitly to avoid that.
const xh = new xHTTPStreamSettings(json.path, json.host);
if (json.mode) xh.mode = json.mode;
if (json.type && !json.mode) xh.mode = json.type;
// Padding / obfuscation — sing-box families use x_padding_bytes,
// while the extra block carries xPaddingBytes.
if (json.x_padding_bytes && !json.xPaddingBytes) json.xPaddingBytes = json.x_padding_bytes;
if (typeof json.xPaddingBytes === 'string' && json.xPaddingBytes) xh.xPaddingBytes = json.xPaddingBytes;
if (json.xPaddingObfsMode === true) {
xh.xPaddingObfsMode = true;
["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => {
if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
});
}
// Bidirectional string fields carried in the extra block
const xFields = ["sessionPlacement", "sessionKey", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes"];
xFields.forEach(k => {
if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
});
// Headers — VMess extra emits them as a {name: value} map
if (json.headers && typeof json.headers === 'object' && !Array.isArray(json.headers)) {
xh.headers = Object.entries(json.headers).map(([name, value]) => ({ name, value }));
}
stream.xhttp = xh;
}
@ -1455,6 +1485,16 @@ export class Outbound extends CommonClass {
["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => {
if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
});
if (!xh.mode && typeof extra.mode === 'string' && extra.mode) xh.mode = extra.mode;
// Bidirectional string fields carried inside the extra block
const xFields = ["sessionPlacement", "sessionKey", "seqPlacement", "seqKey", "uplinkDataPlacement", "uplinkDataKey", "scMaxEachPostBytes"];
xFields.forEach(k => {
if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
});
// Headers — extra emits them as a {name: value} map
if (extra.headers && typeof extra.headers === 'object' && !Array.isArray(extra.headers)) {
xh.headers = Object.entries(extra.headers).map(([name, value]) => ({ name, value }));
}
} catch (_) { /* ignore malformed extra */ }
}
stream.xhttp = xh;
@ -1997,6 +2037,28 @@ Outbound.VLESSSettings = class extends CommonClass {
}
static fromJson(json = {}) {
// Handle v2rayN-style nested vnext array (standard Xray JSON format)
if (!ObjectUtil.isArrEmpty(json.vnext)) {
const v = json.vnext[0] || {};
const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
const saved = json.testseed;
const testseed = (Array.isArray(saved)
&& saved.length === 4
&& saved.every(v => Number.isInteger(v) && v > 0))
? saved
: [];
return new Outbound.VLESSSettings(
v.address,
v.port,
u.id,
u.flow,
u.encryption,
json.reverse?.tag || '',
ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
json.testpre || 0,
testseed,
);
}
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
const saved = json.testseed;
const testseed = (Array.isArray(saved)

View file

@ -43,6 +43,7 @@ const {
tgBotEnable,
expireDiff,
trafficDiff,
pageSize,
create,
update,
remove,
@ -442,6 +443,10 @@ function expiryColor(row) {
const sortState = ref({ column: null, order: null });
const paginationState = ref({ current: 1, pageSize: 20 });
watch(pageSize, (next) => {
if (next > 0) paginationState.value.pageSize = next;
}, { immediate: true });
function sortableCol(col, key) {
return {
...col,
@ -670,8 +675,9 @@ const columns = computed(() => [
</a-select>
</div>
<a-table v-if="!isMobile" :columns="columns" :data-source="sortedClients" :loading="loading" row-key="email"
:row-selection="rowSelection" :pagination="tablePagination" size="small" @change="onTableChange">
<a-table v-if="!isMobile" :columns="columns" :data-source="sortedClients" :loading="loading"
row-key="email" :row-selection="rowSelection" :pagination="tablePagination" size="small"
@change="onTableChange">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'email'">
<div class="email-cell">
@ -842,6 +848,11 @@ const columns = computed(() => [
background: var(--bg-page);
}
.clients-page :deep(.ant-pagination-options-size-changer),
.clients-page :deep(.ant-pagination-options-size-changer .ant-select-selector) {
min-width: 100px !important;
}
.clients-page.is-dark {
--bg-page: #1e1e1e;
--bg-card: #252526;
@ -874,7 +885,7 @@ const columns = computed(() => [
margin-bottom: 8px;
}
.filter-bar.mobile > * {
.filter-bar.mobile>* {
flex: 0 0 auto;
}
@ -911,11 +922,25 @@ const columns = computed(() => [
vertical-align: middle;
}
.dot-green { background: #52c41a; }
.dot-blue { background: #1677ff; }
.dot-red { background: #ff4d4f; }
.dot-orange { background: #fa8c16; }
.dot-gray { background: rgba(128, 128, 128, 0.6); }
.dot-green {
background: #52c41a;
}
.dot-blue {
background: #1677ff;
}
.dot-red {
background: #ff4d4f;
}
.dot-orange {
background: #fa8c16;
}
.dot-gray {
background: rgba(128, 128, 128, 0.6);
}
.status-tag {
margin: 0 0 0 4px;
@ -1050,8 +1075,6 @@ const columns = computed(() => [
</style>
<style>
/* AD-Vue popovers teleport their content to <body>, so scoped styles
don't reach them this block has to be unscoped. */
.client-email-list {
max-height: 280px;
min-width: 160px;
@ -1059,9 +1082,17 @@ const columns = computed(() => [
padding-right: 4px;
}
.client-email-list > div {
.client-email-list>div {
padding: 2px 0;
font-size: 12px;
white-space: nowrap;
}
.ant-select-dropdown:has(.ant-select-item-option[title$="/ page"]) {
min-width: 110px !important;
}
.ant-select-dropdown:has(.ant-select-item-option[title$="/ page"]) .ant-select-item-option-content {
white-space: nowrap;
}
</style>

View file

@ -14,6 +14,7 @@ export function useClients() {
const tgBotEnable = ref(false);
const expireDiff = ref(0);
const trafficDiff = ref(0);
const pageSize = ref(0);
async function refresh() {
loading.value = true;
@ -48,6 +49,7 @@ export function useClients() {
tgBotEnable.value = !!s.tgBotEnable;
expireDiff.value = (s.expireDiff ?? 0) * 86400000;
trafficDiff.value = (s.trafficDiff ?? 0) * 1073741824;
pageSize.value = s.pageSize ?? 0;
}
async function create(payload) {
@ -199,6 +201,7 @@ export function useClients() {
tgBotEnable,
expireDiff,
trafficDiff,
pageSize,
refresh,
create,
update,

View file

@ -19,41 +19,20 @@ import { useDatepicker } from '@/composables/useDatepicker.js';
const { t } = useI18n();
const { datepicker } = useDatepicker();
// One modal handles every protocol's info / share view because the
// legacy template did the same. The big v-if forks at the top decide
// which sub-block of the body renders:
// multi-user inbound (VMess/VLess/Trojan/SS-multi/Hysteria) per-
// client row + share links
// SS single-user connection details + share link
// WireGuard secret/peers + per-peer config download
// Mixed/HTTP/Tunnel connection details only
//
// We display links via QrPanel each link gets its own QR + copy +
// (for WireGuard configs) download button.
const props = defineProps({
open: { type: Boolean, default: false },
// Result of inbounds-page checkFallback() so the link-gen sees the
// root inbound's listen/port/security when the dbInbound is a
// domain-socket fallback (`@<name>`).
dbInbound: { type: Object, default: null },
// Index into inbound.clients to focus on for multi-user inbounds.
clientIndex: { type: Number, default: 0 },
// Sidecar config the legacy panel keyed off `app.*`.
remarkModel: { type: String, default: '-ieo' },
expireDiff: { type: Number, default: 0 },
trafficDiff: { type: Number, default: 0 },
ipLimitEnable: { type: Boolean, default: false },
tgBotEnable: { type: Boolean, default: false },
// Address of the node hosting this inbound; '' for local. Wired
// through to share/QR link generation so node-managed inbounds
// produce links that connect to the node, not the central panel.
nodeAddress: { type: String, default: '' },
subSettings: {
type: Object,
default: () => ({ enable: false, subURI: '', subJsonURI: '', subJsonEnable: false }),
},
// Email -> ts (last-online unix-ms) map fetched at the page level.
lastOnlineMap: { type: Object, default: () => ({}) },
});
@ -598,7 +577,8 @@ const showSubscriptionTab = computed(
<div v-if="inbound.settings.gateway?.length" class="info-row">
<dt>Gateway</dt>
<dd><a-tag v-for="(ip, j) in inbound.settings.gateway" :key="`tun-i-gw-${j}`" color="green"
class="value-tag">{{ ip }}</a-tag></dd>
class="value-tag">{{
ip }}</a-tag></dd>
</div>
<div v-if="inbound.settings.dns?.length" class="info-row">
<dt>DNS</dt>
@ -612,7 +592,8 @@ const showSubscriptionTab = computed(
<div v-if="inbound.settings.autoSystemRoutingTable?.length" class="info-row">
<dt>Auto system routes</dt>
<dd><a-tag v-for="(cidr, j) in inbound.settings.autoSystemRoutingTable" :key="`tun-i-rt-${j}`"
color="green">{{ cidr }}</a-tag></dd>
color="green">{{
cidr }}</a-tag></dd>
</div>
</dl>
@ -670,12 +651,101 @@ const showSubscriptionTab = computed(
<span class="account-sep">:</span>
<a-tag class="value-tag">{{ account.pass }}</a-tag>
<a-tooltip :title="t('copy')">
<a-button size="small" @click="copyText(`${account.user}:${account.pass}`)">
<template #icon>
<CopyOutlined />
</template>
<a-button size="small" type="text"
@click="copyText(`${account.user}:${account.pass}`)">
<template #icon><CopyOutlined /></template>
</a-button>
</a-tooltip>
<a-space :size="4" wrap class="share-buttons share-desktop">
<a-tooltip :title="`socks5://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`">
<a-button size="small"
@click="copyText(`socks5://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`)">
SOCKS5
</a-button>
</a-tooltip>
<a-tooltip :title="`http://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`">
<a-button size="small"
@click="copyText(`http://${dbInbound.address}:${dbInbound.port}@${account.user}:${account.pass}`)">
HTTP
</a-button>
</a-tooltip>
<a-tooltip title="https://t.me/socks?server=...&port=...&user=...&pass=...">
<a-button size="small"
@click="copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`)">
Telegram
</a-button>
</a-tooltip>
</a-space>
<a-dropdown :trigger="['click']" class="share-mobile">
<a-button size="small">
<template #icon><CopyOutlined /></template>
{{ t('copy') }}
</a-button>
<template #overlay>
<a-menu @click="({ key }) => {
const h = dbInbound.address;
const port = dbInbound.port;
if (key === 'telegram') {
copyText(`https://t.me/socks?server=${encodeURIComponent(h)}&port=${port}&user=${encodeURIComponent(account.user)}&pass=${encodeURIComponent(account.pass)}`);
} else {
copyText(`${key}://${h}:${port}@${account.user}:${account.pass}`);
}
}">
<a-menu-item key="socks5">SOCKS5</a-menu-item>
<a-menu-item key="http">HTTP</a-menu-item>
<a-menu-item key="telegram">Telegram</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</dd>
</div>
</template>
<template v-if="inbound.settings.auth === 'noauth'">
<div class="info-row">
<dt>{{ t('copy') }}</dt>
<dd>
<a-space :size="4" wrap class="share-buttons share-desktop">
<a-tooltip :title="`socks5://${dbInbound.address}:${dbInbound.port}`">
<a-button size="small"
@click="copyText(`socks5://${dbInbound.address}:${dbInbound.port}`)">
SOCKS5
</a-button>
</a-tooltip>
<a-tooltip :title="`http://${dbInbound.address}:${dbInbound.port}`">
<a-button size="small"
@click="copyText(`http://${dbInbound.address}:${dbInbound.port}`)">
HTTP
</a-button>
</a-tooltip>
<a-tooltip title="https://t.me/socks?server=...&port=...">
<a-button size="small"
@click="copyText(`https://t.me/socks?server=${encodeURIComponent(dbInbound.address)}&port=${dbInbound.port}`)">
Telegram
</a-button>
</a-tooltip>
</a-space>
<a-dropdown :trigger="['click']" class="share-mobile">
<a-button size="small">
<template #icon><CopyOutlined /></template>
{{ t('copy') }}
</a-button>
<template #overlay>
<a-menu @click="({ key }) => {
const h = dbInbound.address;
const port = dbInbound.port;
if (key === 'telegram') {
copyText(`https://t.me/socks?server=${encodeURIComponent(h)}&port=${port}`);
} else {
copyText(`${key}://${h}:${port}`);
}
}">
<a-menu-item key="socks5">SOCKS5</a-menu-item>
<a-menu-item key="http">HTTP</a-menu-item>
<a-menu-item key="telegram">Telegram</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</dd>
</div>
</template>
@ -897,6 +967,7 @@ const showSubscriptionTab = computed(
white-space: normal;
word-break: break-all;
display: inline-block;
margin-right: 0;
}
.value-block {
@ -927,6 +998,27 @@ const showSubscriptionTab = computed(
flex-shrink: 0;
}
.share-buttons,
.share-mobile {
margin-inline-start: 4px;
padding-inline-start: 8px;
border-inline-start: 1px solid rgba(128, 128, 128, 0.25);
}
.share-mobile {
display: none;
}
@media (max-width: 600px) {
.share-desktop {
display: none !important;
}
.share-mobile {
display: inline-flex;
align-items: center;
}
}
.security-line {
display: flex;
align-items: center;

View file

@ -469,6 +469,13 @@ function cycleTheme() {
font-weight: 500;
}
.login-form :deep(input.ant-input:-webkit-autofill) {
-webkit-text-fill-color: var(--color-text) !important;
-webkit-box-shadow: 0 0 0 1000px var(--bg-card) inset !important;
box-shadow: 0 0 0 1000px var(--bg-card) inset !important;
transition: background-color 9999s ease-in-out 0s, color 9999s ease-in-out 0s;
}
.submit-row {
margin-bottom: 0;
}

View file

@ -125,7 +125,7 @@ const shadowrocketUrl = computed(() => {
if (!subUrl) return '';
const separator = subUrl.includes('?') ? '&' : '?';
const rawUrl = subUrl + separator + 'flag=shadowrocket';
const base64Url = encodeURIComponent(btoa(rawUrl));
const base64Url = btoa(rawUrl).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const remark = encodeURIComponent(subTitle || sId || 'Subscription');
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
});

View file

@ -328,6 +328,47 @@ function regenerateWgKeys() {
</a-select>
</a-form-item>
</template>
<a-form-item label="Final Rules">
<a-button size="small" type="primary" @click="outbound.settings.addFinalRule('allow')">
<template #icon>
<PlusOutlined />
</template>
</a-button>
<span class="ml-8" style="opacity: 0.6;">
Override Xray's default private-IP block (needed for LAN access through proxy)
</span>
</a-form-item>
<template v-for="(rule, index) in outbound.settings.finalRules || []" :key="`fr-${index}`">
<a-form-item :wrapper-col="{ md: { span: 14, offset: 8 } }" :colon="false">
<div class="item-heading">
<span>Rule {{ index + 1 }}</span>
<DeleteOutlined class="danger-icon" @click="outbound.settings.delFinalRule(index)" />
</div>
</a-form-item>
<a-form-item label="Action">
<a-select v-model:value="rule.action">
<a-select-option v-for="x in ['allow', 'block']" :key="x" :value="x">{{ x }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Network">
<a-select v-model:value="rule.network" allow-clear placeholder="(any)">
<a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option>
<a-select-option value="tcp,udp">tcp,udp</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Port">
<a-input v-model:value="rule.port" placeholder="e.g. 80,443 or 1000-2000" />
</a-form-item>
<a-form-item label="IP / CIDR / geoip">
<a-select v-model:value="rule.ip" mode="tags" :token-separators="[',', ' ']"
placeholder="e.g. 10.0.0.0/8, geoip:private, ext:cn.dat:cn" />
</a-form-item>
<a-form-item v-if="rule.action === 'block'" label="Block delay (ms)">
<a-input v-model:value="rule.blockDelay" placeholder="optional: 5000-10000" />
</a-form-item>
</template>
</template>
<!-- ============== Blackhole ============== -->
@ -947,6 +988,12 @@ function regenerateWgKeys() {
<a-form-item label="Penetrate">
<a-switch v-model:checked="outbound.stream.sockopt.penetrate" />
</a-form-item>
<a-form-item label="Mark (fwmark)">
<a-input-number v-model:value="outbound.stream.sockopt.mark" :min="0" />
</a-form-item>
<a-form-item label="Interface">
<a-input v-model:value="outbound.stream.sockopt.interfaceName" />
</a-form-item>
</template>
</template>

View file

@ -30,7 +30,10 @@
"outbounds": [{
"protocol": "freedom",
"settings": {
"domainStrategy": "AsIs"
"domainStrategy": "AsIs",
"finalRules": [
{ "action": "allow", "ip": ["geoip:private"] }
]
},
"tag": "direct"
},

View file

@ -2845,7 +2845,7 @@ func (s *InboundService) MigrationRequirements() {
// Fix inbounds based problems
var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria", "hysteria2"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
@ -2924,6 +2924,12 @@ func (s *InboundService) MigrationRequirements() {
}
}
}
// Heal clients table for installs where the one-shot seeder
// skipped clients due to a tgId-string unmarshal error.
if syncErr := s.clientService.SyncInbound(tx, inbounds[inbound_index].Id, modelClients); syncErr != nil {
logger.Warning("MigrationRequirements sync clients failed:", syncErr)
}
}
tx.Save(inbounds)

View file

@ -154,7 +154,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
directHTTPS := s.isDirectHTTPSConfigured()
engine.Use(middleware.SecurityHeadersMiddleware(directHTTPS))
sendHSTS := directHTTPS && !config.IsSkipHSTS()
engine.Use(middleware.SecurityHeadersMiddleware(sendHSTS))
webDomain, err := s.settingService.GetWebDomain()
if err != nil {