fix(auth): add node-admin token scope for panel updates (#6666)

* feat(auth): add node-admin API token scope

Keep ordinary node-sync credentials unable to self-update a remote panel while allowing explicitly elevated node administrators to inherit synchronization access and invoke updatePanel.

* style(docs): format API scope table

* fix(auth): preserve mTLS node-sync semantics

Allow only an explicit node-admin bearer to elevate a verified mTLS request. Existing admin, monitor, and node-sync bearer values continue to use the historical node-sync identity on mTLS connections.

* chore(ci): rerun flaky checks

---------

Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
This commit is contained in:
libmur-dev
2026-10-18 00:45:26 +03:00
committed by GitHub
parent a1f2589eb5
commit a8d0700bf2
42 changed files with 506 additions and 123 deletions
+9 -4
View File
@@ -161,13 +161,18 @@ type HistoryOfSeeders struct {
const ApiTokenUnixMillisecondsThreshold int64 = 100_000_000_000
const (
ApiScopeAdmin = "admin"
ApiScopeMonitor = "monitor"
ApiScopeNodeSync = "node-sync"
ApiScopeAdmin = "admin"
ApiScopeMonitor = "monitor"
ApiScopeNodeSync = "node-sync"
ApiScopeNodeAdmin = "node-admin"
)
func IsKnownApiScope(s string) bool {
return s == ApiScopeAdmin || s == ApiScopeMonitor || s == ApiScopeNodeSync
return s == ApiScopeAdmin || s == ApiScopeMonitor || s == ApiScopeNodeSync || s == ApiScopeNodeAdmin
}
func IsNodeSyncApiScope(s string) bool {
return s == ApiScopeNodeSync || s == ApiScopeNodeAdmin
}
type ApiToken struct {
+43 -23
View File
@@ -35,30 +35,34 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
}
func (a *APIController) checkAPIAuth(c *gin.Context) {
// A verified client certificate (a completed mTLS handshake) authenticates
// the caller, equivalent to a valid bearer token. api_authed must be set so
// the CSRF middleware lets cert-authed mutations through.
if c.Request.TLS != nil && len(c.Request.TLS.VerifiedChains) > 0 {
setAuthenticated := func(scope string) {
if u, err := a.userService.GetFirstUser(); err == nil {
session.SetAPIAuthUser(c, u)
}
c.Set("api_authed", true)
c.Set("api_token_scope", model.ApiScopeNodeSync)
c.Set("api_token_scope", scope)
}
matchedScope := ""
if after, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer "); ok {
if row, matched := a.apiTokenService.MatchToken(after); matched {
matchedScope = row.Scope
}
}
// Verified mTLS keeps its historical node-sync identity. Only an explicit
// node-admin bearer elevates it to software-update authority.
if c.Request.TLS != nil && len(c.Request.TLS.VerifiedChains) > 0 {
scope := model.ApiScopeNodeSync
if matchedScope == model.ApiScopeNodeAdmin {
scope = model.ApiScopeNodeAdmin
}
setAuthenticated(scope)
c.Next()
return
}
auth := c.GetHeader("Authorization")
if after, ok := strings.CutPrefix(auth, "Bearer "); ok {
tok := after
if row, ok := a.apiTokenService.MatchToken(tok); ok {
if u, err := a.userService.GetFirstUser(); err == nil {
session.SetAPIAuthUser(c, u)
}
c.Set("api_authed", true)
c.Set("api_token_scope", row.Scope)
c.Next()
return
}
if matchedScope != "" {
setAuthenticated(matchedScope)
c.Next()
return
}
if !session.IsLogin(c) {
// A presented Bearer token is not an anonymous scan: return 401 so
@@ -120,7 +124,20 @@ var nodeSyncScopeAllow = map[string]map[string]struct{}{
"/hosts/list": {http.MethodGet: {}},
}
// enforceTokenScope applies explicit allowlists to monitor and node-sync tokens.
var nodeAdminScopeAllow = map[string]map[string]struct{}{
"/server/updatePanel": {http.MethodPost: {}},
}
func scopeAllows(allow map[string]map[string]struct{}, path, method string) bool {
methods, exists := allow[path]
if !exists {
return false
}
_, exists = methods[method]
return exists
}
// enforceTokenScope applies explicit allowlists to restricted API tokens.
// Admin tokens and session-login users retain their existing behavior.
func (a *APIController) enforceTokenScope(c *gin.Context) {
scopeVal, ok := c.Get("api_token_scope")
@@ -147,11 +164,14 @@ func (a *APIController) enforceTokenScope(c *gin.Context) {
return
}
case model.ApiScopeNodeSync:
if methods, allowed := nodeSyncScopeAllow[rel]; allowed {
if _, allowedMethod := methods[c.Request.Method]; allowedMethod {
c.Next()
return
}
if scopeAllows(nodeSyncScopeAllow, rel, c.Request.Method) {
c.Next()
return
}
case model.ApiScopeNodeAdmin:
if scopeAllows(nodeSyncScopeAllow, rel, c.Request.Method) || scopeAllows(nodeAdminScopeAllow, rel, c.Request.Method) {
c.Next()
return
}
default:
deny()
+79
View File
@@ -107,6 +107,43 @@ func TestCheckAPIAuth_BearerSuccess(t *testing.T) {
}
}
func TestPanelUpdateRequiresNodeAdminScope(t *testing.T) {
engine, _ := newAPIAuthTestEngine(t)
cases := []struct {
name string
scope string
wantCode int
wantBody string
}{
{"node sync denied", model.ApiScopeNodeSync, http.StatusForbidden, `{"msg":"this API token is not permitted to access this endpoint","success":false}`},
{"node admin allowed", model.ApiScopeNodeAdmin, http.StatusOK, `{"reached":true}`},
{"monitor denied", model.ApiScopeMonitor, http.StatusForbidden, `{"msg":"this API token is not permitted to access this endpoint","success":false}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
plaintext := tc.name + "-token"
if err := database.GetDB().Create(&model.ApiToken{
Name: tc.name,
Token: crypto.HashTokenSHA256(plaintext),
Enabled: true,
Scope: tc.scope,
}).Error; err != nil {
t.Fatalf("seed token: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/panel/api/server/updatePanel", nil)
req.Header.Set("Authorization", "Bearer "+plaintext)
w := httptest.NewRecorder()
engine.ServeHTTP(w, req)
if w.Code != tc.wantCode || w.Body.String() != tc.wantBody {
t.Fatalf("response = %d %s, want %d %s", w.Code, w.Body.String(), tc.wantCode, tc.wantBody)
}
})
}
}
// TestCheckAPIAuth_AcceptsVerifiedClientCert ensures verified mTLS authenticates
// as node-sync rather than bypassing scope checks as admin.
func TestCheckAPIAuth_AcceptsVerifiedClientCert(t *testing.T) {
@@ -135,6 +172,48 @@ func TestCheckAPIAuth_AcceptsVerifiedClientCert(t *testing.T) {
if w.Code != http.StatusForbidden {
t.Fatalf("updatePanel status = %d, want 403; body=%s", w.Code, w.Body.String())
}
for _, legacyScope := range []string{model.ApiScopeAdmin, model.ApiScopeMonitor, model.ApiScopeNodeSync} {
plaintext := "mtls-" + legacyScope + "-token"
if err := database.GetDB().Create(&model.ApiToken{
Name: "mtls-" + legacyScope,
Token: crypto.HashTokenSHA256(plaintext),
Enabled: true,
Scope: legacyScope,
}).Error; err != nil {
t.Fatalf("seed %s token: %v", legacyScope, err)
}
legacy := httptest.NewRequest(http.MethodGet, "/panel/api/server/status", nil)
legacy.Header.Set("Authorization", "Bearer "+plaintext)
legacy.TLS = &tls.ConnectionState{
VerifiedChains: [][]*x509.Certificate{{&x509.Certificate{}}},
}
w = httptest.NewRecorder()
engine.ServeHTTP(w, legacy)
if w.Code != http.StatusOK || w.Body.String() != `{"api_authed":true,"scope":"node-sync"}` {
t.Fatalf("mTLS %s response = %d %s, want preserved node-sync scope", legacyScope, w.Code, w.Body.String())
}
}
const plaintext = "mtls-node-admin-token"
if err := database.GetDB().Create(&model.ApiToken{
Name: "mtls-node-admin",
Token: crypto.HashTokenSHA256(plaintext),
Enabled: true,
Scope: model.ApiScopeNodeAdmin,
}).Error; err != nil {
t.Fatalf("seed node-admin token: %v", err)
}
allowed := httptest.NewRequest(http.MethodPost, "/panel/api/server/updatePanel", nil)
allowed.Header.Set("Authorization", "Bearer "+plaintext)
allowed.TLS = &tls.ConnectionState{
VerifiedChains: [][]*x509.Certificate{{&x509.Certificate{}}},
}
w = httptest.NewRecorder()
engine.ServeHTTP(w, allowed)
if w.Code != http.StatusOK || w.Body.String() != `{"reached":true}` {
t.Fatalf("mTLS node-admin response = %d %s, want 200 update launch", w.Code, w.Body.String())
}
}
func TestNodeSyncScopeUsesFullPathPatterns(t *testing.T) {
+3 -2
View File
@@ -64,10 +64,11 @@ func (a *InboundController) broadcastInboundsUpdate(userId int) {
// node-sync push, so the node stores the row instead of re-judging it.
func (a *InboundController) inboundServiceFor(c *gin.Context) *service.InboundService {
svc := a.inboundService
scope, _ := c.Get("api_token_scope")
scopeValue, _ := c.Get("api_token_scope")
scope, _ := scopeValue.(string)
// A master enrolled with an admin token (the -getApiToken default) has no
// node-sync scope, so it marks every request it sends instead.
svc.FromNodeSync = scope == model.ApiScopeNodeSync || c.GetHeader(wirecodec.MasterPushHeader) != ""
svc.FromNodeSync = model.IsNodeSyncApiScope(scope) || c.GetHeader(wirecodec.MasterPushHeader) != ""
return &svc
}
@@ -29,7 +29,11 @@ func TestNodeSyncPushSkipsOperatorTLSGuard(t *testing.T) {
runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
t.Cleanup(func() { runtime.SetManager(prev) })
for name, scope := range map[string]string{"node-sync": model.ApiScopeNodeSync, "admin": model.ApiScopeAdmin} {
for name, scope := range map[string]string{
"node-sync": model.ApiScopeNodeSync,
"node-admin": model.ApiScopeNodeAdmin,
"admin": model.ApiScopeAdmin,
} {
row := &model.ApiToken{Name: name, Token: crypto.HashTokenSHA256(name + "-token"), Enabled: true, Scope: scope}
if err := database.GetDB().Create(row).Error; err != nil {
t.Fatalf("seed %s token: %v", name, err)
@@ -79,6 +83,16 @@ func TestNodeSyncPushSkipsOperatorTLSGuard(t *testing.T) {
}
})
t.Run("a node admin push keeps node sync semantics", func(t *testing.T) {
body := add(t, "node-admin-token", 45003)
if !strings.Contains(body, `"success":true`) {
t.Fatalf("node-admin add rejected: %s", body)
}
if got := rows(t, "tls-legacy-45003"); got != 1 {
t.Fatalf("stored rows = %d, want 1", got)
}
})
t.Run("an operator token is still held to the guard", func(t *testing.T) {
body := add(t, "admin-token", 45002)
if !strings.Contains(body, `"success":false`) || !strings.Contains(body, "TLS") {
+3 -1
View File
@@ -60,8 +60,10 @@ func NormalizeScope(scope string) (string, error) {
return model.ApiScopeMonitor, nil
case model.ApiScopeNodeSync:
return model.ApiScopeNodeSync, nil
case model.ApiScopeNodeAdmin:
return model.ApiScopeNodeAdmin, nil
default:
return "", common.NewError("scope must be 'admin', 'monitor', or 'node-sync'")
return "", common.NewError("scope must be 'admin', 'monitor', 'node-sync', or 'node-admin'")
}
}
@@ -83,6 +83,23 @@ func TestAPITokenScopeExpiryAndExpectedRevoke(t *testing.T) {
}
}
func TestAPITokenNodeAdminScopeRoundTrips(t *testing.T) {
setupAPITokenTestDB(t)
svc := &ApiTokenService{}
created, err := svc.Create("node-updater", model.ApiScopeNodeAdmin, 0)
if err != nil {
t.Fatalf("create node-admin token: %v", err)
}
row, ok := svc.MatchToken(created.Token)
if !ok {
t.Fatal("fresh node-admin token did not authenticate")
}
if created.Scope != model.ApiScopeNodeAdmin || row.Scope != model.ApiScopeNodeAdmin {
t.Fatalf("created/matched scopes = %q/%q, want node-admin/node-admin", created.Scope, row.Scope)
}
}
func TestAPITokenDeleteAndEnableRequireExpectedScope(t *testing.T) {
setupAPITokenTestDB(t)
svc := &ApiTokenService{}
+4 -3
View File
@@ -126,8 +126,9 @@ func TestRecreateByNameAppliesGivenScope(t *testing.T) {
wantErr string
}{
{name: "replaces a monitor token as node-sync", seedScope: model.ApiScopeMonitor, scope: model.ApiScopeNodeSync, want: model.ApiScopeNodeSync},
{name: "replaces a monitor token as node-admin", seedScope: model.ApiScopeMonitor, scope: model.ApiScopeNodeAdmin, want: model.ApiScopeNodeAdmin},
{name: "creates a new token as monitor", scope: model.ApiScopeMonitor, want: model.ApiScopeMonitor},
{name: "refuses an unknown scope", seedScope: model.ApiScopeMonitor, scope: "root", want: model.ApiScopeMonitor, wantErr: "scope must be 'admin', 'monitor', or 'node-sync'"},
{name: "refuses an unknown scope", seedScope: model.ApiScopeMonitor, scope: "root", want: model.ApiScopeMonitor, wantErr: "scope must be 'admin', 'monitor', 'node-sync', or 'node-admin'"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -172,12 +173,12 @@ func TestRecreateByNameRefusesUnknownStoredScope(t *testing.T) {
dbtest.InitDB(t, config.GetDBPath())
db := database.GetDB()
stored := model.ApiToken{Name: "remote", Token: "stored-hash", Enabled: true, Scope: "node-admin"}
stored := model.ApiToken{Name: "remote", Token: "stored-hash", Enabled: true, Scope: "future-node-scope"}
if err := db.Create(&stored).Error; err != nil {
t.Fatalf("seed remote: %v", err)
}
const wantErr = `token "remote" has unknown scope "node-admin"`
const wantErr = `token "remote" has unknown scope "future-node-scope"`
_, err := (&ApiTokenService{}).RecreateByName("remote", "")
if err == nil || strings.TrimSpace(err.Error()) != wantErr {
t.Fatalf("error = %v, want %q", err, wantErr)
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "الاسم",
"apiTokenNamePlaceholder": "مثل central-panel-a",
"apiTokenNameRequired": "الاسم مطلوب",
"apiTokenScope": "نطاق الصلاحيات",
"apiTokenScopeAdmin": "مسؤول",
"apiTokenScopeAdminDesc": "وصول كامل إلى جميع نقاط API في اللوحة. استخدمه فقط للأتمتة الموثوقة.",
"apiTokenScopeMonitor": "مراقبة",
"apiTokenScopeMonitorDesc": "وصول للقراءة فقط إلى حالة النظام والإصدارات والمقاييس.",
"apiTokenScopeNodeSync": "مزامنة العقدة",
"apiTokenScopeNodeSyncDesc": "يزامن إعدادات العقدة والاتصالات الواردة والعملاء وحركة البيانات وحالة Xray. لا يمكنه تحديث برنامج اللوحة.",
"apiTokenScopeNodeAdmin": "مسؤول العقدة",
"apiTokenScopeNodeAdminDesc": "يتضمن صلاحيات مزامنة العقدة ويمكنه أيضًا بدء تحديثات برنامج اللوحة.",
"apiTokenEmpty": "لا توجد رموز بعد — أنشئ واحدًا لمصادقة الروبوتات أو اللوحات البعيدة.",
"apiTokenDeleteWarning": "أي عميل يستخدم هذا الرمز سيفقد المصادقة فورًا.",
"apiTokenCreatedTitle": "تم إنشاء الرمز",
+9
View File
@@ -1547,6 +1547,15 @@
"apiTokenName": "Name",
"apiTokenNamePlaceholder": "e.g. central-panel-a",
"apiTokenNameRequired": "Name is required",
"apiTokenScope": "Scope",
"apiTokenScopeAdmin": "Administrator",
"apiTokenScopeAdminDesc": "Full access to every panel API endpoint. Use only for trusted automation.",
"apiTokenScopeMonitor": "Monitoring",
"apiTokenScopeMonitorDesc": "Read-only access to system status, versions, and metrics.",
"apiTokenScopeNodeSync": "Node synchronization",
"apiTokenScopeNodeSyncDesc": "Synchronizes node configuration, inbounds, clients, traffic, and Xray state. Cannot update panel software.",
"apiTokenScopeNodeAdmin": "Node administrator",
"apiTokenScopeNodeAdminDesc": "Includes node synchronization permissions and can also start panel software updates.",
"apiTokenEmpty": "No tokens yet — create one to authenticate bots or remote panels.",
"apiTokenDeleteWarning": "Any caller using this token will stop authenticating immediately.",
"apiTokenCreatedTitle": "Token created",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Nombre",
"apiTokenNamePlaceholder": "por ejemplo central-panel-a",
"apiTokenNameRequired": "El nombre es obligatorio",
"apiTokenScope": "Alcance",
"apiTokenScopeAdmin": "Administrador",
"apiTokenScopeAdminDesc": "Acceso completo a todos los endpoints de la API del panel. Úselo solo para automatizaciones de confianza.",
"apiTokenScopeMonitor": "Monitorización",
"apiTokenScopeMonitorDesc": "Acceso de solo lectura al estado del sistema, versiones y métricas.",
"apiTokenScopeNodeSync": "Sincronización de nodo",
"apiTokenScopeNodeSyncDesc": "Sincroniza la configuración del nodo, entradas, clientes, tráfico y estado de Xray. No puede actualizar el software del panel.",
"apiTokenScopeNodeAdmin": "Administrador de nodo",
"apiTokenScopeNodeAdminDesc": "Incluye los permisos de sincronización de nodo y también puede iniciar actualizaciones del software del panel.",
"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.",
"apiTokenCreatedTitle": "Token creado",
+9
View File
@@ -1429,6 +1429,15 @@
"apiTokenName": "نام",
"apiTokenNamePlaceholder": "مثلاً central-panel-a",
"apiTokenNameRequired": "نام الزامی است",
"apiTokenScope": "محدوده دسترسی",
"apiTokenScopeAdmin": "مدیر",
"apiTokenScopeAdminDesc": "دسترسی کامل به همه نقاط پایانی API پنل. فقط برای اتوماسیون قابل اعتماد استفاده شود.",
"apiTokenScopeMonitor": "پایش",
"apiTokenScopeMonitorDesc": "دسترسی فقط خواندنی به وضعیت سیستم، نسخه‌ها و معیارها.",
"apiTokenScopeNodeSync": "همگام‌سازی گره",
"apiTokenScopeNodeSyncDesc": "پیکربندی گره، ورودی‌ها، کاربران، ترافیک و وضعیت Xray را همگام می‌کند. نمی‌تواند نرم‌افزار پنل را به‌روز کند.",
"apiTokenScopeNodeAdmin": "مدیر گره",
"apiTokenScopeNodeAdminDesc": "شامل مجوزهای همگام‌سازی گره است و می‌تواند به‌روزرسانی نرم‌افزار پنل را نیز آغاز کند.",
"apiTokenEmpty": "هنوز توکنی وجود ندارد — برای احراز هویت ربات‌ها یا پنل‌های راه دور یکی بسازید.",
"apiTokenDeleteWarning": "هر کلاینتی که از این توکن استفاده می‌کند بلافاصله احراز هویتش قطع می‌شود.",
"apiTokenCreatedTitle": "توکن ساخته شد",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Nama",
"apiTokenNamePlaceholder": "misalnya central-panel-a",
"apiTokenNameRequired": "Nama wajib diisi",
"apiTokenScope": "Cakupan",
"apiTokenScopeAdmin": "Administrator",
"apiTokenScopeAdminDesc": "Akses penuh ke semua endpoint API panel. Gunakan hanya untuk otomatisasi tepercaya.",
"apiTokenScopeMonitor": "Pemantauan",
"apiTokenScopeMonitorDesc": "Akses baca-saja ke status sistem, versi, dan metrik.",
"apiTokenScopeNodeSync": "Sinkronisasi node",
"apiTokenScopeNodeSyncDesc": "Menyinkronkan konfigurasi node, inbound, klien, trafik, dan status Xray. Tidak dapat memperbarui perangkat lunak panel.",
"apiTokenScopeNodeAdmin": "Administrator node",
"apiTokenScopeNodeAdminDesc": "Mencakup izin sinkronisasi node dan juga dapat memulai pembaruan perangkat lunak panel.",
"apiTokenEmpty": "Belum ada token — buat satu untuk mengautentikasi bot atau panel jarak jauh.",
"apiTokenDeleteWarning": "Setiap pemanggil yang menggunakan token ini akan berhenti terautentikasi segera.",
"apiTokenCreatedTitle": "Token dibuat",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "名前",
"apiTokenNamePlaceholder": "例: central-panel-a",
"apiTokenNameRequired": "名前は必須です",
"apiTokenScope": "スコープ",
"apiTokenScopeAdmin": "管理者",
"apiTokenScopeAdminDesc": "パネルのすべての API エンドポイントへのフルアクセス。信頼できる自動化にのみ使用してください。",
"apiTokenScopeMonitor": "監視",
"apiTokenScopeMonitorDesc": "システム状態、バージョン、メトリクスへの読み取り専用アクセス。",
"apiTokenScopeNodeSync": "ノード同期",
"apiTokenScopeNodeSyncDesc": "ノード設定、インバウンド、クライアント、トラフィック、Xray 状態を同期します。パネルソフトウェアは更新できません。",
"apiTokenScopeNodeAdmin": "ノード管理者",
"apiTokenScopeNodeAdminDesc": "ノード同期権限に加え、パネルソフトウェアの更新も開始できます。",
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。",
"apiTokenCreatedTitle": "トークンを作成しました",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Nome",
"apiTokenNamePlaceholder": "ex.: central-panel-a",
"apiTokenNameRequired": "O nome é obrigatório",
"apiTokenScope": "Escopo",
"apiTokenScopeAdmin": "Administrador",
"apiTokenScopeAdminDesc": "Acesso completo a todos os endpoints da API do painel. Use apenas para automações confiáveis.",
"apiTokenScopeMonitor": "Monitoramento",
"apiTokenScopeMonitorDesc": "Acesso somente leitura ao estado do sistema, versões e métricas.",
"apiTokenScopeNodeSync": "Sincronização de nó",
"apiTokenScopeNodeSyncDesc": "Sincroniza configuração do nó, entradas, clientes, tráfego e estado do Xray. Não pode atualizar o software do painel.",
"apiTokenScopeNodeAdmin": "Administrador de nó",
"apiTokenScopeNodeAdminDesc": "Inclui as permissões de sincronização do nó e também pode iniciar atualizações do software do painel.",
"apiTokenEmpty": "Nenhum token ainda — crie um para autenticar bots ou painéis remotos.",
"apiTokenDeleteWarning": "Qualquer cliente usando este token deixará de se autenticar imediatamente.",
"apiTokenCreatedTitle": "Token criado",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Имя",
"apiTokenNamePlaceholder": "например, central-panel-a",
"apiTokenNameRequired": "Имя обязательно",
"apiTokenScope": "Область доступа",
"apiTokenScopeAdmin": "Администратор",
"apiTokenScopeAdminDesc": "Полный доступ ко всем API-методам панели. Используйте только для доверенной автоматизации.",
"apiTokenScopeMonitor": "Мониторинг",
"apiTokenScopeMonitorDesc": "Доступ только для чтения состояния системы, версий и метрик.",
"apiTokenScopeNodeSync": "Синхронизация узла",
"apiTokenScopeNodeSyncDesc": "Синхронизирует конфигурацию узла, входящие, клиентов, трафик и состояние Xray. Не может обновлять ПО панели.",
"apiTokenScopeNodeAdmin": "Администратор узла",
"apiTokenScopeNodeAdminDesc": "Включает права синхронизации узла и также может запускать обновление ПО панели.",
"apiTokenEmpty": "Токенов пока нет — создайте один для аутентификации ботов или удалённых панелей.",
"apiTokenDeleteWarning": "Любой клиент, использующий этот токен, немедленно потеряет аутентификацию.",
"apiTokenCreatedTitle": "Токен создан",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Ad",
"apiTokenNamePlaceholder": "örn. central-panel-a",
"apiTokenNameRequired": "Ad zorunludur",
"apiTokenScope": "Kapsam",
"apiTokenScopeAdmin": "Yönetici",
"apiTokenScopeAdminDesc": "Paneldeki tüm API uç noktalarına tam erişim. Yalnızca güvenilir otomasyonlarda kullanın.",
"apiTokenScopeMonitor": "İzleme",
"apiTokenScopeMonitorDesc": "Sistem durumu, sürümler ve metriklere salt okunur erişim.",
"apiTokenScopeNodeSync": "Düğüm senkronizasyonu",
"apiTokenScopeNodeSyncDesc": "Düğüm yapılandırmasını, gelen bağlantıları, istemcileri, trafiği ve Xray durumunu senkronize eder. Panel yazılımını güncelleyemez.",
"apiTokenScopeNodeAdmin": "Düğüm yöneticisi",
"apiTokenScopeNodeAdminDesc": "Düğüm senkronizasyonu izinlerini içerir ve panel yazılımı güncellemelerini de başlatabilir.",
"apiTokenEmpty": "Henüz token yok — botları veya uzak panelleri doğrulamak için bir tane oluşturun.",
"apiTokenDeleteWarning": "Bu token'ı kullanan tüm bağlantılar anında kimlik doğrulamasını kaybeder.",
"apiTokenCreatedTitle": "Token Oluşturuldu",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Назва",
"apiTokenNamePlaceholder": "наприклад, central-panel-a",
"apiTokenNameRequired": "Назва обов'язкова",
"apiTokenScope": "Область доступу",
"apiTokenScopeAdmin": "Адміністратор",
"apiTokenScopeAdminDesc": "Повний доступ до всіх API-методів панелі. Використовуйте лише для довіреної автоматизації.",
"apiTokenScopeMonitor": "Моніторинг",
"apiTokenScopeMonitorDesc": "Доступ лише для читання стану системи, версій і метрик.",
"apiTokenScopeNodeSync": "Синхронізація вузла",
"apiTokenScopeNodeSyncDesc": "Синхронізує конфігурацію вузла, вхідні, клієнтів, трафік і стан Xray. Не може оновлювати ПЗ панелі.",
"apiTokenScopeNodeAdmin": "Адміністратор вузла",
"apiTokenScopeNodeAdminDesc": "Включає права синхронізації вузла й також може запускати оновлення ПЗ панелі.",
"apiTokenEmpty": "Поки немає токенів — створіть один для автентифікації ботів або віддалених панелей.",
"apiTokenDeleteWarning": "Будь-який клієнт, що використовує цей токен, негайно втратить автентифікацію.",
"apiTokenCreatedTitle": "Токен створено",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "Tên",
"apiTokenNamePlaceholder": "ví dụ: central-panel-a",
"apiTokenNameRequired": "Tên là bắt buộc",
"apiTokenScope": "Phạm vi",
"apiTokenScopeAdmin": "Quản trị viên",
"apiTokenScopeAdminDesc": "Toàn quyền truy cập mọi endpoint API của panel. Chỉ dùng cho tự động hóa đáng tin cậy.",
"apiTokenScopeMonitor": "Giám sát",
"apiTokenScopeMonitorDesc": "Quyền chỉ đọc trạng thái hệ thống, phiên bản và số liệu.",
"apiTokenScopeNodeSync": "Đồng bộ node",
"apiTokenScopeNodeSyncDesc": "Đồng bộ cấu hình node, inbound, client, lưu lượng và trạng thái Xray. Không thể cập nhật phần mềm panel.",
"apiTokenScopeNodeAdmin": "Quản trị node",
"apiTokenScopeNodeAdminDesc": "Bao gồm quyền đồng bộ node và cũng có thể khởi chạy cập nhật phần mềm panel.",
"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.",
"apiTokenCreatedTitle": "Đã tạo token",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "名称",
"apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名称必填",
"apiTokenScope": "权限范围",
"apiTokenScopeAdmin": "管理员",
"apiTokenScopeAdminDesc": "可访问面板的所有 API 端点。仅用于可信的自动化。",
"apiTokenScopeMonitor": "监控",
"apiTokenScopeMonitorDesc": "只读访问系统状态、版本和指标。",
"apiTokenScopeNodeSync": "节点同步",
"apiTokenScopeNodeSyncDesc": "同步节点配置、入站、客户端、流量和 Xray 状态。不能更新面板软件。",
"apiTokenScopeNodeAdmin": "节点管理员",
"apiTokenScopeNodeAdminDesc": "包含节点同步权限,还可以启动面板软件更新。",
"apiTokenEmpty": "暂无令牌 — 创建一个用于认证机器人或远程面板。",
"apiTokenDeleteWarning": "使用此令牌的任何调用方将立即无法认证。",
"apiTokenCreatedTitle": "令牌已创建",
+9
View File
@@ -1425,6 +1425,15 @@
"apiTokenName": "名稱",
"apiTokenNamePlaceholder": "例如 central-panel-a",
"apiTokenNameRequired": "名稱必填",
"apiTokenScope": "權限範圍",
"apiTokenScopeAdmin": "管理員",
"apiTokenScopeAdminDesc": "可存取面板的所有 API 端點。僅用於可信的自動化。",
"apiTokenScopeMonitor": "監控",
"apiTokenScopeMonitorDesc": "唯讀存取系統狀態、版本和指標。",
"apiTokenScopeNodeSync": "節點同步",
"apiTokenScopeNodeSyncDesc": "同步節點設定、入站、用戶端、流量和 Xray 狀態。無法更新面板軟體。",
"apiTokenScopeNodeAdmin": "節點管理員",
"apiTokenScopeNodeAdminDesc": "包含節點同步權限,也可以啟動面板軟體更新。",
"apiTokenEmpty": "尚無令牌 — 建立一個以認證機器人或遠端面板。",
"apiTokenDeleteWarning": "使用此令牌的任何呼叫方將立即無法認證。",
"apiTokenCreatedTitle": "權杖已建立",