diff --git a/config/version b/config/version
index c4a602db..6a3913b0 100644
--- a/config/version
+++ b/config/version
@@ -1 +1 @@
-3.2.6
\ No newline at end of file
+3.2.7
\ No newline at end of file
diff --git a/database/db.go b/database/db.go
index c2d79742..b3b914e7 100644
--- a/database/db.go
+++ b/database/db.go
@@ -181,7 +181,7 @@ func runSeeders(isUsersEmpty bool) error {
}
if empty && isUsersEmpty {
- seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix"}
+ seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "ApiTokensHash"}
for _, name := range seeders {
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
return err
@@ -232,6 +232,12 @@ func runSeeders(isUsersEmpty bool) error {
}
}
+ if !slices.Contains(seedersHistory, "ApiTokensHash") {
+ if err := hashExistingApiTokens(); err != nil {
+ return err
+ }
+ }
+
if !slices.Contains(seedersHistory, "ClientsTable") {
if err := seedClientsFromInboundJSON(); err != nil {
return err
@@ -646,6 +652,28 @@ func seedApiTokens() error {
return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensTable"}).Error
}
+// hashExistingApiTokens replaces any plaintext token stored before tokens were
+// hashed at rest with its SHA-256 digest. Callers keep their plaintext copy
+// (used on remote nodes), so existing tokens keep authenticating; the panel
+// just can no longer reveal them. Idempotent — already-hashed rows are skipped.
+func hashExistingApiTokens() error {
+ var rows []*model.ApiToken
+ if err := db.Find(&rows).Error; err != nil {
+ return err
+ }
+ for _, r := range rows {
+ if crypto.IsSHA256Hex(r.Token) {
+ continue
+ }
+ hashed := crypto.HashTokenSHA256(r.Token)
+ if err := db.Model(model.ApiToken{}).Where("id = ?", r.Id).Update("token", hashed).Error; err != nil {
+ log.Printf("Error hashing api token %d: %v", r.Id, err)
+ return err
+ }
+ }
+ return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensHash"}).Error
+}
+
// isTableEmpty returns true if the named table contains zero rows.
func isTableEmpty(tableName string) (bool, error) {
var count int64
diff --git a/database/model/model.go b/database/model/model.go
index 4d70a44d..2db08a29 100644
--- a/database/model/model.go
+++ b/database/model/model.go
@@ -138,7 +138,7 @@ type HistoryOfSeeders struct {
type ApiToken struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"uniqueIndex;not null"`
- Token string `json:"token" gorm:"not null"`
+ Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
Enabled bool `json:"enabled" gorm:"default:true"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
}
diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json
index d51b0b8d..dab418d5 100644
--- a/frontend/public/openapi.json
+++ b/frontend/public/openapi.json
@@ -69,7 +69,7 @@
},
{
"name": "API Tokens",
- "description": "Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored plaintext so the SPA can show them on demand. Send one as Authorization: Bearer <token> on any /panel/api/* request."
+ "description": "Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as Authorization: Bearer <token> on any /panel/api/* request."
},
{
"name": "Xray Settings",
@@ -5105,7 +5105,7 @@
"tags": [
"API Tokens"
],
- "summary": "List every API token, enabled or not.",
+ "summary": "List every API token, enabled or not. The token value is never returned — only metadata.",
"operationId": "get_panel_setting_apiTokens",
"responses": {
"200": {
@@ -5130,7 +5130,6 @@
{
"id": 1,
"name": "default",
- "token": "abcdef-12345-...",
"enabled": true,
"createdAt": 1736000000
}
@@ -5147,7 +5146,7 @@
"tags": [
"API Tokens"
],
- "summary": "Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated.",
+ "summary": "Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.",
"operationId": "post_panel_setting_apiTokens_create",
"requestBody": {
"required": true,
diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx
index 8e662ed2..4ad7cf60 100644
--- a/frontend/src/layouts/AppSidebar.tsx
+++ b/frontend/src/layouts/AppSidebar.tsx
@@ -234,6 +234,7 @@ export default function AppSidebar() {
Authorization: Bearer <token> on any /panel/api/* request.',
+ 'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as Authorization: Bearer <token> on any /panel/api/* request.',
endpoints: [
{
method: 'GET',
path: '/panel/setting/apiTokens',
- summary: 'List every API token, enabled or not.',
- response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "token": "abcdef-12345-...",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}',
+ summary: 'List every API token, enabled or not. The token value is never returned — only metadata.',
+ response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}',
},
{
method: 'POST',
path: '/panel/setting/apiTokens/create',
- summary: 'Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated.',
+ summary: 'Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.',
params: [
{ name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' },
],
diff --git a/frontend/src/pages/settings/SecurityTab.css b/frontend/src/pages/settings/SecurityTab.css
index e078c081..87a6d5f4 100644
--- a/frontend/src/pages/settings/SecurityTab.css
+++ b/frontend/src/pages/settings/SecurityTab.css
@@ -83,6 +83,11 @@
word-break: break-all;
}
+.api-token-created-notice {
+ margin: 0 0 12px;
+ font-size: 13px;
+}
+
.security-actions {
padding: 12px 0;
display: flex;
diff --git a/frontend/src/pages/settings/SecurityTab.tsx b/frontend/src/pages/settings/SecurityTab.tsx
index 8c4dd2ab..f564a528 100644
--- a/frontend/src/pages/settings/SecurityTab.tsx
+++ b/frontend/src/pages/settings/SecurityTab.tsx
@@ -30,7 +30,6 @@ interface ApiMsg {
interface ApiTokenRow {
id: number;
name: string;
- token: string;
enabled: boolean;
createdAt: number;
}
@@ -77,10 +76,10 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
const [apiTokens, setApiTokens] = useState([]);
const [apiTokensLoading, setApiTokensLoading] = useState(false);
- const [visibleTokenIds, setVisibleTokenIds] = useState>(() => new Set());
const [createOpen, setCreateOpen] = useState(false);
const [createName, setCreateName] = useState('');
const [creating, setCreating] = useState(false);
+ const [createdToken, setCreatedToken] = useState<{ name: string; token: string } | null>(null);
const openTfa = useCallback((opts: Omit) => {
setTfa({ ...opts, open: true });
@@ -137,14 +136,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
loadApiTokens();
}, [loadApiTokens]);
- function toggleTokenVisibility(id: number) {
- setVisibleTokenIds((prev) => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id); else next.add(id);
- return next;
- });
- }
-
async function copyToken(token: string) {
if (!token) return;
const ok = await ClipboardManager.copyText(token);
@@ -165,17 +156,12 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
}
setCreating(true);
try {
- const msg = await HttpUtil.post('/panel/setting/apiTokens/create', { name }) as ApiMsg<{ id?: number }>;
+ const msg = await HttpUtil.post('/panel/setting/apiTokens/create', { name }) as ApiMsg<{ token?: string }>;
if (msg?.success) {
setCreateOpen(false);
await loadApiTokens();
- if (msg.obj?.id != null) {
- const id = msg.obj.id;
- setVisibleTokenIds((prev) => {
- const next = new Set(prev);
- next.add(id);
- return next;
- });
+ if (msg.obj?.token) {
+ setCreatedToken({ name, token: msg.obj.token });
}
}
} finally {
@@ -206,11 +192,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
}
}
- function maskToken(token: string): string {
- if (!token) return '';
- return '•'.repeat(Math.min(token.length, 24));
- }
-
function formatTokenDate(ts: number): string {
if (!ts) return '';
return new Date(ts * 1000).toLocaleString();
@@ -326,17 +307,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
+ {t('pages.settings.security.apiTokenCreatedNotice')
+ || 'Copy this token now. For security it is not stored in readable form and will not be shown again.'}
+
+
+ {createdToken?.token}
+
+
+
+
mutate((tt) => {
+ if (!tt.policy) tt.policy = {};
+ if (!tt.policy.levels) tt.policy.levels = {};
+ if (!tt.policy.levels['0']) tt.policy.levels['0'] = {};
+ if (value === null || value === undefined) {
+ delete tt.policy.levels['0'][field];
+ } else {
+ tt.policy.levels['0'][field] = value;
+ }
+ }),
+ [mutate],
+ );
+
function confirmResetDefault() {
modal.confirm({
title: t('pages.settings.resetDefaultConfig'),
@@ -72,6 +87,7 @@ export default function BasicsTab({
const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs';
const log = (templateSettings?.log || {}) as Record;
const policy = (templateSettings?.policy?.system || {}) as Record;
+ const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record;
const items = [
{
@@ -168,6 +184,50 @@ export default function BasicsTab({
>
),
},
+ {
+ key: 'connection',
+ label: catTabLabel(, t('pages.xray.connectionLimits'), isMobile),
+ children: (
+ <>
+
+ setLevel0('connIdle', v as number | null)}
+ />
+ }
+ />
+ setLevel0('bufferSize', v as number | null)}
+ />
+ }
+ />
+ >
+ ),
+ },
{
key: '3',
label: catTabLabel(, t('pages.xray.logConfigs'), isMobile),
diff --git a/frontend/src/schemas/xray.ts b/frontend/src/schemas/xray.ts
index ad19e5dd..1eeedb4d 100644
--- a/frontend/src/schemas/xray.ts
+++ b/frontend/src/schemas/xray.ts
@@ -28,6 +28,7 @@ export const XraySettingsValueSchema = z.object({
log: z.record(z.string(), z.unknown()).optional(),
policy: z.object({
system: z.record(z.string(), z.boolean()).optional(),
+ levels: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
}).loose().optional(),
observatory: z.unknown().optional(),
burstObservatory: z.unknown().optional(),
diff --git a/go.mod b/go.mod
index d59ef1c3..860eef9d 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.71.0
github.com/xlzd/gotp v0.1.0
- github.com/xtls/xray-core v1.260327.0
+ github.com/xtls/xray-core v1.260327.1-0.20260601021109-94ffd50060f1
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.52.0
golang.org/x/sys v0.45.0
@@ -33,10 +33,19 @@ require (
gorm.io/gorm v1.31.1
)
+require (
+ github.com/pion/dtls/v3 v3.1.2 // indirect
+ github.com/pion/logging v0.2.4 // indirect
+ github.com/pion/stun/v3 v3.1.2 // indirect
+ github.com/pion/transport/v4 v4.0.1 // indirect
+ github.com/wlynxg/anet v0.0.5 // indirect
+ golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
+)
+
require (
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
- github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
+ github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.1 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
diff --git a/go.sum b/go.sum
index e5cdee93..02bc521e 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
-github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU=
-github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
+github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
+github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
@@ -148,6 +148,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
+github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
+github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
+github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
+github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY=
+github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA=
+github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
+github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -202,12 +210,14 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
+github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
-github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE=
-github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60=
+github.com/xtls/xray-core v1.260327.1-0.20260601021109-94ffd50060f1 h1:RAxvdTekSZCn1OO5P9d0ioDrdiiqdOsdqllxLvC+IGQ=
+github.com/xtls/xray-core v1.260327.1-0.20260601021109-94ffd50060f1/go.mod h1:klRI+zA2uG6qrelDRoUaEur3gasszRE9W8e2zTgqXNU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -263,6 +273,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w=
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
+golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
+golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
diff --git a/util/crypto/crypto.go b/util/crypto/crypto.go
index bd7b6ffa..b55634ff 100644
--- a/util/crypto/crypto.go
+++ b/util/crypto/crypto.go
@@ -2,6 +2,9 @@
package crypto
import (
+ "crypto/sha256"
+ "encoding/hex"
+
"golang.org/x/crypto/bcrypt"
)
@@ -20,3 +23,25 @@ func IsHashed(s string) bool {
_, err := bcrypt.Cost([]byte(s))
return err == nil
}
+
+// HashTokenSHA256 returns the hex-encoded SHA-256 digest of token. API tokens
+// are high-entropy random strings, so a fast unsalted digest is sufficient to
+// keep them irrecoverable at rest while allowing constant-time verification.
+func HashTokenSHA256(token string) string {
+ sum := sha256.Sum256([]byte(token))
+ return hex.EncodeToString(sum[:])
+}
+
+// IsSHA256Hex reports whether s looks like a hex-encoded SHA-256 digest
+// (64 lowercase hex characters), used to skip already-hashed token rows.
+func IsSHA256Hex(s string) bool {
+ if len(s) != 64 {
+ return false
+ }
+ for _, c := range s {
+ if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
+ return false
+ }
+ }
+ return true
+}
diff --git a/web/service/api_token.go b/web/service/api_token.go
index fbde1a47..adeeae18 100644
--- a/web/service/api_token.go
+++ b/web/service/api_token.go
@@ -8,6 +8,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/database"
"github.com/mhsanaei/3x-ui/v3/database/model"
"github.com/mhsanaei/3x-ui/v3/util/common"
+ "github.com/mhsanaei/3x-ui/v3/util/crypto"
"github.com/mhsanaei/3x-ui/v3/util/random"
)
@@ -18,16 +19,18 @@ const apiTokenLength = 48
type ApiTokenView struct {
Id int `json:"id"`
Name string `json:"name"`
- Token string `json:"token"`
+ Token string `json:"token,omitempty"`
Enabled bool `json:"enabled"`
CreatedAt int64 `json:"createdAt"`
}
+// toView builds the metadata view returned by List. It never carries the
+// token value: only a SHA-256 hash is stored, and the plaintext is shown
+// exactly once at creation time.
func toView(t *model.ApiToken) *ApiTokenView {
return &ApiTokenView{
Id: t.Id,
Name: t.Name,
- Token: t.Token,
Enabled: t.Enabled,
CreatedAt: t.CreatedAt,
}
@@ -62,15 +65,18 @@ func (s *ApiTokenService) Create(name string) (*ApiTokenView, error) {
if count > 0 {
return nil, common.NewError("a token with that name already exists")
}
+ plaintext := random.Seq(apiTokenLength)
row := &model.ApiToken{
Name: name,
- Token: random.Seq(apiTokenLength),
+ Token: crypto.HashTokenSHA256(plaintext),
Enabled: true,
}
if err := db.Create(row).Error; err != nil {
return nil, err
}
- return toView(row), nil
+ view := toView(row)
+ view.Token = plaintext
+ return view, nil
}
func (s *ApiTokenService) Delete(id int) error {
@@ -97,8 +103,9 @@ func (s *ApiTokenService) SetEnabled(id int, enabled bool) error {
}
// Match returns true when the presented bearer token matches any enabled
-// row in api_tokens. Uses constant-time compare per row so a remote
-// attacker can't time-attack tokens byte-by-byte.
+// row in api_tokens. Tokens are stored as SHA-256 hashes, so the presented
+// value is hashed before a constant-time compare per row keeps a remote
+// attacker from timing the comparison byte-by-byte.
func (s *ApiTokenService) Match(presented string) bool {
if presented == "" {
return false
@@ -108,10 +115,10 @@ func (s *ApiTokenService) Match(presented string) bool {
if err := db.Model(model.ApiToken{}).Where("enabled = ?", true).Find(&rows).Error; err != nil {
return false
}
- presentedBytes := []byte(presented)
+ presentedHash := []byte(crypto.HashTokenSHA256(presented))
matched := false
for _, r := range rows {
- if subtle.ConstantTimeCompare([]byte(r.Token), presentedBytes) == 1 {
+ if subtle.ConstantTimeCompare([]byte(r.Token), presentedHash) == 1 {
matched = true
}
}
diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json
index 9fb955db..ad992a52 100644
--- a/web/translation/ar-EG.json
+++ b/web/translation/ar-EG.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "مثل central-panel-a",
"apiTokenNameRequired": "الاسم مطلوب",
"apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.",
- "apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا."
+ "apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا.",
+ "apiTokenCreatedTitle": "تم إنشاء الرمز",
+ "apiTokenCreatedNotice": "انسخ هذا الرمز الآن. لأسباب أمنية لا يتم تخزينه بصيغة قابلة للقراءة ولن يتم عرضه مرة أخرى."
},
"toasts": {
"modifySettings": "تم تغيير المعلمات.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات.",
"statsOutboundDownlink": "إحصائيات تنزيل المخرجات",
"statsOutboundDownlinkDesc": "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات.",
+ "connectionLimits": "حدود الاتصال",
+ "connectionLimitsDesc": "سياسات على مستوى الاتصال لمستوى المستخدم 0. اترك الحقل فارغًا لاستخدام القيمة الافتراضية لـ Xray.",
+ "connIdle": "مهلة الخمول",
+ "connIdleDesc": "يغلق الاتصال بعد بقائه خاملًا لهذا العدد من الثواني. خفضه يحرر الذاكرة وواصفات الملفات بشكل أسرع على الخوادم المزدحمة (الافتراضي في Xray: 300).",
+ "bufferSize": "حجم المخزن المؤقت",
+ "bufferSizeDesc": "حجم المخزن المؤقت الداخلي لكل اتصال بالكيلوبايت. اضبطه على 0 لتقليل استهلاك الذاكرة على الخوادم منخفضة الذاكرة (الافتراضي في Xray يعتمد على المنصة).",
+ "bufferSizePlaceholder": "تلقائي",
+ "seconds": "ثانية",
"rules": {
"first": "أول",
"last": "آخر",
diff --git a/web/translation/en-US.json b/web/translation/en-US.json
index 4a122049..0498f0e1 100644
--- a/web/translation/en-US.json
+++ b/web/translation/en-US.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "e.g. central-panel-a",
"apiTokenNameRequired": "Name is required",
"apiTokenEmpty": "No tokens yet — create one to authenticate bots or remote panels.",
- "apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately."
+ "apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately.",
+ "apiTokenCreatedTitle": "Token created",
+ "apiTokenCreatedNotice": "Copy this token now. For security it is not stored in readable form and will not be shown again."
},
"toasts": {
"modifySettings": "The parameters have been changed.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Enables the statistics collection for upstream traffic of all outbound proxies.",
"statsOutboundDownlink": "Outbound Download Statistics",
"statsOutboundDownlinkDesc": "Enables the statistics collection for downstream traffic of all outbound proxies.",
+ "connectionLimits": "Connection Limits",
+ "connectionLimitsDesc": "Connection-level policies for user level 0. Leave a field empty to use Xray's default.",
+ "connIdle": "Idle Timeout",
+ "connIdleDesc": "Closes a connection after it stays idle for this many seconds. Lowering it frees memory and file descriptors faster on busy servers (Xray default: 300).",
+ "bufferSize": "Buffer Size",
+ "bufferSizeDesc": "Per-connection internal buffer size in KB. Set to 0 to minimize memory usage on low-RAM servers (Xray default depends on the platform).",
+ "bufferSizePlaceholder": "auto",
+ "seconds": "seconds",
"rules": {
"first": "First",
"last": "Last",
diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json
index 056d8c94..36b77d7c 100644
--- a/web/translation/es-ES.json
+++ b/web/translation/es-ES.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "por ejemplo central-panel-a",
"apiTokenNameRequired": "El nombre es obligatorio",
"apiTokenEmpty": "Aún no hay tokens — crea uno para autenticar bots o paneles remotos.",
- "apiTokenDeleteWarning": "Cualquier cliente que use este token dejará de autenticarse inmediatamente."
+ "apiTokenDeleteWarning": "Cualquier cliente que use este token dejará de autenticarse inmediatamente.",
+ "apiTokenCreatedTitle": "Token creado",
+ "apiTokenCreatedNotice": "Copia este token ahora. Por seguridad, no se almacena de forma legible y no se volverá a mostrar."
},
"toasts": {
"modifySettings": "Los parámetros han sido modificados.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida.",
"statsOutboundDownlink": "Estadísticas de Bajada de Salida",
"statsOutboundDownlinkDesc": "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida.",
+ "connectionLimits": "Límites de conexión",
+ "connectionLimitsDesc": "Políticas a nivel de conexión para el nivel de usuario 0. Deja un campo vacío para usar el valor predeterminado de Xray.",
+ "connIdle": "Tiempo de inactividad",
+ "connIdleDesc": "Cierra una conexión después de que permanezca inactiva durante esta cantidad de segundos. Reducirlo libera memoria y descriptores de archivo más rápido en servidores con mucha carga (predeterminado de Xray: 300).",
+ "bufferSize": "Tamaño del búfer",
+ "bufferSizeDesc": "Tamaño del búfer interno por conexión en KB. Ponlo en 0 para minimizar el uso de memoria en servidores con poca RAM (el valor predeterminado de Xray depende de la plataforma).",
+ "bufferSizePlaceholder": "automático",
+ "seconds": "segundos",
"rules": {
"first": "Primero",
"last": "Último",
diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json
index 847215f8..7a4ba151 100644
--- a/web/translation/fa-IR.json
+++ b/web/translation/fa-IR.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "مثلاً central-panel-a",
"apiTokenNameRequired": "نام الزامی است",
"apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت رباتها یا پنلهای راه دور یکی بسازید.",
- "apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود."
+ "apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود.",
+ "apiTokenCreatedTitle": "توکن ساخته شد",
+ "apiTokenCreatedNotice": "اکنون این توکن را کپی کنید. بهدلیل امنیتی بهصورت قابلخواندن ذخیره نمیشود و دوباره نمایش داده نخواهد شد."
},
"toasts": {
"modifySettings": "پارامترها تغییر کردهاند.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "جمعآوری آمار برای ترافیک بالارو (آپلود) تمام پروکسیهای خروجی را فعال میکند.",
"statsOutboundDownlink": "آمار دانلود خروجی",
"statsOutboundDownlinkDesc": "جمعآوری آمار برای ترافیک پایینرو (دانلود) تمام پروکسیهای خروجی را فعال میکند.",
+ "connectionLimits": "محدودیت اتصال",
+ "connectionLimitsDesc": "سیاستهای سطح اتصال برای کاربرانِ سطح ۰. هر فیلد را خالی بگذارید تا مقدار پیشفرض Xray استفاده شود.",
+ "connIdle": "مهلت بیکاری",
+ "connIdleDesc": "اتصال را پس از این تعداد ثانیه بیکار ماندن میبندد. کمکردن آن، روی سرورهای شلوغ حافظه و file descriptor را زودتر آزاد میکند (پیشفرض Xray: ۳۰۰).",
+ "bufferSize": "اندازهٔ بافر",
+ "bufferSizeDesc": "اندازهٔ بافر داخلی هر اتصال بر حسب کیلوبایت. برای کمکردن مصرف حافظه روی سرورهای کمرم روی ۰ بگذارید (پیشفرض Xray به پلتفرم بستگی دارد).",
+ "bufferSizePlaceholder": "خودکار",
+ "seconds": "ثانیه",
"rules": {
"first": "اولین",
"last": "آخرین",
diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json
index 7ee50064..56f7b85e 100644
--- a/web/translation/id-ID.json
+++ b/web/translation/id-ID.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "misalnya central-panel-a",
"apiTokenNameRequired": "Nama wajib diisi",
"apiTokenEmpty": "Belum ada token — buat satu untuk mengautentikasi bot atau panel jarak jauh.",
- "apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera."
+ "apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera.",
+ "apiTokenCreatedTitle": "Token dibuat",
+ "apiTokenCreatedNotice": "Salin token ini sekarang. Demi keamanan, token tidak disimpan dalam bentuk yang dapat dibaca dan tidak akan ditampilkan lagi."
},
"toasts": {
"modifySettings": "Parameter telah diubah.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar.",
"statsOutboundDownlink": "Statistik Unduh Keluar",
"statsOutboundDownlinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar.",
+ "connectionLimits": "Batas Koneksi",
+ "connectionLimitsDesc": "Kebijakan tingkat koneksi untuk level pengguna 0. Biarkan kolom kosong untuk menggunakan nilai bawaan Xray.",
+ "connIdle": "Batas Waktu Idle",
+ "connIdleDesc": "Menutup koneksi setelah idle selama sekian detik. Menurunkannya membebaskan memori dan file descriptor lebih cepat pada server yang sibuk (bawaan Xray: 300).",
+ "bufferSize": "Ukuran Buffer",
+ "bufferSizeDesc": "Ukuran buffer internal per koneksi dalam KB. Setel ke 0 untuk meminimalkan penggunaan memori pada server ber-RAM rendah (nilai bawaan Xray bergantung pada platform).",
+ "bufferSizePlaceholder": "otomatis",
+ "seconds": "detik",
"rules": {
"first": "Pertama",
"last": "Terakhir",
diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json
index b6b0c101..f1d804d4 100644
--- a/web/translation/ja-JP.json
+++ b/web/translation/ja-JP.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "例: central-panel-a",
"apiTokenNameRequired": "名前は必須です",
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
- "apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。"
+ "apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。",
+ "apiTokenCreatedTitle": "トークンを作成しました",
+ "apiTokenCreatedNotice": "このトークンを今すぐコピーしてください。セキュリティ上、読み取り可能な形式では保存されず、再表示されません。"
},
"toasts": {
"modifySettings": "パラメーターが変更されました。",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。",
"statsOutboundDownlink": "アウトバウンドダウンロード統計",
"statsOutboundDownlinkDesc": "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。",
+ "connectionLimits": "接続制限",
+ "connectionLimitsDesc": "ユーザーレベル0の接続レベルのポリシーです。フィールドを空のままにすると Xray のデフォルト値が使用されます。",
+ "connIdle": "アイドルタイムアウト",
+ "connIdleDesc": "接続がこの秒数アイドル状態のままになると接続を閉じます。値を下げると、混雑したサーバーでメモリとファイルディスクリプタをより早く解放できます(Xray のデフォルト: 300)。",
+ "bufferSize": "バッファサイズ",
+ "bufferSizeDesc": "接続ごとの内部バッファサイズ(KB単位)。低メモリのサーバーでメモリ使用量を最小限にするには 0 に設定します(Xray のデフォルトはプラットフォームに依存します)。",
+ "bufferSizePlaceholder": "自動",
+ "seconds": "秒",
"rules": {
"first": "最初",
"last": "最後",
diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json
index fdd5687c..56bbde70 100644
--- a/web/translation/pt-BR.json
+++ b/web/translation/pt-BR.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "ex.: central-panel-a",
"apiTokenNameRequired": "O nome é obrigatório",
"apiTokenEmpty": "Nenhum token ainda — crie um para autenticar bots ou painéis remotos.",
- "apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente."
+ "apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente.",
+ "apiTokenCreatedTitle": "Token criado",
+ "apiTokenCreatedNotice": "Copie este token agora. Por segurança, ele não é armazenado de forma legível e não será exibido novamente."
},
"toasts": {
"modifySettings": "Os parâmetros foram alterados.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída.",
"statsOutboundDownlink": "Estatísticas de Download de Saída",
"statsOutboundDownlinkDesc": "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída.",
+ "connectionLimits": "Limites de conexão",
+ "connectionLimitsDesc": "Políticas em nível de conexão para o nível de usuário 0. Deixe um campo vazio para usar o padrão do Xray.",
+ "connIdle": "Tempo limite de inatividade",
+ "connIdleDesc": "Fecha uma conexão depois que ela fica inativa por esta quantidade de segundos. Reduzi-lo libera memória e descritores de arquivo mais rápido em servidores ocupados (padrão do Xray: 300).",
+ "bufferSize": "Tamanho do buffer",
+ "bufferSizeDesc": "Tamanho do buffer interno por conexão em KB. Defina como 0 para minimizar o uso de memória em servidores com pouca RAM (o padrão do Xray depende da plataforma).",
+ "bufferSizePlaceholder": "automático",
+ "seconds": "segundos",
"rules": {
"first": "Primeiro",
"last": "Último",
diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json
index eca2a4fc..2fc599ab 100644
--- a/web/translation/ru-RU.json
+++ b/web/translation/ru-RU.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "например, central-panel-a",
"apiTokenNameRequired": "Имя обязательно",
"apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.",
- "apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию."
+ "apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию.",
+ "apiTokenCreatedTitle": "Токен создан",
+ "apiTokenCreatedNotice": "Скопируйте этот токен сейчас. В целях безопасности он не хранится в читаемом виде и больше не будет показан."
},
"toasts": {
"modifySettings": "Настройки изменены",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Включает сбор статистики для исходящего трафика всех исходящих прокси.",
"statsOutboundDownlink": "Статистика исходящего даунлинка",
"statsOutboundDownlinkDesc": "Включает сбор статистики для входящего трафика всех исходящих прокси.",
+ "connectionLimits": "Ограничения соединения",
+ "connectionLimitsDesc": "Политики уровня соединения для пользователей уровня 0. Оставьте поле пустым, чтобы использовать значение Xray по умолчанию.",
+ "connIdle": "Тайм-аут простоя",
+ "connIdleDesc": "Закрывает соединение после простоя в течение указанного числа секунд. Уменьшение значения быстрее освобождает память и файловые дескрипторы на нагруженных серверах (по умолчанию в Xray: 300).",
+ "bufferSize": "Размер буфера",
+ "bufferSizeDesc": "Размер внутреннего буфера на соединение в КБ. Установите 0, чтобы минимизировать использование памяти на серверах с малым объёмом ОЗУ (значение Xray по умолчанию зависит от платформы).",
+ "bufferSizePlaceholder": "авто",
+ "seconds": "секунд",
"rules": {
"first": "Первый",
"last": "Последний",
diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json
index fc3f5ca6..46c88047 100644
--- a/web/translation/tr-TR.json
+++ b/web/translation/tr-TR.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "örn. central-panel-a",
"apiTokenNameRequired": "Ad zorunludur",
"apiTokenEmpty": "Henüz token yok — bot veya uzak panelleri doğrulamak için bir tane oluşturun.",
- "apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder."
+ "apiTokenDeleteWarning": "Bu tokenı kullanan tüm istemciler anında kimlik doğrulamasını kaybeder.",
+ "apiTokenCreatedTitle": "Belirteç oluşturuldu",
+ "apiTokenCreatedNotice": "Bu belirteci şimdi kopyalayın. Güvenlik nedeniyle okunabilir biçimde saklanmaz ve tekrar gösterilmez."
},
"toasts": {
"modifySettings": "Parametreler değiştirildi.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir.",
"statsOutboundDownlink": "Giden İndirme İstatistikleri",
"statsOutboundDownlinkDesc": "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir.",
+ "connectionLimits": "Bağlantı Sınırları",
+ "connectionLimitsDesc": "Kullanıcı seviyesi 0 için bağlantı düzeyi politikaları. Xray'in varsayılanını kullanmak için alanı boş bırakın.",
+ "connIdle": "Boşta Kalma Zaman Aşımı",
+ "connIdleDesc": "Bağlantı bu kadar saniye boşta kaldıktan sonra kapatılır. Değerin düşürülmesi, yoğun sunucularda belleği ve dosya tanımlayıcılarını daha hızlı serbest bırakır (Xray varsayılanı: 300).",
+ "bufferSize": "Arabellek Boyutu",
+ "bufferSizeDesc": "Bağlantı başına dahili arabellek boyutu (KB). Düşük RAM'li sunucularda bellek kullanımını en aza indirmek için 0 olarak ayarlayın (Xray varsayılanı platforma bağlıdır).",
+ "bufferSizePlaceholder": "otomatik",
+ "seconds": "saniye",
"rules": {
"first": "İlk",
"last": "Son",
diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json
index 15403ebf..c25f3d4b 100644
--- a/web/translation/uk-UA.json
+++ b/web/translation/uk-UA.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "наприклад, central-panel-a",
"apiTokenNameRequired": "Назва обов'язкова",
"apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.",
- "apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію."
+ "apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію.",
+ "apiTokenCreatedTitle": "Токен створено",
+ "apiTokenCreatedNotice": "Скопіюйте цей токен зараз. З міркувань безпеки він не зберігається у читабельному вигляді й більше не відображатиметься."
},
"toasts": {
"modifySettings": "Параметри було змінено.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі.",
"statsOutboundDownlink": "Статистика вихідного даунлінку",
"statsOutboundDownlinkDesc": "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі.",
+ "connectionLimits": "Обмеження з'єднання",
+ "connectionLimitsDesc": "Політики рівня з'єднання для користувачів рівня 0. Залиште поле порожнім, щоб використовувати значення Xray за замовчуванням.",
+ "connIdle": "Тайм-аут простою",
+ "connIdleDesc": "Закриває з'єднання після простою протягом вказаної кількості секунд. Зменшення значення швидше звільняє пам'ять і файлові дескриптори на завантажених серверах (за замовчуванням у Xray: 300).",
+ "bufferSize": "Розмір буфера",
+ "bufferSizeDesc": "Розмір внутрішнього буфера на з'єднання в КБ. Встановіть 0, щоб мінімізувати використання пам'яті на серверах з малим обсягом ОЗП (значення Xray за замовчуванням залежить від платформи).",
+ "bufferSizePlaceholder": "авто",
+ "seconds": "секунд",
"rules": {
"first": "Перший",
"last": "Останній",
diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json
index 0afe8a68..8b25d74f 100644
--- a/web/translation/vi-VN.json
+++ b/web/translation/vi-VN.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "ví dụ: central-panel-a",
"apiTokenNameRequired": "Tên là bắt buộc",
"apiTokenEmpty": "Chưa có token nào — tạo một token để xác thực bot hoặc panel từ xa.",
- "apiTokenDeleteWarning": "Mọi client đang dùng token này sẽ ngừng xác thực ngay lập tức."
+ "apiTokenDeleteWarning": "Mọi client đang dùng token này sẽ ngừng xác thực ngay lập tức.",
+ "apiTokenCreatedTitle": "Đã tạo token",
+ "apiTokenCreatedNotice": "Hãy sao chép token này ngay bây giờ. Vì lý do bảo mật, token không được lưu ở dạng đọc được và sẽ không hiển thị lại."
},
"toasts": {
"modifySettings": "Các tham số đã được thay đổi.",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu ra.",
"statsOutboundDownlink": "Thống kê tải xuống đầu ra",
"statsOutboundDownlinkDesc": "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu ra.",
+ "connectionLimits": "Giới hạn kết nối",
+ "connectionLimitsDesc": "Chính sách cấp kết nối cho người dùng cấp 0. Để trống một trường để sử dụng giá trị mặc định của Xray.",
+ "connIdle": "Thời gian chờ nhàn rỗi",
+ "connIdleDesc": "Đóng kết nối sau khi nó ở trạng thái nhàn rỗi trong số giây này. Giảm giá trị này giúp giải phóng bộ nhớ và file descriptor nhanh hơn trên các máy chủ bận (mặc định của Xray: 300).",
+ "bufferSize": "Kích thước bộ đệm",
+ "bufferSizeDesc": "Kích thước bộ đệm nội bộ trên mỗi kết nối tính bằng KB. Đặt thành 0 để giảm thiểu mức sử dụng bộ nhớ trên các máy chủ ít RAM (giá trị mặc định của Xray tùy thuộc vào nền tảng).",
+ "bufferSizePlaceholder": "tự động",
+ "seconds": "giây",
"rules": {
"first": "Đầu tiên",
"last": "Cuối cùng",
diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json
index 67cd4219..5986a826 100644
--- a/web/translation/zh-CN.json
+++ b/web/translation/zh-CN.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名称必填",
"apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。",
- "apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。"
+ "apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。",
+ "apiTokenCreatedTitle": "令牌已创建",
+ "apiTokenCreatedNotice": "请立即复制此令牌。出于安全考虑,它不会以可读形式存储,也不会再次显示。"
},
"toasts": {
"modifySettings": "参数已更改。",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "启用所有出站代理的上行流量统计收集。",
"statsOutboundDownlink": "出站下载统计",
"statsOutboundDownlinkDesc": "启用所有出站代理的下行流量统计收集。",
+ "connectionLimits": "连接限制",
+ "connectionLimitsDesc": "用户等级 0 的连接级策略。留空则使用 Xray 的默认值。",
+ "connIdle": "空闲超时",
+ "connIdleDesc": "连接空闲达到该秒数后将被关闭。在繁忙的服务器上调低此值可更快释放内存和文件描述符(Xray 默认值:300)。",
+ "bufferSize": "缓冲区大小",
+ "bufferSizeDesc": "每个连接的内部缓冲区大小(KB)。在低内存服务器上设为 0 可最大限度减少内存占用(Xray 默认值取决于平台)。",
+ "bufferSizePlaceholder": "自动",
+ "seconds": "秒",
"rules": {
"first": "置顶",
"last": "置底",
diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json
index 6504d160..616d018c 100644
--- a/web/translation/zh-TW.json
+++ b/web/translation/zh-TW.json
@@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名稱必填",
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
- "apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。"
+ "apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。",
+ "apiTokenCreatedTitle": "權杖已建立",
+ "apiTokenCreatedNotice": "請立即複製此權杖。基於安全考量,它不會以可讀形式儲存,也不會再次顯示。"
},
"toasts": {
"modifySettings": "參數已更改。",
@@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "啟用所有出站代理的上行流量統計收集。",
"statsOutboundDownlink": "出站下載統計",
"statsOutboundDownlinkDesc": "啟用所有出站代理的下行流量統計收集。",
+ "connectionLimits": "連線限制",
+ "connectionLimitsDesc": "使用者等級 0 的連線層級原則。留空則使用 Xray 的預設值。",
+ "connIdle": "閒置逾時",
+ "connIdleDesc": "連線閒置達到該秒數後將被關閉。在繁忙的伺服器上調低此值可更快釋放記憶體和檔案描述符(Xray 預設值:300)。",
+ "bufferSize": "緩衝區大小",
+ "bufferSizeDesc": "每個連線的內部緩衝區大小(KB)。在低記憶體伺服器上設為 0 可最大限度減少記憶體佔用(Xray 預設值取決於平台)。",
+ "bufferSizePlaceholder": "自動",
+ "seconds": "秒",
"rules": {
"first": "置頂",
"last": "置底",