Merge branch 'main' into main

This commit is contained in:
Misfit-s 2026-06-04 12:49:34 +03:00 committed by GitHub
commit 30ba1f416b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 340 additions and 73 deletions

View file

@ -1 +1 @@
3.2.6 3.2.7

View file

@ -181,7 +181,7 @@ func runSeeders(isUsersEmpty bool) error {
} }
if empty && isUsersEmpty { if empty && isUsersEmpty {
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix"} seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "ApiTokensHash"}
for _, name := range seeders { for _, name := range seeders {
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil { if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
return err 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 !slices.Contains(seedersHistory, "ClientsTable") {
if err := seedClientsFromInboundJSON(); err != nil { if err := seedClientsFromInboundJSON(); err != nil {
return err return err
@ -646,6 +652,28 @@ func seedApiTokens() error {
return db.Create(&model.HistoryOfSeeders{SeederName: "ApiTokensTable"}).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. // isTableEmpty returns true if the named table contains zero rows.
func isTableEmpty(tableName string) (bool, error) { func isTableEmpty(tableName string) (bool, error) {
var count int64 var count int64

View file

@ -138,7 +138,7 @@ type HistoryOfSeeders struct {
type ApiToken struct { type ApiToken struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"uniqueIndex;not null"` 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"` Enabled bool `json:"enabled" gorm:"default:true"`
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"` CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
} }

View file

@ -69,7 +69,7 @@
}, },
{ {
"name": "API Tokens", "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 <code>Authorization: Bearer &lt;token&gt;</code> 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 <code>Authorization: Bearer &lt;token&gt;</code> on any /panel/api/* request."
}, },
{ {
"name": "Xray Settings", "name": "Xray Settings",
@ -5105,7 +5105,7 @@
"tags": [ "tags": [
"API Tokens" "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", "operationId": "get_panel_setting_apiTokens",
"responses": { "responses": {
"200": { "200": {
@ -5130,7 +5130,6 @@
{ {
"id": 1, "id": 1,
"name": "default", "name": "default",
"token": "abcdef-12345-...",
"enabled": true, "enabled": true,
"createdAt": 1736000000 "createdAt": 1736000000
} }
@ -5147,7 +5146,7 @@
"tags": [ "tags": [
"API Tokens" "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", "operationId": "post_panel_setting_apiTokens_create",
"requestBody": { "requestBody": {
"required": true, "required": true,

View file

@ -234,6 +234,7 @@ export default function AppSidebar() {
<div className="ant-sidebar"> <div className="ant-sidebar">
<Layout.Sider <Layout.Sider
theme={currentTheme} theme={currentTheme}
width={220}
collapsible collapsible
collapsed={collapsed} collapsed={collapsed}
breakpoint="md" breakpoint="md"

View file

@ -951,18 +951,18 @@ export const sections: readonly Section[] = [
id: 'api-tokens', id: 'api-tokens',
title: 'API Tokens', title: 'API Tokens',
description: 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 <code>Authorization: Bearer &lt;token&gt;</code> 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 <code>Authorization: Bearer &lt;token&gt;</code> on any /panel/api/* request.',
endpoints: [ endpoints: [
{ {
method: 'GET', method: 'GET',
path: '/panel/setting/apiTokens', path: '/panel/setting/apiTokens',
summary: 'List every API token, enabled or not.', 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 "token": "abcdef-12345-...",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}', response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}',
}, },
{ {
method: 'POST', method: 'POST',
path: '/panel/setting/apiTokens/create', 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: [ params: [
{ name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' }, { name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' },
], ],

View file

@ -83,6 +83,11 @@
word-break: break-all; word-break: break-all;
} }
.api-token-created-notice {
margin: 0 0 12px;
font-size: 13px;
}
.security-actions { .security-actions {
padding: 12px 0; padding: 12px 0;
display: flex; display: flex;

View file

@ -30,7 +30,6 @@ interface ApiMsg<T = unknown> {
interface ApiTokenRow { interface ApiTokenRow {
id: number; id: number;
name: string; name: string;
token: string;
enabled: boolean; enabled: boolean;
createdAt: number; createdAt: number;
} }
@ -77,10 +76,10 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
const [apiTokens, setApiTokens] = useState<ApiTokenRow[]>([]); const [apiTokens, setApiTokens] = useState<ApiTokenRow[]>([]);
const [apiTokensLoading, setApiTokensLoading] = useState(false); const [apiTokensLoading, setApiTokensLoading] = useState(false);
const [visibleTokenIds, setVisibleTokenIds] = useState<Set<number>>(() => new Set());
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [createName, setCreateName] = useState(''); const [createName, setCreateName] = useState('');
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [createdToken, setCreatedToken] = useState<{ name: string; token: string } | null>(null);
const openTfa = useCallback((opts: Omit<TfaState, 'open'>) => { const openTfa = useCallback((opts: Omit<TfaState, 'open'>) => {
setTfa({ ...opts, open: true }); setTfa({ ...opts, open: true });
@ -137,14 +136,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
loadApiTokens(); loadApiTokens();
}, [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) { async function copyToken(token: string) {
if (!token) return; if (!token) return;
const ok = await ClipboardManager.copyText(token); const ok = await ClipboardManager.copyText(token);
@ -165,17 +156,12 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
} }
setCreating(true); setCreating(true);
try { 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) { if (msg?.success) {
setCreateOpen(false); setCreateOpen(false);
await loadApiTokens(); await loadApiTokens();
if (msg.obj?.id != null) { if (msg.obj?.token) {
const id = msg.obj.id; setCreatedToken({ name, token: msg.obj.token });
setVisibleTokenIds((prev) => {
const next = new Set(prev);
next.add(id);
return next;
});
} }
} }
} finally { } 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 { function formatTokenDate(ts: number): string {
if (!ts) return ''; if (!ts) return '';
return new Date(ts * 1000).toLocaleString(); return new Date(ts * 1000).toLocaleString();
@ -326,17 +307,6 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
</Button> </Button>
</div> </div>
</div> </div>
<div className="api-token-value-wrap">
<code className="api-token-value">
{visibleTokenIds.has(row.id) ? row.token : maskToken(row.token)}
</code>
<Button size="small" onClick={() => toggleTokenVisibility(row.id)}>
{visibleTokenIds.has(row.id)
? (t('pages.settings.security.hide') || 'Hide')
: (t('pages.settings.security.show') || 'Show')}
</Button>
<Button size="small" onClick={() => copyToken(row.token)}>{t('copy')}</Button>
</div>
</div> </div>
))} ))}
</Spin> </Spin>
@ -367,6 +337,26 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
</Form> </Form>
</Modal> </Modal>
<Modal
open={!!createdToken}
title={t('pages.settings.security.apiTokenCreatedTitle') || 'Token created'}
okText={t('done')}
onOk={() => setCreatedToken(null)}
onCancel={() => setCreatedToken(null)}
cancelButtonProps={{ style: { display: 'none' } }}
>
<p className="api-token-created-notice">
{t('pages.settings.security.apiTokenCreatedNotice')
|| 'Copy this token now. For security it is not stored in readable form and will not be shown again.'}
</p>
<div className="api-token-value-wrap">
<code className="api-token-value">{createdToken?.token}</code>
<Button size="small" type="primary" onClick={() => createdToken && copyToken(createdToken.token)}>
{t('copy')}
</Button>
</div>
</Modal>
<TwoFactorModal <TwoFactorModal
open={tfa.open} open={tfa.open}
title={tfa.title} title={tfa.title}

View file

@ -1,8 +1,9 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Alert, Button, Input, Modal, Select, Space, Switch, Tabs } from 'antd'; import { Alert, Button, Input, InputNumber, Modal, Select, Space, Switch, Tabs } from 'antd';
import { import {
BarChartOutlined, BarChartOutlined,
ClockCircleOutlined,
FileTextOutlined, FileTextOutlined,
ReloadOutlined, ReloadOutlined,
SettingOutlined, SettingOutlined,
@ -54,6 +55,20 @@ export default function BasicsTab({
[setTemplateSettings], [setTemplateSettings],
); );
const setLevel0 = useCallback(
(field: string, value: number | null) => 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() { function confirmResetDefault() {
modal.confirm({ modal.confirm({
title: t('pages.settings.resetDefaultConfig'), title: t('pages.settings.resetDefaultConfig'),
@ -72,6 +87,7 @@ export default function BasicsTab({
const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs'; const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs';
const log = (templateSettings?.log || {}) as Record<string, unknown>; const log = (templateSettings?.log || {}) as Record<string, unknown>;
const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>; const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>;
const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record<string, unknown>;
const items = [ const items = [
{ {
@ -168,6 +184,50 @@ export default function BasicsTab({
</> </>
), ),
}, },
{
key: 'connection',
label: catTabLabel(<ClockCircleOutlined />, t('pages.xray.connectionLimits'), isMobile),
children: (
<>
<Alert
type="warning"
showIcon
className="mb-12 hint-alert"
title={t('pages.xray.connectionLimitsDesc')}
/>
<SettingListItem
title={t('pages.xray.connIdle')}
description={t('pages.xray.connIdleDesc')}
paddings="small"
control={
<InputNumber
value={typeof level0.connIdle === 'number' ? level0.connIdle : undefined}
min={0}
style={{ width: '100%' }}
placeholder="300"
addonAfter={t('pages.xray.seconds')}
onChange={(v) => setLevel0('connIdle', v as number | null)}
/>
}
/>
<SettingListItem
title={t('pages.xray.bufferSize')}
description={t('pages.xray.bufferSizeDesc')}
paddings="small"
control={
<InputNumber
value={typeof level0.bufferSize === 'number' ? level0.bufferSize : undefined}
min={0}
style={{ width: '100%' }}
placeholder={t('pages.xray.bufferSizePlaceholder')}
addonAfter="KB"
onChange={(v) => setLevel0('bufferSize', v as number | null)}
/>
}
/>
</>
),
},
{ {
key: '3', key: '3',
label: catTabLabel(<FileTextOutlined />, t('pages.xray.logConfigs'), isMobile), label: catTabLabel(<FileTextOutlined />, t('pages.xray.logConfigs'), isMobile),

View file

@ -28,6 +28,7 @@ export const XraySettingsValueSchema = z.object({
log: z.record(z.string(), z.unknown()).optional(), log: z.record(z.string(), z.unknown()).optional(),
policy: z.object({ policy: z.object({
system: z.record(z.string(), z.boolean()).optional(), system: z.record(z.string(), z.boolean()).optional(),
levels: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
}).loose().optional(), }).loose().optional(),
observatory: z.unknown().optional(), observatory: z.unknown().optional(),
burstObservatory: z.unknown().optional(), burstObservatory: z.unknown().optional(),

13
go.mod
View file

@ -21,7 +21,7 @@ require (
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.71.0 github.com/valyala/fasthttp v1.71.0
github.com/xlzd/gotp v0.1.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 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.52.0 golang.org/x/crypto v0.52.0
golang.org/x/sys v0.45.0 golang.org/x/sys v0.45.0
@ -33,10 +33,19 @@ require (
gorm.io/gorm v1.31.1 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 ( require (
github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/andybalholm/brotli v1.2.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/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.1 // indirect github.com/bytedance/sonic v1.15.1 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect

20
go.sum
View file

@ -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/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 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= 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.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
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/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw= 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 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 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 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 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= 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 h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= 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.1-0.20260601021109-94ffd50060f1 h1:RAxvdTekSZCn1OO5P9d0ioDrdiiqdOsdqllxLvC+IGQ=
github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60= 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 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/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 h1:cqHQ3AycTHvM2R7ikgyX57D+XvtcSnGylsLkOVhta/w=
golang.zx2c4.com/wireguard v0.0.0-20260522210424-ecfc5a8d5446/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= 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 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= 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= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=

View file

@ -2,6 +2,9 @@
package crypto package crypto
import ( import (
"crypto/sha256"
"encoding/hex"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -20,3 +23,25 @@ func IsHashed(s string) bool {
_, err := bcrypt.Cost([]byte(s)) _, err := bcrypt.Cost([]byte(s))
return err == nil 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
}

View file

@ -8,6 +8,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/database" "github.com/mhsanaei/3x-ui/v3/database"
"github.com/mhsanaei/3x-ui/v3/database/model" "github.com/mhsanaei/3x-ui/v3/database/model"
"github.com/mhsanaei/3x-ui/v3/util/common" "github.com/mhsanaei/3x-ui/v3/util/common"
"github.com/mhsanaei/3x-ui/v3/util/crypto"
"github.com/mhsanaei/3x-ui/v3/util/random" "github.com/mhsanaei/3x-ui/v3/util/random"
) )
@ -18,16 +19,18 @@ const apiTokenLength = 48
type ApiTokenView struct { type ApiTokenView struct {
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Token string `json:"token"` Token string `json:"token,omitempty"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
CreatedAt int64 `json:"createdAt"` 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 { func toView(t *model.ApiToken) *ApiTokenView {
return &ApiTokenView{ return &ApiTokenView{
Id: t.Id, Id: t.Id,
Name: t.Name, Name: t.Name,
Token: t.Token,
Enabled: t.Enabled, Enabled: t.Enabled,
CreatedAt: t.CreatedAt, CreatedAt: t.CreatedAt,
} }
@ -62,15 +65,18 @@ func (s *ApiTokenService) Create(name string) (*ApiTokenView, error) {
if count > 0 { if count > 0 {
return nil, common.NewError("a token with that name already exists") return nil, common.NewError("a token with that name already exists")
} }
plaintext := random.Seq(apiTokenLength)
row := &model.ApiToken{ row := &model.ApiToken{
Name: name, Name: name,
Token: random.Seq(apiTokenLength), Token: crypto.HashTokenSHA256(plaintext),
Enabled: true, Enabled: true,
} }
if err := db.Create(row).Error; err != nil { if err := db.Create(row).Error; err != nil {
return nil, err return nil, err
} }
return toView(row), nil view := toView(row)
view.Token = plaintext
return view, nil
} }
func (s *ApiTokenService) Delete(id int) error { 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 // Match returns true when the presented bearer token matches any enabled
// row in api_tokens. Uses constant-time compare per row so a remote // row in api_tokens. Tokens are stored as SHA-256 hashes, so the presented
// attacker can't time-attack tokens byte-by-byte. // 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 { func (s *ApiTokenService) Match(presented string) bool {
if presented == "" { if presented == "" {
return false 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 { if err := db.Model(model.ApiToken{}).Where("enabled = ?", true).Find(&rows).Error; err != nil {
return false return false
} }
presentedBytes := []byte(presented) presentedHash := []byte(crypto.HashTokenSHA256(presented))
matched := false matched := false
for _, r := range rows { for _, r := range rows {
if subtle.ConstantTimeCompare([]byte(r.Token), presentedBytes) == 1 { if subtle.ConstantTimeCompare([]byte(r.Token), presentedHash) == 1 {
matched = true matched = true
} }
} }

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "مثل central-panel-a", "apiTokenNamePlaceholder": "مثل central-panel-a",
"apiTokenNameRequired": "الاسم مطلوب", "apiTokenNameRequired": "الاسم مطلوب",
"apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.", "apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.",
"apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا." "apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا.",
"apiTokenCreatedTitle": "تم إنشاء الرمز",
"apiTokenCreatedNotice": "انسخ هذا الرمز الآن. لأسباب أمنية لا يتم تخزينه بصيغة قابلة للقراءة ولن يتم عرضه مرة أخرى."
}, },
"toasts": { "toasts": {
"modifySettings": "تم تغيير المعلمات.", "modifySettings": "تم تغيير المعلمات.",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات.", "statsOutboundUplinkDesc": "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات.",
"statsOutboundDownlink": "إحصائيات تنزيل المخرجات", "statsOutboundDownlink": "إحصائيات تنزيل المخرجات",
"statsOutboundDownlinkDesc": "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات.", "statsOutboundDownlinkDesc": "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات.",
"connectionLimits": "حدود الاتصال",
"connectionLimitsDesc": "سياسات على مستوى الاتصال لمستوى المستخدم 0. اترك الحقل فارغًا لاستخدام القيمة الافتراضية لـ Xray.",
"connIdle": "مهلة الخمول",
"connIdleDesc": "يغلق الاتصال بعد بقائه خاملًا لهذا العدد من الثواني. خفضه يحرر الذاكرة وواصفات الملفات بشكل أسرع على الخوادم المزدحمة (الافتراضي في Xray: 300).",
"bufferSize": "حجم المخزن المؤقت",
"bufferSizeDesc": "حجم المخزن المؤقت الداخلي لكل اتصال بالكيلوبايت. اضبطه على 0 لتقليل استهلاك الذاكرة على الخوادم منخفضة الذاكرة (الافتراضي في Xray يعتمد على المنصة).",
"bufferSizePlaceholder": "تلقائي",
"seconds": "ثانية",
"rules": { "rules": {
"first": "أول", "first": "أول",
"last": "آخر", "last": "آخر",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "e.g. central-panel-a", "apiTokenNamePlaceholder": "e.g. central-panel-a",
"apiTokenNameRequired": "Name is required", "apiTokenNameRequired": "Name is required",
"apiTokenEmpty": "No tokens yet — create one to authenticate bots or remote panels.", "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": { "toasts": {
"modifySettings": "The parameters have been changed.", "modifySettings": "The parameters have been changed.",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Enables the statistics collection for upstream traffic of all outbound proxies.", "statsOutboundUplinkDesc": "Enables the statistics collection for upstream traffic of all outbound proxies.",
"statsOutboundDownlink": "Outbound Download Statistics", "statsOutboundDownlink": "Outbound Download Statistics",
"statsOutboundDownlinkDesc": "Enables the statistics collection for downstream traffic of all outbound proxies.", "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": { "rules": {
"first": "First", "first": "First",
"last": "Last", "last": "Last",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "por ejemplo central-panel-a", "apiTokenNamePlaceholder": "por ejemplo central-panel-a",
"apiTokenNameRequired": "El nombre es obligatorio", "apiTokenNameRequired": "El nombre es obligatorio",
"apiTokenEmpty": "Aún no hay tokens — crea uno para autenticar bots o paneles remotos.", "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": { "toasts": {
"modifySettings": "Los parámetros han sido modificados.", "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.", "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", "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.", "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": { "rules": {
"first": "Primero", "first": "Primero",
"last": "Último", "last": "Último",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "مثلاً central-panel-a", "apiTokenNamePlaceholder": "مثلاً central-panel-a",
"apiTokenNameRequired": "نام الزامی است", "apiTokenNameRequired": "نام الزامی است",
"apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت ربات‌ها یا پنل‌های راه دور یکی بسازید.", "apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت ربات‌ها یا پنل‌های راه دور یکی بسازید.",
"apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده می‌کند بلافاصله احراز هویتش قطع می‌شود." "apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده می‌کند بلافاصله احراز هویتش قطع می‌شود.",
"apiTokenCreatedTitle": "توکن ساخته شد",
"apiTokenCreatedNotice": "اکنون این توکن را کپی کنید. به‌دلیل امنیتی به‌صورت قابل‌خواندن ذخیره نمی‌شود و دوباره نمایش داده نخواهد شد."
}, },
"toasts": { "toasts": {
"modifySettings": "پارامترها تغییر کرده‌اند.", "modifySettings": "پارامترها تغییر کرده‌اند.",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند.", "statsOutboundUplinkDesc": "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند.",
"statsOutboundDownlink": "آمار دانلود خروجی", "statsOutboundDownlink": "آمار دانلود خروجی",
"statsOutboundDownlinkDesc": "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند.", "statsOutboundDownlinkDesc": "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند.",
"connectionLimits": "محدودیت اتصال",
"connectionLimitsDesc": "سیاست‌های سطح اتصال برای کاربرانِ سطح ۰. هر فیلد را خالی بگذارید تا مقدار پیش‌فرض Xray استفاده شود.",
"connIdle": "مهلت بی‌کاری",
"connIdleDesc": "اتصال را پس از این تعداد ثانیه بی‌کار ماندن می‌بندد. کم‌کردن آن، روی سرورهای شلوغ حافظه و file descriptor را زودتر آزاد می‌کند (پیش‌فرض Xray: ۳۰۰).",
"bufferSize": "اندازهٔ بافر",
"bufferSizeDesc": "اندازهٔ بافر داخلی هر اتصال بر حسب کیلوبایت. برای کم‌کردن مصرف حافظه روی سرورهای کم‌رم روی ۰ بگذارید (پیش‌فرض Xray به پلتفرم بستگی دارد).",
"bufferSizePlaceholder": "خودکار",
"seconds": "ثانیه",
"rules": { "rules": {
"first": "اولین", "first": "اولین",
"last": "آخرین", "last": "آخرین",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "misalnya central-panel-a", "apiTokenNamePlaceholder": "misalnya central-panel-a",
"apiTokenNameRequired": "Nama wajib diisi", "apiTokenNameRequired": "Nama wajib diisi",
"apiTokenEmpty": "Belum ada token — buat satu untuk mengautentikasi bot atau panel jarak jauh.", "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": { "toasts": {
"modifySettings": "Parameter telah diubah.", "modifySettings": "Parameter telah diubah.",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar.", "statsOutboundUplinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar.",
"statsOutboundDownlink": "Statistik Unduh Keluar", "statsOutboundDownlink": "Statistik Unduh Keluar",
"statsOutboundDownlinkDesc": "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy 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": { "rules": {
"first": "Pertama", "first": "Pertama",
"last": "Terakhir", "last": "Terakhir",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "例: central-panel-a", "apiTokenNamePlaceholder": "例: central-panel-a",
"apiTokenNameRequired": "名前は必須です", "apiTokenNameRequired": "名前は必須です",
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。", "apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。" "apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。",
"apiTokenCreatedTitle": "トークンを作成しました",
"apiTokenCreatedNotice": "このトークンを今すぐコピーしてください。セキュリティ上、読み取り可能な形式では保存されず、再表示されません。"
}, },
"toasts": { "toasts": {
"modifySettings": "パラメーターが変更されました。", "modifySettings": "パラメーターが変更されました。",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。", "statsOutboundUplinkDesc": "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。",
"statsOutboundDownlink": "アウトバウンドダウンロード統計", "statsOutboundDownlink": "アウトバウンドダウンロード統計",
"statsOutboundDownlinkDesc": "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。", "statsOutboundDownlinkDesc": "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。",
"connectionLimits": "接続制限",
"connectionLimitsDesc": "ユーザーレベル0の接続レベルのポリシーです。フィールドを空のままにすると Xray のデフォルト値が使用されます。",
"connIdle": "アイドルタイムアウト",
"connIdleDesc": "接続がこの秒数アイドル状態のままになると接続を閉じます。値を下げると、混雑したサーバーでメモリとファイルディスクリプタをより早く解放できますXray のデフォルト: 300。",
"bufferSize": "バッファサイズ",
"bufferSizeDesc": "接続ごとの内部バッファサイズKB単位。低メモリのサーバーでメモリ使用量を最小限にするには 0 に設定しますXray のデフォルトはプラットフォームに依存します)。",
"bufferSizePlaceholder": "自動",
"seconds": "秒",
"rules": { "rules": {
"first": "最初", "first": "最初",
"last": "最後", "last": "最後",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "ex.: central-panel-a", "apiTokenNamePlaceholder": "ex.: central-panel-a",
"apiTokenNameRequired": "O nome é obrigatório", "apiTokenNameRequired": "O nome é obrigatório",
"apiTokenEmpty": "Nenhum token ainda — crie um para autenticar bots ou painéis remotos.", "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": { "toasts": {
"modifySettings": "Os parâmetros foram alterados.", "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.", "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", "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.", "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": { "rules": {
"first": "Primeiro", "first": "Primeiro",
"last": "Último", "last": "Último",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "например, central-panel-a", "apiTokenNamePlaceholder": "например, central-panel-a",
"apiTokenNameRequired": "Имя обязательно", "apiTokenNameRequired": "Имя обязательно",
"apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.", "apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.",
"apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию." "apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию.",
"apiTokenCreatedTitle": "Токен создан",
"apiTokenCreatedNotice": "Скопируйте этот токен сейчас. В целях безопасности он не хранится в читаемом виде и больше не будет показан."
}, },
"toasts": { "toasts": {
"modifySettings": "Настройки изменены", "modifySettings": "Настройки изменены",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Включает сбор статистики для исходящего трафика всех исходящих прокси.", "statsOutboundUplinkDesc": "Включает сбор статистики для исходящего трафика всех исходящих прокси.",
"statsOutboundDownlink": "Статистика исходящего даунлинка", "statsOutboundDownlink": "Статистика исходящего даунлинка",
"statsOutboundDownlinkDesc": "Включает сбор статистики для входящего трафика всех исходящих прокси.", "statsOutboundDownlinkDesc": "Включает сбор статистики для входящего трафика всех исходящих прокси.",
"connectionLimits": "Ограничения соединения",
"connectionLimitsDesc": "Политики уровня соединения для пользователей уровня 0. Оставьте поле пустым, чтобы использовать значение Xray по умолчанию.",
"connIdle": "Тайм-аут простоя",
"connIdleDesc": "Закрывает соединение после простоя в течение указанного числа секунд. Уменьшение значения быстрее освобождает память и файловые дескрипторы на нагруженных серверах (по умолчанию в Xray: 300).",
"bufferSize": "Размер буфера",
"bufferSizeDesc": "Размер внутреннего буфера на соединение в КБ. Установите 0, чтобы минимизировать использование памяти на серверах с малым объёмом ОЗУ (значение Xray по умолчанию зависит от платформы).",
"bufferSizePlaceholder": "авто",
"seconds": "секунд",
"rules": { "rules": {
"first": "Первый", "first": "Первый",
"last": "Последний", "last": "Последний",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "örn. central-panel-a", "apiTokenNamePlaceholder": "örn. central-panel-a",
"apiTokenNameRequired": "Ad zorunludur", "apiTokenNameRequired": "Ad zorunludur",
"apiTokenEmpty": "Henüz token yok — bot veya uzak panelleri doğrulamak için bir tane oluşturun.", "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": { "toasts": {
"modifySettings": "Parametreler değiştirildi.", "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.", "statsOutboundUplinkDesc": "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir.",
"statsOutboundDownlink": "Giden İndirme İstatistikleri", "statsOutboundDownlink": "Giden İndirme İstatistikleri",
"statsOutboundDownlinkDesc": "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir.", "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": { "rules": {
"first": "İlk", "first": "İlk",
"last": "Son", "last": "Son",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "наприклад, central-panel-a", "apiTokenNamePlaceholder": "наприклад, central-panel-a",
"apiTokenNameRequired": "Назва обов'язкова", "apiTokenNameRequired": "Назва обов'язкова",
"apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.", "apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.",
"apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію." "apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію.",
"apiTokenCreatedTitle": "Токен створено",
"apiTokenCreatedNotice": "Скопіюйте цей токен зараз. З міркувань безпеки він не зберігається у читабельному вигляді й більше не відображатиметься."
}, },
"toasts": { "toasts": {
"modifySettings": "Параметри було змінено.", "modifySettings": "Параметри було змінено.",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі.", "statsOutboundUplinkDesc": "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі.",
"statsOutboundDownlink": "Статистика вихідного даунлінку", "statsOutboundDownlink": "Статистика вихідного даунлінку",
"statsOutboundDownlinkDesc": "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі.", "statsOutboundDownlinkDesc": "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі.",
"connectionLimits": "Обмеження з'єднання",
"connectionLimitsDesc": "Політики рівня з'єднання для користувачів рівня 0. Залиште поле порожнім, щоб використовувати значення Xray за замовчуванням.",
"connIdle": "Тайм-аут простою",
"connIdleDesc": "Закриває з'єднання після простою протягом вказаної кількості секунд. Зменшення значення швидше звільняє пам'ять і файлові дескриптори на завантажених серверах (за замовчуванням у Xray: 300).",
"bufferSize": "Розмір буфера",
"bufferSizeDesc": "Розмір внутрішнього буфера на з'єднання в КБ. Встановіть 0, щоб мінімізувати використання пам'яті на серверах з малим обсягом ОЗП (значення Xray за замовчуванням залежить від платформи).",
"bufferSizePlaceholder": "авто",
"seconds": "секунд",
"rules": { "rules": {
"first": "Перший", "first": "Перший",
"last": "Останній", "last": "Останній",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "ví dụ: central-panel-a", "apiTokenNamePlaceholder": "ví dụ: central-panel-a",
"apiTokenNameRequired": "Tên là bắt buộc", "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.", "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": { "toasts": {
"modifySettings": "Các tham số đã được thay đổi.", "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.", "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", "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.", "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": { "rules": {
"first": "Đầu tiên", "first": "Đầu tiên",
"last": "Cuối cùng", "last": "Cuối cùng",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "例如 central-panel-a", "apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名称必填", "apiTokenNameRequired": "名称必填",
"apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。", "apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。",
"apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。" "apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。",
"apiTokenCreatedTitle": "令牌已创建",
"apiTokenCreatedNotice": "请立即复制此令牌。出于安全考虑,它不会以可读形式存储,也不会再次显示。"
}, },
"toasts": { "toasts": {
"modifySettings": "参数已更改。", "modifySettings": "参数已更改。",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "启用所有出站代理的上行流量统计收集。", "statsOutboundUplinkDesc": "启用所有出站代理的上行流量统计收集。",
"statsOutboundDownlink": "出站下载统计", "statsOutboundDownlink": "出站下载统计",
"statsOutboundDownlinkDesc": "启用所有出站代理的下行流量统计收集。", "statsOutboundDownlinkDesc": "启用所有出站代理的下行流量统计收集。",
"connectionLimits": "连接限制",
"connectionLimitsDesc": "用户等级 0 的连接级策略。留空则使用 Xray 的默认值。",
"connIdle": "空闲超时",
"connIdleDesc": "连接空闲达到该秒数后将被关闭。在繁忙的服务器上调低此值可更快释放内存和文件描述符Xray 默认值300。",
"bufferSize": "缓冲区大小",
"bufferSizeDesc": "每个连接的内部缓冲区大小KB。在低内存服务器上设为 0 可最大限度减少内存占用Xray 默认值取决于平台)。",
"bufferSizePlaceholder": "自动",
"seconds": "秒",
"rules": { "rules": {
"first": "置顶", "first": "置顶",
"last": "置底", "last": "置底",

View file

@ -1123,7 +1123,9 @@
"apiTokenNamePlaceholder": "例如 central-panel-a", "apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名稱必填", "apiTokenNameRequired": "名稱必填",
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。", "apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。" "apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。",
"apiTokenCreatedTitle": "權杖已建立",
"apiTokenCreatedNotice": "請立即複製此權杖。基於安全考量,它不會以可讀形式儲存,也不會再次顯示。"
}, },
"toasts": { "toasts": {
"modifySettings": "參數已更改。", "modifySettings": "參數已更改。",
@ -1205,6 +1207,14 @@
"statsOutboundUplinkDesc": "啟用所有出站代理的上行流量統計收集。", "statsOutboundUplinkDesc": "啟用所有出站代理的上行流量統計收集。",
"statsOutboundDownlink": "出站下載統計", "statsOutboundDownlink": "出站下載統計",
"statsOutboundDownlinkDesc": "啟用所有出站代理的下行流量統計收集。", "statsOutboundDownlinkDesc": "啟用所有出站代理的下行流量統計收集。",
"connectionLimits": "連線限制",
"connectionLimitsDesc": "使用者等級 0 的連線層級原則。留空則使用 Xray 的預設值。",
"connIdle": "閒置逾時",
"connIdleDesc": "連線閒置達到該秒數後將被關閉。在繁忙的伺服器上調低此值可更快釋放記憶體和檔案描述符Xray 預設值300。",
"bufferSize": "緩衝區大小",
"bufferSizeDesc": "每個連線的內部緩衝區大小KB。在低記憶體伺服器上設為 0 可最大限度減少記憶體佔用Xray 預設值取決於平台)。",
"bufferSizePlaceholder": "自動",
"seconds": "秒",
"rules": { "rules": {
"first": "置頂", "first": "置頂",
"last": "置底", "last": "置底",