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/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts
index 6b3a5c8e..6e9c7e8e 100644
--- a/frontend/src/pages/api-docs/endpoints.ts
+++ b/frontend/src/pages/api-docs/endpoints.ts
@@ -951,18 +951,18 @@ export const sections: readonly Section[] = [
id: 'api-tokens',
title: '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.',
+ '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}
+
+
+
+
'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 fb9c71dc..91290fc5 100644
--- a/web/translation/ar-EG.json
+++ b/web/translation/ar-EG.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "مثل central-panel-a",
"apiTokenNameRequired": "الاسم مطلوب",
"apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.",
- "apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا."
+ "apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا.",
+ "apiTokenCreatedTitle": "تم إنشاء الرمز",
+ "apiTokenCreatedNotice": "انسخ هذا الرمز الآن. لأسباب أمنية لا يتم تخزينه بصيغة قابلة للقراءة ولن يتم عرضه مرة أخرى."
},
"toasts": {
"modifySettings": "تم تغيير المعلمات.",
diff --git a/web/translation/en-US.json b/web/translation/en-US.json
index 7e0f91be..a28c0894 100644
--- a/web/translation/en-US.json
+++ b/web/translation/en-US.json
@@ -1119,7 +1119,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.",
diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json
index 2a31dd99..0b9ba12c 100644
--- a/web/translation/es-ES.json
+++ b/web/translation/es-ES.json
@@ -1119,7 +1119,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.",
diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json
index bef48625..359a6de8 100644
--- a/web/translation/fa-IR.json
+++ b/web/translation/fa-IR.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "مثلاً central-panel-a",
"apiTokenNameRequired": "نام الزامی است",
"apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت رباتها یا پنلهای راه دور یکی بسازید.",
- "apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود."
+ "apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده میکند بلافاصله احراز هویتش قطع میشود.",
+ "apiTokenCreatedTitle": "توکن ساخته شد",
+ "apiTokenCreatedNotice": "اکنون این توکن را کپی کنید. بهدلیل امنیتی بهصورت قابلخواندن ذخیره نمیشود و دوباره نمایش داده نخواهد شد."
},
"toasts": {
"modifySettings": "پارامترها تغییر کردهاند.",
diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json
index 1b3ccf3f..6742f21f 100644
--- a/web/translation/id-ID.json
+++ b/web/translation/id-ID.json
@@ -1119,7 +1119,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.",
diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json
index d3dc3a35..7397e12a 100644
--- a/web/translation/ja-JP.json
+++ b/web/translation/ja-JP.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "例: central-panel-a",
"apiTokenNameRequired": "名前は必須です",
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
- "apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。"
+ "apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。",
+ "apiTokenCreatedTitle": "トークンを作成しました",
+ "apiTokenCreatedNotice": "このトークンを今すぐコピーしてください。セキュリティ上、読み取り可能な形式では保存されず、再表示されません。"
},
"toasts": {
"modifySettings": "パラメーターが変更されました。",
diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json
index 99da5271..955df690 100644
--- a/web/translation/pt-BR.json
+++ b/web/translation/pt-BR.json
@@ -1119,7 +1119,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.",
diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json
index f0ed1d89..32b0e436 100644
--- a/web/translation/ru-RU.json
+++ b/web/translation/ru-RU.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "например, central-panel-a",
"apiTokenNameRequired": "Имя обязательно",
"apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.",
- "apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию."
+ "apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию.",
+ "apiTokenCreatedTitle": "Токен создан",
+ "apiTokenCreatedNotice": "Скопируйте этот токен сейчас. В целях безопасности он не хранится в читаемом виде и больше не будет показан."
},
"toasts": {
"modifySettings": "Настройки изменены",
diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json
index 9b20a188..e78f55ab 100644
--- a/web/translation/tr-TR.json
+++ b/web/translation/tr-TR.json
@@ -1119,7 +1119,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.",
diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json
index 8b3a1ce8..4d9974d8 100644
--- a/web/translation/uk-UA.json
+++ b/web/translation/uk-UA.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "наприклад, central-panel-a",
"apiTokenNameRequired": "Назва обов'язкова",
"apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.",
- "apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію."
+ "apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію.",
+ "apiTokenCreatedTitle": "Токен створено",
+ "apiTokenCreatedNotice": "Скопіюйте цей токен зараз. З міркувань безпеки він не зберігається у читабельному вигляді й більше не відображатиметься."
},
"toasts": {
"modifySettings": "Параметри було змінено.",
diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json
index 47c6affc..66b2693e 100644
--- a/web/translation/vi-VN.json
+++ b/web/translation/vi-VN.json
@@ -1119,7 +1119,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.",
diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json
index 1841e980..cbd9aea6 100644
--- a/web/translation/zh-CN.json
+++ b/web/translation/zh-CN.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名称必填",
"apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。",
- "apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。"
+ "apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。",
+ "apiTokenCreatedTitle": "令牌已创建",
+ "apiTokenCreatedNotice": "请立即复制此令牌。出于安全考虑,它不会以可读形式存储,也不会再次显示。"
},
"toasts": {
"modifySettings": "参数已更改。",
diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json
index a84b9981..c3422480 100644
--- a/web/translation/zh-TW.json
+++ b/web/translation/zh-TW.json
@@ -1119,7 +1119,9 @@
"apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名稱必填",
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
- "apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。"
+ "apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。",
+ "apiTokenCreatedTitle": "權杖已建立",
+ "apiTokenCreatedNotice": "請立即複製此權杖。基於安全考量,它不會以可讀形式儲存,也不會再次顯示。"
},
"toasts": {
"modifySettings": "參數已更改。",