From c752674139739233154273c879d3cf675bfb539b Mon Sep 17 00:00:00 2001 From: UNITRONIX <36471318+UNITRONIX@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:13:33 +0200 Subject: [PATCH] fix: RustDesk Pro ID change and panel parity (Refs #213) Accept RustDesk 1.4.7 empty-PK ID changes on the Go signal server, cascade panel DB rows on client/panel renames, and align Node panel with live events, strategies UI, org address book editor, and device-group accessible API payload. --- CHANGELOG.md | 8 +- .../api/client_group_handlers.go | 58 +++- betterdesk-server/api/server.go | 15 +- betterdesk-server/db/database.go | 6 +- betterdesk-server/db/postgres.go | 70 ++++- betterdesk-server/db/sqlite.go | 74 ++++- betterdesk-server/db/sqlite_test.go | 67 ++++- betterdesk-server/signal/handler.go | 49 +++- betterdesk-server/signal/handler_test.go | 114 ++++++++ .../signal/peer_id_registration.go | 68 +++++ betterdesk-server/signal/ws.go | 3 +- web-nodejs/lang/ar.json | 52 +++- web-nodejs/lang/cs.json | 52 +++- web-nodejs/lang/da.json | 52 +++- web-nodejs/lang/de.json | 52 +++- web-nodejs/lang/en.json | 50 +++- web-nodejs/lang/es.json | 52 +++- web-nodejs/lang/fi.json | 52 +++- web-nodejs/lang/fr.json | 52 +++- web-nodejs/lang/hi.json | 52 +++- web-nodejs/lang/hu.json | 52 +++- web-nodejs/lang/id.json | 52 +++- web-nodejs/lang/it.json | 52 +++- web-nodejs/lang/ja.json | 52 +++- web-nodejs/lang/ko.json | 52 +++- web-nodejs/lang/nb.json | 52 +++- web-nodejs/lang/nl.json | 52 +++- web-nodejs/lang/pl.json | 52 +++- web-nodejs/lang/pt.json | 52 +++- web-nodejs/lang/ro.json | 52 +++- web-nodejs/lang/sv.json | 52 +++- web-nodejs/lang/th.json | 52 +++- web-nodejs/lang/tr.json | 52 +++- web-nodejs/lang/uk.json | 52 +++- web-nodejs/lang/vi.json | 52 +++- web-nodejs/lang/zh-TW.json | 52 +++- web-nodejs/lang/zh.json | 52 +++- web-nodejs/public/css/devices.css | 116 ++++++++ web-nodejs/public/css/organizations.css | 36 +++ web-nodejs/public/js/deviceDetail.js | 49 ++++ web-nodejs/public/js/devices.js | 232 +++++++++++++++ web-nodejs/public/js/organizationDetail.js | 159 ++++++++++- web-nodejs/routes/bd-api.routes.js | 29 +- web-nodejs/routes/devices.routes.js | 166 +++++++++++ web-nodejs/routes/rustdesk-api.routes.js | 40 ++- web-nodejs/services/database.js | 9 + web-nodejs/services/dbAdapter.js | 263 ++++++++++++++++++ web-nodejs/services/deviceStatusPush.js | 29 +- web-nodejs/tests/bd-api.routes.test.js | 58 ++++ web-nodejs/tests/devices.routes.test.js | 21 +- web-nodejs/views/devices.ejs | 17 ++ 51 files changed, 2997 insertions(+), 109 deletions(-) create mode 100644 betterdesk-server/signal/peer_id_registration.go create mode 100644 web-nodejs/tests/bd-api.routes.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f74afe09..dc1ae6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ ## [Unreleased] +### Fixed +- **Device ID change (#213):** RustDesk 1.4.x client rename works again — the server accepts `RegisterPk` ID-change requests with an empty PK (stock client wire format) instead of returning `NOT_SUPPORT` / “Not yet supported by the server”. +- **Panel ID change:** Permanent delete now clears `id_change_history` so released IDs can be reused; panel/API ID changes cascade to enrollment tokens, org assignments, and linked peers; renamed-ID registration allows the same device (matching IP or PK/UUID) to stay connected after a panel-side rename. +- **Panel sync:** Go `peer_id_changed` events now cascade panel DB rows in real time; periodic Go→panel sync applies client-side renames; hard delete purges stale panel `peer` rows; BD-API `/register` uses the same identity-aware rename guard as the signal server. + ### Changed -- _(none yet)_ +- **Panel UX:** Device list reloads on live ID-change events; online-device warning before change-ID from both list and detail views. +- **RustDesk Pro parity:** Panel strategies manager on Devices page; org address book structured editor; `/api/device-group/accessible` returns accessible payload (matches Go); Go API supports `POST /api/device-group`, strategy update/delete; device detail panel receives live status and ID-change events. --- diff --git a/betterdesk-server/api/client_group_handlers.go b/betterdesk-server/api/client_group_handlers.go index 81a9e796..875fbca3 100644 --- a/betterdesk-server/api/client_group_handlers.go +++ b/betterdesk-server/api/client_group_handlers.go @@ -130,9 +130,10 @@ func (s *Server) handleStrategiesGet(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"data": data, "total": len(data)}) } -// handleStrategiesPost creates a strategy. Auth enforced by route wrapper. +// handleStrategiesPost creates or updates a strategy. Auth enforced by route wrapper. func (s *Server) handleStrategiesPost(w http.ResponseWriter, r *http.Request) { var body struct { + GUID string `json:"guid"` Name string `json:"name"` UserGroupGUID string `json:"user_group_guid"` DeviceGroupGUID string `json:"device_group_guid"` @@ -156,6 +157,38 @@ func (s *Server) handleStrategiesPost(w http.ResponseWriter, r *http.Request) { if len(body.Permissions) > 0 && string(body.Permissions) != "null" { perms = string(body.Permissions) } + + guid := truncStr(body.GUID, maxIDLen) + if guid != "" { + existing, err := s.db.GetStrategy(guid) + if err != nil { + writeInternalError(w, err, "GetStrategy") + return + } + if existing == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "Strategy not found"}) + return + } + st := &db.Strategy{ + Name: body.Name, + UserGroupGUID: truncStr(body.UserGroupGUID, maxIDLen), + DeviceGroupGUID: truncStr(body.DeviceGroupGUID, maxIDLen), + Enabled: enabled, + Permissions: perms, + } + if err := s.db.UpdateStrategy(guid, st); err != nil { + writeInternalError(w, err, "UpdateStrategy") + return + } + updated, err := s.db.GetStrategy(guid) + if err != nil { + writeInternalError(w, err, "GetStrategy") + return + } + writeJSON(w, http.StatusOK, updated) + return + } + st := &db.Strategy{ Name: body.Name, UserGroupGUID: truncStr(body.UserGroupGUID, maxIDLen), @@ -169,3 +202,26 @@ func (s *Server) handleStrategiesPost(w http.ResponseWriter, r *http.Request) { } writeJSON(w, http.StatusOK, st) } + +// handleStrategiesDelete removes a strategy by GUID. +func (s *Server) handleStrategiesDelete(w http.ResponseWriter, r *http.Request) { + guid := truncStr(r.PathValue("guid"), maxIDLen) + if guid == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "guid is required"}) + return + } + existing, err := s.db.GetStrategy(guid) + if err != nil { + writeInternalError(w, err, "GetStrategy") + return + } + if existing == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "Strategy not found"}) + return + } + if err := s.db.DeleteStrategy(guid); err != nil { + writeInternalError(w, err, "DeleteStrategy") + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) +} diff --git a/betterdesk-server/api/server.go b/betterdesk-server/api/server.go index a7041c39..6df47d0a 100644 --- a/betterdesk-server/api/server.go +++ b/betterdesk-server/api/server.go @@ -361,6 +361,7 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("POST /api/group/get", s.handleClientGroupList) mux.HandleFunc("GET /api/device-group", s.handleClientGroupList) mux.HandleFunc("GET /api/device-group/accessible", s.handleClientGroupList) + mux.HandleFunc("POST /api/device-group", s.requirePanelAdmin(s.handleDeviceGroupsPost)) mux.HandleFunc("GET /api/peers/list", s.handleClientGroupPeers) mux.HandleFunc("POST /api/peers/list", s.handleClientGroupPeers) @@ -378,6 +379,7 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("POST /api/user-groups", s.requirePanelAdmin(s.handleUserGroupsPost)) mux.HandleFunc("GET /api/strategies", s.handleStrategiesGet) mux.HandleFunc("POST /api/strategies", s.requirePanelAdmin(s.handleStrategiesPost)) + mux.HandleFunc("DELETE /api/strategies/{guid}", s.requirePanelAdmin(s.handleStrategiesDelete)) mux.HandleFunc("POST /api/audit/conn", s.handleAuditConnPost) mux.HandleFunc("GET /api/audit/conn", s.requirePermission(auth.PermAuditView, s.handleAuditConnGet)) mux.HandleFunc("POST /api/audit/file", s.handleAuditFilePost) @@ -1294,7 +1296,7 @@ func (s *Server) handleChangePeerID(w http.ResponseWriter, r *http.Request) { return } - if err := s.db.ChangePeerID(oldID, body.NewID); err != nil { + if err := s.db.ChangePeerID(oldID, body.NewID, "panel"); err != nil { if errors.Is(err, db.ErrPeerIDExists) { writeJSON(w, http.StatusConflict, map[string]string{"error": "Device ID already exists"}) return @@ -1324,6 +1326,17 @@ func (s *Server) handleChangePeerID(w http.ResponseWriter, r *http.Request) { s.auditLog.Log(audit.ActionPeerIDChanged, s.remoteIP(r), oldID, map[string]string{"new_id": body.NewID}) } + if s.eventBus != nil { + s.eventBus.Publish(eventsModule.Event{ + Type: eventsModule.EventPeerIDChanged, + Data: map[string]string{ + "old_id": oldID, + "new_id": body.NewID, + "source": "panel", + }, + }) + } + writeJSON(w, http.StatusOK, map[string]string{ "status": "changed", "old_id": oldID, diff --git a/betterdesk-server/db/database.go b/betterdesk-server/db/database.go index 86cae862..f3fffdd2 100644 --- a/betterdesk-server/db/database.go +++ b/betterdesk-server/db/database.go @@ -467,11 +467,15 @@ type Database interface { RestorePeer(id string) error // ID change - ChangePeerID(oldID, newID string) error + ChangePeerID(oldID, newID, reason string) error GetIDChangeHistory(id string) ([]*IDChangeHistory, error) // IsRenamedPeerID returns true if the given ID was previously used and // changed to a different one (appears as old_id in id_change_history). IsRenamedPeerID(id string) (bool, error) + // GetLatestRenameTarget returns the most recent new_id for a renamed old_id. + GetLatestRenameTarget(oldID string) (string, error) + // ReleasePeerID clears id_change_history rows involving id so the ID can be reused. + ReleasePeerID(id string) error // CDAP: linked device queries GetLinkedPeers(id string) ([]*Peer, error) diff --git a/betterdesk-server/db/postgres.go b/betterdesk-server/db/postgres.go index 5c3d47ae..77cc9ac1 100644 --- a/betterdesk-server/db/postgres.go +++ b/betterdesk-server/db/postgres.go @@ -751,10 +751,23 @@ func (pg *PostgresDB) DeletePeer(id string) error { return err } -// HardDeletePeer permanently removes a peer from the database. +// HardDeletePeer permanently removes a peer from the database and releases +// any id_change_history reservations for that ID (#213). func (pg *PostgresDB) HardDeletePeer(id string) error { - _, err := pg.pool.Exec(pg.ctx, `DELETE FROM peers WHERE id = $1`, id) - return err + tx, err := pg.pool.Begin(pg.ctx) + if err != nil { + return err + } + defer tx.Rollback(pg.ctx) + + if _, err := tx.Exec(pg.ctx, `DELETE FROM peers WHERE id = $1`, id); err != nil { + return err + } + if _, err := tx.Exec(pg.ctx, + `DELETE FROM id_change_history WHERE old_id = $1 OR new_id = $1`, id); err != nil { + return err + } + return tx.Commit(pg.ctx) } // ListPeers returns all peers, optionally including soft-deleted ones. @@ -952,9 +965,28 @@ func (pg *PostgresDB) UpdatePeerFields(id string, fields map[string]string) erro // ── ID Change ───────────────────────────────────────────────────────── +func (pg *PostgresDB) cascadePeerIDInTx(ctx context.Context, tx pgx.Tx, oldID, newID string) error { + stmts := []struct { + query string + args []any + }{ + {`UPDATE device_tokens SET peer_id = $1 WHERE peer_id = $2`, []any{newID, oldID}}, + {`UPDATE org_devices SET device_id = $1 WHERE device_id = $2`, []any{newID, oldID}}, + {`UPDATE peers SET linked_peer_id = $1 WHERE linked_peer_id = $2`, []any{newID, oldID}}, + {`UPDATE device_folder_assignments SET device_id = $1 WHERE device_id = $2`, []any{newID, oldID}}, + {`UPDATE device_group_members SET peer_id = $1 WHERE peer_id = $2`, []any{newID, oldID}}, + } + for _, st := range stmts { + if _, err := tx.Exec(ctx, st.query, st.args...); err != nil { + return fmt.Errorf("db: cascadePeerID %s→%s: %w", oldID, newID, err) + } + } + return nil +} + // ChangePeerID changes a peer's ID and records it in history. // Uses a PostgreSQL transaction with row-level locking. -func (pg *PostgresDB) ChangePeerID(oldID, newID string) error { +func (pg *PostgresDB) ChangePeerID(oldID, newID, reason string) error { tx, err := pg.pool.Begin(pg.ctx) if err != nil { return err @@ -996,9 +1028,13 @@ func (pg *PostgresDB) ChangePeerID(oldID, newID string) error { return fmt.Errorf("db: ChangePeerID delete: %w", err) } + if err := pg.cascadePeerIDInTx(pg.ctx, tx, oldID, newID); err != nil { + return err + } + if _, err := tx.Exec(pg.ctx, - `INSERT INTO id_change_history (old_id, new_id) VALUES ($1, $2)`, - oldID, newID); err != nil { + `INSERT INTO id_change_history (old_id, new_id, reason) VALUES ($1, $2, $3)`, + oldID, newID, reason); err != nil { return fmt.Errorf("db: ChangePeerID history: %w", err) } @@ -1038,6 +1074,28 @@ func (pg *PostgresDB) IsRenamedPeerID(id string) (bool, error) { return count > 0, nil } +// GetLatestRenameTarget returns the most recent new_id for old_id. +func (pg *PostgresDB) GetLatestRenameTarget(oldID string) (string, error) { + var newID string + err := pg.pool.QueryRow(pg.ctx, + `SELECT new_id FROM id_change_history WHERE old_id = $1 ORDER BY id DESC LIMIT 1`, + oldID).Scan(&newID) + if err == pgx.ErrNoRows { + return "", nil + } + if err != nil { + return "", err + } + return newID, nil +} + +// ReleasePeerID clears id_change_history rows involving id. +func (pg *PostgresDB) ReleasePeerID(id string) error { + _, err := pg.pool.Exec(pg.ctx, + `DELETE FROM id_change_history WHERE old_id = $1 OR new_id = $1`, id) + return err +} + // GetLinkedPeers returns all non-deleted peers that have linked_peer_id matching the given ID. func (pg *PostgresDB) GetLinkedPeers(id string) ([]*Peer, error) { rows, err := pg.pool.Query(pg.ctx, ` diff --git a/betterdesk-server/db/sqlite.go b/betterdesk-server/db/sqlite.go index 67006b40..d1189e07 100644 --- a/betterdesk-server/db/sqlite.go +++ b/betterdesk-server/db/sqlite.go @@ -746,13 +746,26 @@ func (s *SQLiteDB) DeletePeer(id string) error { return err } -// HardDeletePeer permanently removes a peer from the database. +// HardDeletePeer permanently removes a peer from the database and releases +// any id_change_history reservations for that ID (#213). func (s *SQLiteDB) HardDeletePeer(id string) error { s.mu.Lock() defer s.mu.Unlock() - _, err := s.db.Exec(`DELETE FROM peers WHERE id = ?`, id) - return err + tx, err := s.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if _, err := tx.Exec(`DELETE FROM peers WHERE id = ?`, id); err != nil { + return err + } + if _, err := tx.Exec( + `DELETE FROM id_change_history WHERE old_id = ? OR new_id = ?`, id, id); err != nil { + return err + } + return tx.Commit() } // ListPeers returns all peers, optionally including soft-deleted ones. @@ -1024,8 +1037,25 @@ func (s *SQLiteDB) UpdatePeerFields(id string, fields map[string]string) error { return err } +func (s *SQLiteDB) cascadePeerIDInTx(tx *sql.Tx, oldID, newID string) error { + stmts := []struct { + query string + args []any + }{ + {`UPDATE device_tokens SET peer_id = ? WHERE peer_id = ?`, []any{newID, oldID}}, + {`UPDATE org_devices SET device_id = ? WHERE device_id = ?`, []any{newID, oldID}}, + {`UPDATE peers SET linked_peer_id = ? WHERE linked_peer_id = ?`, []any{newID, oldID}}, + } + for _, st := range stmts { + if _, err := tx.Exec(st.query, st.args...); err != nil { + return fmt.Errorf("db: cascadePeerID %s→%s: %w", oldID, newID, err) + } + } + return nil +} + // ChangePeerID changes a peer's ID and records it in history. -func (s *SQLiteDB) ChangePeerID(oldID, newID string) error { +func (s *SQLiteDB) ChangePeerID(oldID, newID, reason string) error { s.mu.Lock() defer s.mu.Unlock() @@ -1074,10 +1104,14 @@ func (s *SQLiteDB) ChangePeerID(oldID, newID string) error { return fmt.Errorf("db: ChangePeerID delete: %w", err) } + if err := s.cascadePeerIDInTx(tx, oldID, newID); err != nil { + return err + } + // Record in history if _, err := tx.Exec( - `INSERT INTO id_change_history (old_id, new_id) VALUES (?, ?)`, - oldID, newID); err != nil { + `INSERT INTO id_change_history (old_id, new_id, reason) VALUES (?, ?, ?)`, + oldID, newID, reason); err != nil { return fmt.Errorf("db: ChangePeerID history: %w", err) } @@ -1126,6 +1160,34 @@ func (s *SQLiteDB) IsRenamedPeerID(id string) (bool, error) { return count > 0, nil } +// GetLatestRenameTarget returns the most recent new_id for old_id. +func (s *SQLiteDB) GetLatestRenameTarget(oldID string) (string, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var newID string + err := s.db.QueryRow( + `SELECT new_id FROM id_change_history WHERE old_id = ? ORDER BY rowid DESC LIMIT 1`, + oldID).Scan(&newID) + if err == sql.ErrNoRows { + return "", nil + } + if err != nil { + return "", err + } + return newID, nil +} + +// ReleasePeerID clears id_change_history rows involving id. +func (s *SQLiteDB) ReleasePeerID(id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + _, err := s.db.Exec( + `DELETE FROM id_change_history WHERE old_id = ? OR new_id = ?`, id, id) + return err +} + // GetLinkedPeers returns all non-deleted peers that have linked_peer_id matching the given ID. func (s *SQLiteDB) GetLinkedPeers(id string) ([]*Peer, error) { s.mu.RLock() diff --git a/betterdesk-server/db/sqlite_test.go b/betterdesk-server/db/sqlite_test.go index dd25cfbd..14eb3606 100644 --- a/betterdesk-server/db/sqlite_test.go +++ b/betterdesk-server/db/sqlite_test.go @@ -406,7 +406,7 @@ func TestChangePeerID(t *testing.T) { db.UpsertPeer(&Peer{ID: "OLD1", Hostname: "mypc", Status: "ONLINE"}) - if err := db.ChangePeerID("OLD1", "NEW1"); err != nil { + if err := db.ChangePeerID("OLD1", "NEW1", ""); err != nil { t.Fatal(err) } @@ -441,7 +441,7 @@ func TestChangePeerIDDuplicate(t *testing.T) { db.UpsertPeer(&Peer{ID: "A1", Status: "ONLINE"}) db.UpsertPeer(&Peer{ID: "B1", Status: "ONLINE"}) - err := db.ChangePeerID("A1", "B1") + err := db.ChangePeerID("A1", "B1", "") if err == nil { t.Error("expected error when changing to existing ID") } @@ -468,12 +468,12 @@ func TestChangePeerIDSoftDeletedTarget(t *testing.T) { t.Fatalf("MACPRO state = %s, want %s", state, PeerIDSoftDeleted) } - err = db.ChangePeerID("NEWCLIENT", "MACPRO") + err = db.ChangePeerID("NEWCLIENT", "MACPRO", "") if !errors.Is(err, ErrPeerIDSoftDeleted) { t.Fatalf("ChangePeerID to soft-deleted target error = %v, want ErrPeerIDSoftDeleted", err) } - if err := db.ChangePeerID("NEWCLIENT", "MACPRO1"); err != nil { + if err := db.ChangePeerID("NEWCLIENT", "MACPRO1", ""); err != nil { t.Fatalf("ChangePeerID to free target: %v", err) } peer, err := db.GetPeer("MACPRO1") @@ -495,7 +495,7 @@ func TestChangePeerIDSoftDeletedSourceDoesNotMoveReservation(t *testing.T) { t.Fatal(err) } - err := db.ChangePeerID("MACPRO", "MACPRO1") + err := db.ChangePeerID("MACPRO", "MACPRO1", "") if !errors.Is(err, ErrPeerNotFound) { t.Fatalf("ChangePeerID from soft-deleted source error = %v, want ErrPeerNotFound", err) } @@ -602,6 +602,63 @@ func TestHardDelete(t *testing.T) { } } +func TestHardDeleteReleasesIDHistory(t *testing.T) { + db := newTestDB(t) + + if err := db.UpsertPeer(&Peer{ID: "OLDREL", Status: "ONLINE"}); err != nil { + t.Fatal(err) + } + if err := db.ChangePeerID("OLDREL", "NEWREL", "client"); err != nil { + t.Fatal(err) + } + renamed, err := db.IsRenamedPeerID("OLDREL") + if err != nil || !renamed { + t.Fatalf("OLDREL should be reserved before hard delete: %v %v", renamed, err) + } + + if err := db.HardDeletePeer("NEWREL"); err != nil { + t.Fatal(err) + } + renamed, err = db.IsRenamedPeerID("OLDREL") + if err != nil { + t.Fatal(err) + } + if renamed { + t.Fatal("OLDREL should be released after permanent delete of successor") + } +} + +func TestChangePeerIDCascadesDeviceTokens(t *testing.T) { + db := newTestDB(t) + + if err := db.UpsertPeer(&Peer{ID: "OLDCAS", Status: "ONLINE"}); err != nil { + t.Fatal(err) + } + token := &DeviceToken{ + Token: "cascadetoken12345", + TokenHash: "hash-cascade-token", + Name: "Cascade test", + PeerID: "OLDCAS", + Status: TokenStatusPending, + MaxUses: 1, + } + if err := db.CreateDeviceToken(token); err != nil { + t.Fatal(err) + } + + if err := db.ChangePeerID("OLDCAS", "NEWCAS", "panel"); err != nil { + t.Fatal(err) + } + + got, err := db.GetDeviceTokenByPeerID("NEWCAS") + if err != nil || got == nil { + t.Fatalf("device token should follow ID change: %v %+v", err, got) + } + if stale, _ := db.GetDeviceTokenByPeerID("OLDCAS"); stale != nil { + t.Fatalf("token should not remain on old ID: %+v", stale) + } +} + // TestMigrateUpgradesLegacySchema simulates a database created by an older // version (without totp_secret, totp_enabled in users table). Migrate() // should add the missing columns so CreateUser and GetUser work correctly. diff --git a/betterdesk-server/signal/handler.go b/betterdesk-server/signal/handler.go index b580c7ab..7b7b0d36 100644 --- a/betterdesk-server/signal/handler.go +++ b/betterdesk-server/signal/handler.go @@ -272,9 +272,8 @@ func (s *Server) handleRegisterPeer(msg *pb.RegisterPeer, raddr *net.UDPAddr) { } // Check if this ID was previously changed to a different one (#97) — - // do not allow a device to come back under its old ID. - if renamed, _ := s.db.IsRenamedPeerID(id); renamed { - log.Printf("[signal] Rejected registration for renamed peer ID: %s from %s", id, raddr.IP) + // allow the same device to keep using the old ID until it adopts the new one (#213). + if s.rejectRenamedPeerRegistration(id, clientHost, nil, nil) { return } @@ -412,9 +411,8 @@ func (s *Server) processRegisterPk(msg *pb.RegisterPk, addrStr string) *pb.Rende return registerPkResponse(pb.RegisterPkResponse_NOT_SUPPORT) } - // Check if this ID was previously changed to a different one (#97) - if renamed, _ := s.db.IsRenamedPeerID(id); renamed { - log.Printf("[signal] Rejected PK registration for renamed peer ID: %s", id) + // Check if this ID was previously changed to a different one (#97). + if s.rejectRenamedPeerRegistration(id, clientHost, msg.Uuid, msg.Pk) { return registerPkResponse(pb.RegisterPkResponse_NOT_SUPPORT) } @@ -487,15 +485,26 @@ func (s *Server) processIDChange(msg *pb.RegisterPk) *pb.RendezvousMessage { if oldPeer == nil { return registerPkResponse(pb.RegisterPkResponse_NOT_SUPPORT) } - if oldPeer.UUID != "" && oldPeer.UUID != fmt.Sprintf("%x", msg.Uuid) { - log.Printf("[signal] Rejected ID change %s → %s: UUID mismatch", oldID, newID) - return registerPkResponse(pb.RegisterPkResponse_UUID_MISMATCH) - } - if len(oldPeer.PK) > 0 && !bytes.Equal(oldPeer.PK, msg.Pk) { + + // RustDesk 1.4.x change-ID sends RegisterPk without pk (see change_id_shared). + // When pk is omitted, use the stored key; when present, enforce a match (#213). + effectivePK := msg.Pk + if len(effectivePK) == 0 { + effectivePK = oldPeer.PK + } else if len(oldPeer.PK) > 0 && !bytes.Equal(oldPeer.PK, effectivePK) { log.Printf("[signal] Rejected ID change %s → %s: PK mismatch", oldID, newID) return registerPkResponse(pb.RegisterPkResponse_NOT_SUPPORT) } + // UUID: stock clients send machine_uid bytes on ID change (not 16-byte get_uuid()). + // Enforce match only for wire-format 16-byte UUIDs. + if len(msg.Uuid) == 16 && oldPeer.UUID != "" { + if !peerUUIDEqual(peerUUIDFromDB(oldPeer.UUID), msg.Uuid) { + log.Printf("[signal] Rejected ID change %s → %s: UUID mismatch", oldID, newID) + return registerPkResponse(pb.RegisterPkResponse_UUID_MISMATCH) + } + } + // Validate new ID doesn't exist existing := s.peers.Get(newID) if existing != nil { @@ -512,7 +521,7 @@ func (s *Server) processIDChange(msg *pb.RegisterPk) *pb.RendezvousMessage { } // Perform the change - if err := s.db.ChangePeerID(oldID, newID); err != nil { + if err := s.db.ChangePeerID(oldID, newID, "client"); err != nil { if errors.Is(err, db.ErrPeerIDExists) || errors.Is(err, db.ErrPeerIDSoftDeleted) { return registerPkResponse(pb.RegisterPkResponse_ID_EXISTS) } @@ -527,12 +536,24 @@ func (s *Server) processIDChange(msg *pb.RegisterPk) *pb.RendezvousMessage { oldEntry := s.peers.Remove(oldID) if oldEntry != nil { oldEntry.ID = newID - oldEntry.PK = msg.Pk - oldEntry.UUID = msg.Uuid + oldEntry.PK = effectivePK + if len(msg.Uuid) > 0 { + oldEntry.UUID = normalizePeerUUIDBytes(msg.Uuid) + } s.peers.Put(oldEntry) } log.Printf("[signal] ID changed: %s → %s", oldID, newID) + if s.eventBus != nil { + s.eventBus.Publish(events.Event{ + Type: events.EventPeerIDChanged, + Data: map[string]string{ + "old_id": oldID, + "new_id": newID, + "source": "client", + }, + }) + } return registerPkResponse(pb.RegisterPkResponse_OK) } diff --git a/betterdesk-server/signal/handler_test.go b/betterdesk-server/signal/handler_test.go index a5584b54..baa7a9d4 100644 --- a/betterdesk-server/signal/handler_test.go +++ b/betterdesk-server/signal/handler_test.go @@ -338,6 +338,120 @@ func TestProcessIDChangeRejectsPKMismatch(t *testing.T) { } } +func TestProcessIDChangeSuccessEmptyPK(t *testing.T) { + srv, database := newTestSignalServer(t, config.EnrollmentModeOpen) + + storedPK := bytes.Repeat([]byte{0x42}, 32) + if err := database.UpsertPeer(&db.Peer{ + ID: "MACPRO1", + Status: "ONLINE", + PK: storedPK, + UUID: "aabbccddeeff00112233445566778899", + }); err != nil { + t.Fatal(err) + } + + msg := &pb.RegisterPk{ + Id: "MACPRO", + OldId: "MACPRO1", + Uuid: []byte("machine-uid-bytes"), + } + resp := srv.processIDChange(msg) + if got := registerPkResult(resp); got != pb.RegisterPkResponse_OK { + t.Fatalf("ID change result = %v, want %v", got, pb.RegisterPkResponse_OK) + } + + peer, err := database.GetPeer("MACPRO") + if err != nil || peer == nil { + t.Fatalf("GetPeer MACPRO: %v %+v", err, peer) + } + if len(peer.PK) != 32 || !bytes.Equal(peer.PK, storedPK) { + t.Fatalf("PK not preserved: len=%d", len(peer.PK)) + } + if old, _ := database.GetPeer("MACPRO1"); old != nil { + t.Fatalf("MACPRO1 should be removed after ID change: %+v", old) + } +} + +func TestProcessIDChangeRejectsWrongPKWhenPKSent(t *testing.T) { + srv, database := newTestSignalServer(t, config.EnrollmentModeOpen) + + if err := database.UpsertPeer(&db.Peer{ + ID: "OLD213", + Status: "ONLINE", + PK: bytes.Repeat([]byte{0x99}, 32), + }); err != nil { + t.Fatal(err) + } + + msg := newRegisterPk("NEW213") + msg.OldId = "OLD213" + msg.Pk = bytes.Repeat([]byte{0x42}, 32) + resp := srv.processIDChange(msg) + if got := registerPkResult(resp); got != pb.RegisterPkResponse_NOT_SUPPORT { + t.Fatalf("ID change result = %v, want %v", got, pb.RegisterPkResponse_NOT_SUPPORT) + } +} + +func TestRejectRenamedPeerRegistrationAllowsSameIP(t *testing.T) { + srv, database := newTestSignalServer(t, config.EnrollmentModeOpen) + + if err := database.UpsertPeer(&db.Peer{ + ID: "MACPRO1", + Status: "ONLINE", + IP: "203.0.113.50", + PK: bytes.Repeat([]byte{0x42}, 32), + }); err != nil { + t.Fatal(err) + } + if err := database.ChangePeerID("MACPRO1", "MACPRO2", "panel"); err != nil { + t.Fatal(err) + } + + if srv.rejectRenamedPeerRegistration("MACPRO1", "203.0.113.50", nil, nil) { + t.Fatal("same IP should be allowed on renamed old ID") + } + if !srv.rejectRenamedPeerRegistration("MACPRO1", "198.51.100.99", nil, nil) { + t.Fatal("different IP should be rejected on renamed old ID") + } +} + +func TestTCPIDChangeEmptyPKRustDesk147(t *testing.T) { + srv, database := newTestSignalServer(t, config.EnrollmentModeOpen) + + storedPK := bytes.Repeat([]byte{0x42}, 32) + if err := database.UpsertPeer(&db.Peer{ + ID: "MACPRO1", + Status: "ONLINE", + PK: storedPK, + IP: "203.0.113.10", + }); err != nil { + t.Fatal(err) + } + + fakeAddr, err := net.ResolveTCPAddr("tcp", "203.0.113.10:50123") + if err != nil { + t.Fatal(err) + } + + resp := srv.handleRegisterPkTCP(&pb.RegisterPk{ + OldId: "MACPRO1", + Id: "MACPRO", + Uuid: []byte("machine-uid-bytes"), + }, fakeAddr) + if got := registerPkResult(resp); got != pb.RegisterPkResponse_OK { + t.Fatalf("TCP ID change result = %v, want OK", got) + } + + peer, err := database.GetPeer("MACPRO") + if err != nil || peer == nil { + t.Fatalf("GetPeer MACPRO: %v %+v", err, peer) + } + if !bytes.Equal(peer.PK, storedPK) { + t.Fatal("stored PK should be preserved when client omits pk") + } +} + func TestProcessRegisterPkManagedAllowsTokenBoundPeer(t *testing.T) { srv, database := newTestSignalServer(t, config.EnrollmentModeManaged) diff --git a/betterdesk-server/signal/peer_id_registration.go b/betterdesk-server/signal/peer_id_registration.go new file mode 100644 index 00000000..1ab0f5be --- /dev/null +++ b/betterdesk-server/signal/peer_id_registration.go @@ -0,0 +1,68 @@ +package signal + +import ( + "bytes" + "log" + "net" + "strings" +) + +// rejectRenamedPeerRegistration returns true when a peer must be blocked from +// registering under id because it was renamed and the caller is not the owner. +// +// RustDesk 1.4.x clients may keep using the old ID after a panel-initiated +// rename; allow the same device (matching PK/UUID or IP) to continue until it +// adopts the new ID (#213). +func (s *Server) rejectRenamedPeerRegistration(id, clientHost string, uuid, pk []byte) bool { + renamed, err := s.db.IsRenamedPeerID(id) + if err != nil { + log.Printf("[signal] IsRenamedPeerID(%s): %v", id, err) + return true + } + if !renamed { + return false + } + + newID, err := s.db.GetLatestRenameTarget(id) + if err != nil { + log.Printf("[signal] GetLatestRenameTarget(%s): %v", id, err) + return true + } + if newID == "" { + return true + } + + successor, err := s.db.GetPeer(newID) + if err != nil { + log.Printf("[signal] GetPeer(%s) for rename successor: %v", newID, err) + return true + } + if successor == nil { + // Successor removed — old ID is free to reuse. + return false + } + + if len(pk) > 0 && len(successor.PK) > 0 && bytes.Equal(successor.PK, pk) { + return false + } + if len(uuid) > 0 && successor.UUID != "" && peerUUIDEqual(peerUUIDFromDB(successor.UUID), uuid) { + return false + } + if len(pk) == 0 && len(uuid) == 0 && clientHost != "" && peerIPMatches(clientHost, successor.IP) { + return false + } + + log.Printf("[signal] Rejected registration for renamed peer ID: %s (successor %s)", id, newID) + return true +} + +func peerIPMatches(clientHost, storedIP string) bool { + if clientHost == "" || storedIP == "" { + return false + } + storedHost := storedIP + if h, _, err := net.SplitHostPort(storedIP); err == nil && h != "" { + storedHost = h + } + return clientHost == storedHost || strings.HasPrefix(storedIP, clientHost+":") +} diff --git a/betterdesk-server/signal/ws.go b/betterdesk-server/signal/ws.go index 55e0c815..3a45acab 100644 --- a/betterdesk-server/signal/ws.go +++ b/betterdesk-server/signal/ws.go @@ -371,8 +371,7 @@ func (s *Server) handleRegisterPeerWS(msg *pb.RegisterPeer, remoteAddr string) * return nil } - if renamed, _ := s.db.IsRenamedPeerID(id); renamed { - log.Printf("[signal] Rejected WS registration for renamed peer ID: %s from %s", id, remoteAddr) + if s.rejectRenamedPeerRegistration(id, clientHost, nil, nil) { return nil } diff --git a/web-nodejs/lang/ar.json b/web-nodejs/lang/ar.json index 4fe4f145..87141660 100644 --- a/web-nodejs/lang/ar.json +++ b/web-nodejs/lang/ar.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Arabic", "native_name": "العربية", @@ -245,6 +245,7 @@ "change_id_title": "تغيير معرف الجهاز", "new_id": "New ID", "change_id_hint": "6-16 حرفًا وأحرفًا وأرقامًا وشرطات وشرطات سفلية فقط", + "change_id_online_warn": "الجهاز متصل. يُفضّل تغيير المعرّف في عميل RustDesk. يحافظ التغيير من اللوحة على هوية الجهاز، لكن يجب على العميل اعتماد المعرّف الجديد.", "change_id_success": "تم تغيير معرف الجهاز بنجاح", "id_length_error": "يجب أن يتراوح طول المعرف من 6 إلى 16 حرفًا", "id_format_error": "يمكن أن يحتوي المعرف فقط على أحرف وأرقام وشرطات وشرطات سفلية", @@ -341,6 +342,41 @@ "group_invalid": "معرف المجموعة غير صالح", "group_not_found": "لم يتم العثور على المجموعة", "group_saved": "تم حفظ مجموعة الأجهزة", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "تم حفظ مجموعات الأجهزة", "group_membership_added": "تمت الإضافة إلى المجموعة", "group_membership_removed": "تمت الإزالة من المجموعة", @@ -2607,7 +2643,19 @@ "address_book_saved": "تم حفظ دفتر العناوين", "address_book_save_failed": "فشل حفظ دفتر العناوين", "address_book_imported": "تم استيراد أجهزة المنظمة", - "address_book_import_none": "لا توجد أجهزة منظمة جديدة للاستيراد" + "address_book_import_none": "لا توجد أجهزة منظمة جديدة للاستيراد", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "مشفرة من طرف إلى طرف", diff --git a/web-nodejs/lang/cs.json b/web-nodejs/lang/cs.json index c4d71ab4..e053cfed 100644 --- a/web-nodejs/lang/cs.json +++ b/web-nodejs/lang/cs.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "cs", "name": "Czech", @@ -238,6 +238,7 @@ "change_id_title": "Změnit ID zařízení", "new_id": "Nové ID", "change_id_hint": "6-16 znaků, pouze písmena, číslice, pomlčky a podtržítka", + "change_id_online_warn": "Zařízení je online. ID raději měňte v klientu RustDesk. Změna v panelu zachová identitu, ale klient musí přijmout nové ID.", "change_id_success": "ID zařízení úspěšně změněno", "id_length_error": "ID musí mít 6-16 znaků", "id_format_error": "ID může obsahovat pouze písmena, číslice, pomlčky a podtržítka", @@ -334,6 +335,41 @@ "group_invalid": "Neplatný identifikátor skupiny", "group_not_found": "Skupina nebyla nalezena", "group_saved": "Skupina zařízení uložena", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Skupiny zařízení uloženy", "group_membership_added": "Přidáno do skupiny", "group_membership_removed": "Odebráno ze skupiny", @@ -2600,7 +2636,19 @@ "address_book_saved": "Adresář uložen", "address_book_save_failed": "Adresář se nepodařilo uložit", "address_book_imported": "Zařízení organizace importována", - "address_book_import_none": "Žádná nová zařízení organizace k importu" + "address_book_import_none": "Žádná nová zařízení organizace k importu", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Koncové šifrování aktivní", diff --git a/web-nodejs/lang/da.json b/web-nodejs/lang/da.json index 15693dda..394a2262 100644 --- a/web-nodejs/lang/da.json +++ b/web-nodejs/lang/da.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Danish", "native_name": "Dansk", @@ -244,6 +244,7 @@ "change_id_title": "Skift enheds-id", "new_id": "New ID", "change_id_hint": "Kun 6-16 tegn, bogstaver, tal, bindestreger og understregninger", + "change_id_online_warn": "Enheden er online. Skift ID helst i RustDesk-klienten. Ændring i panelet bevarer identiteten, men klienten skal overtage det nye ID.", "change_id_success": "Enheds-id blev ændret", "id_length_error": "ID skal være på 6-16 tegn", "id_format_error": "ID kan kun indeholde bogstaver, tal, bindestreger og understregninger", @@ -340,6 +341,41 @@ "group_invalid": "Ugyldig gruppe-id", "group_not_found": "Gruppen blev ikke fundet", "group_saved": "Enhedsgruppen er gemt", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Enhedsgrupper blev gemt", "group_membership_added": "Tilføjet til gruppe", "group_membership_removed": "Fjernet fra gruppe", @@ -2606,7 +2642,19 @@ "address_book_saved": "Adressebog gemt", "address_book_save_failed": "Kunne ikke gemme adressebog", "address_book_imported": "Organisationsenheder importeret", - "address_book_import_none": "Ingen nye organisationsenheder at importere" + "address_book_import_none": "Ingen nye organisationsenheder at importere", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "End-to-end krypteret", diff --git a/web-nodejs/lang/de.json b/web-nodejs/lang/de.json index a51c4436..c5eea0d4 100644 --- a/web-nodejs/lang/de.json +++ b/web-nodejs/lang/de.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "de", "name": "German", @@ -238,6 +238,7 @@ "change_id_title": "Geräte-ID ändern", "new_id": "Neue ID", "change_id_hint": "6-16 Zeichen, nur Buchstaben, Zahlen, Bindestriche und Unterstriche", + "change_id_online_warn": "Das Gerät ist online. Ändern Sie die ID nach Möglichkeit im RustDesk-Client. Eine Änderung im Panel behält die Geräteidentität bei, der Client muss jedoch die neue ID übernehmen.", "change_id_success": "Geräte-ID erfolgreich geändert", "id_length_error": "ID muss 6-16 Zeichen lang sein", "id_format_error": "ID darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten", @@ -334,6 +335,41 @@ "group_invalid": "Ungültige Gruppenkennung", "group_not_found": "Gruppe nicht gefunden", "group_saved": "Gerätegruppe gespeichert", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Gerätegruppen gespeichert", "group_membership_added": "Zur Gruppe hinzugefügt", "group_membership_removed": "Aus Gruppe entfernt", @@ -2600,7 +2636,19 @@ "address_book_saved": "Adressbuch gespeichert", "address_book_save_failed": "Adressbuch konnte nicht gespeichert werden", "address_book_imported": "Organisationsgeräte importiert", - "address_book_import_none": "Keine neuen Organisationsgeräte zum Importieren" + "address_book_import_none": "Keine neuen Organisationsgeräte zum Importieren", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Ende-zu-Ende verschlüsselt", diff --git a/web-nodejs/lang/en.json b/web-nodejs/lang/en.json index 0d085c71..719eb3f1 100644 --- a/web-nodejs/lang/en.json +++ b/web-nodejs/lang/en.json @@ -248,6 +248,7 @@ "change_id_title": "Change Device ID", "new_id": "New ID", "change_id_hint": "6-16 characters, letters, numbers, dashes and underscores only", + "change_id_online_warn": "This device is online. Prefer changing the ID in the RustDesk client when possible. A panel-side change keeps the same device identity but the client must adopt the new ID to stay connected.", "change_id_success": "Device ID changed successfully", "id_length_error": "ID must be 6-16 characters long", "id_format_error": "ID can only contain letters, numbers, dashes and underscores", @@ -311,6 +312,41 @@ "group_invalid": "Invalid group identifier", "group_not_found": "Group not found", "group_saved": "Device group saved", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Device groups saved", "group_membership_added": "Added to group", "group_membership_removed": "Removed from group", @@ -2730,7 +2766,19 @@ "address_book_saved": "Address book saved", "address_book_save_failed": "Failed to save address book", "address_book_imported": "Organization devices imported", - "address_book_import_none": "No new organization devices to import" + "address_book_import_none": "No new organization devices to import", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "End-to-end encrypted", diff --git a/web-nodejs/lang/es.json b/web-nodejs/lang/es.json index 159a0905..ae286bd6 100644 --- a/web-nodejs/lang/es.json +++ b/web-nodejs/lang/es.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "es", "name": "Spanish", @@ -238,6 +238,7 @@ "change_id_title": "Cambiar ID del dispositivo", "new_id": "Nuevo ID", "change_id_hint": "6-16 caracteres, solo letras, números, guiones y guiones bajos", + "change_id_online_warn": "El dispositivo está en línea. Prefiera cambiar el ID en el cliente RustDesk. Un cambio en el panel conserva la identidad del dispositivo, pero el cliente debe adoptar el nuevo ID.", "change_id_success": "ID del dispositivo cambiado correctamente", "id_length_error": "El ID debe tener entre 6 y 16 caracteres", "id_format_error": "El ID solo puede contener letras, números, guiones y guiones bajos", @@ -334,6 +335,41 @@ "group_invalid": "Identificador de grupo no válido", "group_not_found": "Grupo no encontrado", "group_saved": "Grupo de dispositivos guardado", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Grupos de dispositivos guardados", "group_membership_added": "Añadido al grupo", "group_membership_removed": "Eliminado del grupo", @@ -2600,7 +2636,19 @@ "address_book_saved": "Libreta de direcciones guardada", "address_book_save_failed": "No se pudo guardar la libreta de direcciones", "address_book_imported": "Dispositivos de la organización importados", - "address_book_import_none": "No hay nuevos dispositivos de la organización para importar" + "address_book_import_none": "No hay nuevos dispositivos de la organización para importar", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Cifrado de extremo a extremo", diff --git a/web-nodejs/lang/fi.json b/web-nodejs/lang/fi.json index 9d039197..c78dff29 100644 --- a/web-nodejs/lang/fi.json +++ b/web-nodejs/lang/fi.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Finnish", "native_name": "Suomi", @@ -244,6 +244,7 @@ "change_id_title": "Vaihda laitetunnus", "new_id": "New ID", "change_id_hint": "Vain 6-16 merkkiä, kirjaimia, numeroita, väliviivoja ja alaviivoja", + "change_id_online_warn": "Laite on online-tilassa. Muuta ID mieluiten RustDesk-asiakkaassa. Paneelimuutos säilyttää identiteetin, mutta asiakkaan on otettava uusi ID käyttöön.", "change_id_success": "Laitetunnus vaihdettiin onnistuneesti", "id_length_error": "ID:n tulee olla 6-16 merkkiä pitkä", "id_format_error": "Tunnus voi sisältää vain kirjaimia, numeroita, väliviivoja ja alaviivoja", @@ -340,6 +341,41 @@ "group_invalid": "Virheellinen ryhmän tunniste", "group_not_found": "Ryhmää ei löydy", "group_saved": "Laiteryhmä tallennettu", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Laiteryhmät tallennettu", "group_membership_added": "Lisätty ryhmään", "group_membership_removed": "Poistettu ryhmästä", @@ -2606,7 +2642,19 @@ "address_book_saved": "Osoitekirja tallennettu", "address_book_save_failed": "Osoitekirjan tallennus epäonnistui", "address_book_imported": "Organisaation laitteet tuotu", - "address_book_import_none": "Ei uusia organisaation laitteita tuotavaksi" + "address_book_import_none": "Ei uusia organisaation laitteita tuotavaksi", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Päästä päähän salattu", diff --git a/web-nodejs/lang/fr.json b/web-nodejs/lang/fr.json index 88c189f5..ceb1f24b 100644 --- a/web-nodejs/lang/fr.json +++ b/web-nodejs/lang/fr.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "language": "Français", "code": "fr", @@ -244,6 +244,7 @@ "change_id_title": "Changer l'ID de l'appareil", "new_id": "Nouvel ID", "change_id_hint": "6-16 caractères, lettres, chiffres, tirets et underscores uniquement", + "change_id_online_warn": "L'appareil est en ligne. Privilégiez le changement d'ID dans le client RustDesk. Un changement via le panneau conserve l'identité, mais le client doit adopter le nouvel ID.", "change_id_success": "ID de l'appareil modifié avec succès", "id_length_error": "L'ID doit contenir entre 6 et 16 caractères", "id_format_error": "L'ID ne peut contenir que des lettres, des chiffres, des tirets et des underscores", @@ -307,6 +308,41 @@ "group_invalid": "Ungültige Gruppenkennung", "group_not_found": "Gruppe nicht gefunden", "group_saved": "Gerätegruppe gespeichert", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Gerätegruppen gespeichert", "group_membership_added": "Zur Gruppe hinzugefügt", "group_membership_removed": "Aus Gruppe entfernt", @@ -2688,7 +2724,19 @@ "address_book_saved": "Carnet d'adresses enregistré", "address_book_save_failed": "Impossible d'enregistrer le carnet d'adresses", "address_book_imported": "Appareils de l'organisation importés", - "address_book_import_none": "Aucun nouvel appareil de l'organisation à importer" + "address_book_import_none": "Aucun nouvel appareil de l'organisation à importer", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Chiffré de bout en bout", diff --git a/web-nodejs/lang/hi.json b/web-nodejs/lang/hi.json index b1d1b8fb..415ab63d 100644 --- a/web-nodejs/lang/hi.json +++ b/web-nodejs/lang/hi.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Hindi", "native_name": "हिन्दी", @@ -244,6 +244,7 @@ "change_id_title": "डिवाइस आईडी बदलें", "new_id": "New ID", "change_id_hint": "केवल 6-16 अक्षर, अक्षर, संख्याएँ, डैश और अंडरस्कोर", + "change_id_online_warn": "डिवाइस ऑनलाइन है। RustDesk क्लाइंट में ID बदलना बेहतर है। पैनल पर बदलाव पहचान बनाए रखता है, लेकिन क्लाइंट को नई ID अपनानी होगी।", "change_id_success": "डिवाइस आईडी सफलतापूर्वक बदल दी गई", "id_length_error": "आईडी 6-16 अक्षर लंबी होनी चाहिए", "id_format_error": "आईडी में केवल अक्षर, संख्याएं, डैश और अंडरस्कोर हो सकते हैं", @@ -340,6 +341,41 @@ "group_invalid": "अमान्य समूह पहचानकर्ता", "group_not_found": "समूह नहीं मिला", "group_saved": "डिवाइस समूह सहेजा गया", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "डिवाइस समूह सहेजे गए", "group_membership_added": "समूह में जोड़ा गया", "group_membership_removed": "समूह से हटाया गया", @@ -2606,7 +2642,19 @@ "address_book_saved": "पता पुस्तिका सहेजी गई", "address_book_save_failed": "पता पुस्तिका सहेजने में विफल", "address_book_imported": "संगठन डिवाइस आयात किए गए", - "address_book_import_none": "आयात करने के लिए कोई नया संगठन डिवाइस नहीं" + "address_book_import_none": "आयात करने के लिए कोई नया संगठन डिवाइस नहीं", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "एंड-टू-एंड एन्क्रिप्टेड", diff --git a/web-nodejs/lang/hu.json b/web-nodejs/lang/hu.json index d649da5a..497a7082 100644 --- a/web-nodejs/lang/hu.json +++ b/web-nodejs/lang/hu.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Hungarian", "native_name": "Magyar", @@ -244,6 +244,7 @@ "change_id_title": "Eszközazonosító módosítása", "new_id": "New ID", "change_id_hint": "Csak 6-16 karakter, betűk, számok, kötőjelek és aláhúzásjelek", + "change_id_online_warn": "Az eszköz online. Az ID-t inkább a RustDesk kliensben változtassa. A panelen történő módosítás megtartja az identitást, de a kliensnek át kell vennie az új ID-t.", "change_id_success": "Az eszközazonosító sikeresen módosítva", "id_length_error": "Az azonosító 6-16 karakter hosszú lehet", "id_format_error": "Az azonosító csak betűket, számokat, kötőjeleket és aláhúzásjeleket tartalmazhat", @@ -340,6 +341,41 @@ "group_invalid": "Érvénytelen csoportazonosító", "group_not_found": "Csoport nem található", "group_saved": "Eszközcsoport mentve", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Eszközcsoportok mentve", "group_membership_added": "Hozzáadva a csoporthoz", "group_membership_removed": "Eltávolítva a csoportból", @@ -2606,7 +2642,19 @@ "address_book_saved": "Címjegyzék mentve", "address_book_save_failed": "Nem sikerült menteni a címjegyzéket", "address_book_imported": "Szervezeti eszközök importálva", - "address_book_import_none": "Nincs importálható új szervezeti eszköz" + "address_book_import_none": "Nincs importálható új szervezeti eszköz", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Végponttól végpontig titkosítva", diff --git a/web-nodejs/lang/id.json b/web-nodejs/lang/id.json index 7aeb9f58..6809f1d1 100644 --- a/web-nodejs/lang/id.json +++ b/web-nodejs/lang/id.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Indonesian", "native_name": "Bahasa Indonesia", @@ -244,6 +244,7 @@ "change_id_title": "Ubah ID Perangkat", "new_id": "New ID", "change_id_hint": "6-16 karakter, huruf, angka, tanda hubung, dan garis bawah saja", + "change_id_online_warn": "Perangkat sedang online. Ubah ID di klien RustDesk jika memungkinkan. Perubahan panel mempertahankan identitas, tetapi klien harus mengadopsi ID baru.", "change_id_success": "ID Perangkat berhasil diubah", "id_length_error": "Panjang ID harus 6-16 karakter", "id_format_error": "ID hanya boleh berisi huruf, angka, tanda hubung, dan garis bawah", @@ -340,6 +341,41 @@ "group_invalid": "Pengenal grup tidak valid", "group_not_found": "Grup tidak ditemukan", "group_saved": "Grup perangkat disimpan", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Grup perangkat disimpan", "group_membership_added": "Ditambahkan ke grup", "group_membership_removed": "Dihapus dari grup", @@ -2606,7 +2642,19 @@ "address_book_saved": "Buku alamat disimpan", "address_book_save_failed": "Gagal menyimpan buku alamat", "address_book_imported": "Perangkat organisasi diimpor", - "address_book_import_none": "Tidak ada perangkat organisasi baru untuk diimpor" + "address_book_import_none": "Tidak ada perangkat organisasi baru untuk diimpor", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Dienkripsi ujung ke ujung", diff --git a/web-nodejs/lang/it.json b/web-nodejs/lang/it.json index 8a5a7f5a..fc5f5453 100644 --- a/web-nodejs/lang/it.json +++ b/web-nodejs/lang/it.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "it", "name": "Italian", @@ -238,6 +238,7 @@ "change_id_title": "Cambia ID dispositivo", "new_id": "Nuovo ID", "change_id_hint": "6-16 caratteri, solo lettere, numeri, trattini e underscore", + "change_id_online_warn": "Il dispositivo è online. Preferire la modifica ID nel client RustDesk. La modifica dal pannello mantiene l'identità, ma il client deve adottare il nuovo ID.", "change_id_success": "ID dispositivo cambiato con successo", "id_length_error": "L'ID deve essere lungo 6-16 caratteri", "id_format_error": "L'ID può contenere solo lettere, numeri, trattini e underscore", @@ -334,6 +335,41 @@ "group_invalid": "ID gruppo non valido", "group_not_found": "Gruppo non trovato", "group_saved": "Gruppo dispositivi salvato", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Gruppi dispositivi salvati", "group_membership_added": "Aggiunto al gruppo", "group_membership_removed": "Rimosso dal gruppo", @@ -2600,7 +2636,19 @@ "address_book_saved": "Rubrica salvata", "address_book_save_failed": "Impossibile salvare la rubrica", "address_book_imported": "Dispositivi dell'organizzazione importati", - "address_book_import_none": "Nessun nuovo dispositivo dell'organizzazione da importare" + "address_book_import_none": "Nessun nuovo dispositivo dell'organizzazione da importare", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Crittografia end-to-end", diff --git a/web-nodejs/lang/ja.json b/web-nodejs/lang/ja.json index 3fcc1e31..d90cf508 100644 --- a/web-nodejs/lang/ja.json +++ b/web-nodejs/lang/ja.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "ja", "name": "Japanese", @@ -238,6 +238,7 @@ "change_id_title": "デバイスIDの変更", "new_id": "New ID", "change_id_hint": "6 ~ 16 文字、文字、数字、ダッシュ、アンダースコアのみ", + "change_id_online_warn": "デバイスはオンラインです。可能であれば RustDesk クライアントで ID を変更してください。パネルでの変更は同一性を維持しますが、クライアントが新しい ID を採用する必要があります。", "change_id_success": "デバイスIDが正常に変更されました", "id_length_error": "ID の長さは 6 ~ 16 文字���ある必要があります", "id_format_error": "ID には文字、数字、ダッシュ、アンダースコアのみを含めることができます", @@ -334,6 +335,41 @@ "group_invalid": "無効なグループ識別子", "group_not_found": "グループが見つかりません", "group_saved": "デバイスグループが保存されました", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "デバイスグループが保存されました", "group_membership_added": "グループに追加しました", "group_membership_removed": "グループから削除しました", @@ -2600,7 +2636,19 @@ "address_book_saved": "アドレス帳を保存しました", "address_book_save_failed": "アドレス帳の保存に失敗しました", "address_book_imported": "組織のデバイスをインポートしました", - "address_book_import_none": "インポートする新しい組織デバイスはありません" + "address_book_import_none": "インポートする新しい組織デバイスはありません", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "エンドツーエンド暗号化", diff --git a/web-nodejs/lang/ko.json b/web-nodejs/lang/ko.json index 834a10a4..346d4d04 100644 --- a/web-nodejs/lang/ko.json +++ b/web-nodejs/lang/ko.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "ko", "name": "Korean", @@ -238,6 +238,7 @@ "change_id_title": "장치 ID 변경", "new_id": "New ID", "change_id_hint": "6~16자, 문자, 숫자, 대시, 밑줄만 가능", + "change_id_online_warn": "기기가 온라인 상태입니다. 가능하면 RustDesk 클라이언트에서 ID를 변경하세요. 패널 변경은 동일성을 유지하지만 클라이언트가 새 ID를 채택해야 합니다.", "change_id_success": "기기 ID가 성공적으로 변경되었습니다.", "id_length_error": "ID는 6~16자여야 합니다.", "id_format_error": "ID에는 문자, 숫자, 대시, 밑줄만 포함할 수 있습니다.", @@ -334,6 +335,41 @@ "group_invalid": "잘못된 그룹 식별자", "group_not_found": "그룹을 찾을 수 없습니다", "group_saved": "장치 그룹이 저장되었습니다.", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "장치 그룹이 저장되었습니다.", "group_membership_added": "그룹에 추가됨", "group_membership_removed": "그룹에서 제거됨", @@ -2600,7 +2636,19 @@ "address_book_saved": "주소록이 저장되었습니다", "address_book_save_failed": "주소록 저장에 실패했습니다", "address_book_imported": "조직 장치를 가져왔습니다", - "address_book_import_none": "가져올 새 조직 장치가 없습니다" + "address_book_import_none": "가져올 새 조직 장치가 없습니다", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "종단 간 암호화", diff --git a/web-nodejs/lang/nb.json b/web-nodejs/lang/nb.json index 8ece5af0..2d4e3219 100644 --- a/web-nodejs/lang/nb.json +++ b/web-nodejs/lang/nb.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Norwegian", "native_name": "Norsk", @@ -244,6 +244,7 @@ "change_id_title": "Endre enhets-ID", "new_id": "New ID", "change_id_hint": "Kun 6-16 tegn, bokstaver, tall, bindestreker og understreker", + "change_id_online_warn": "Enheten er online. Endre ID helst i RustDesk-klienten. Endring i panelet beholder identiteten, men klienten må ta i bruk ny ID.", "change_id_success": "Enhets-ID ble endret", "id_length_error": "ID må bestå av 6–16 tegn", "id_format_error": "ID kan bare inneholde bokstaver, tall, bindestreker og understreker", @@ -340,6 +341,41 @@ "group_invalid": "Ugyldig gruppeidentifikator", "group_not_found": "Finner ikke gruppen", "group_saved": "Enhetsgruppen er lagret", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Enhetsgrupper lagret", "group_membership_added": "Lagt til i gruppe", "group_membership_removed": "Fjernet fra gruppe", @@ -2606,7 +2642,19 @@ "address_book_saved": "Adressebok lagret", "address_book_save_failed": "Kunne ikke lagre adresseboken", "address_book_imported": "Organisasjonsenheter importert", - "address_book_import_none": "Ingen nye organisasjonsenheter å importere" + "address_book_import_none": "Ingen nye organisasjonsenheter å importere", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Ende-til-ende kryptert", diff --git a/web-nodejs/lang/nl.json b/web-nodejs/lang/nl.json index f10fd5da..7dfafad6 100644 --- a/web-nodejs/lang/nl.json +++ b/web-nodejs/lang/nl.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "nl", "name": "Dutch", @@ -238,6 +238,7 @@ "change_id_title": "Apparaat-ID wijzigen", "new_id": "Nieuw ID", "change_id_hint": "6-16 tekens, alleen letters, cijfers, koppeltekens en underscores", + "change_id_online_warn": "Apparaat is online. Wijzig de ID bij voorkeur in de RustDesk-client. Een wijziging in het paneel behoudt de identiteit, maar de client moet de nieuwe ID overnemen.", "change_id_success": "Apparaat-ID succesvol gewijzigd", "id_length_error": "ID moet 6-16 tekens lang zijn", "id_format_error": "ID mag alleen letters, cijfers, koppeltekens en underscores bevatten", @@ -334,6 +335,41 @@ "group_invalid": "Ongeldige groeps-ID", "group_not_found": "Groep niet gevonden", "group_saved": "Apparaatgroep opgeslagen", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Apparaatgroepen opgeslagen", "group_membership_added": "Toegevoegd aan groep", "group_membership_removed": "Verwijderd uit groep", @@ -2600,7 +2636,19 @@ "address_book_saved": "Adresboek opgeslagen", "address_book_save_failed": "Adresboek opslaan mislukt", "address_book_imported": "Organisatieapparaten geïmporteerd", - "address_book_import_none": "Geen nieuwe organisatieapparaten om te importeren" + "address_book_import_none": "Geen nieuwe organisatieapparaten om te importeren", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "End-to-end versleuteld", diff --git a/web-nodejs/lang/pl.json b/web-nodejs/lang/pl.json index 21c899ce..ba71bee3 100644 --- a/web-nodejs/lang/pl.json +++ b/web-nodejs/lang/pl.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "pl", "name": "Polish", @@ -238,6 +238,7 @@ "change_id_title": "Zmień ID urządzenia", "new_id": "Nowe ID", "change_id_hint": "6-16 znaków, tylko litery, cyfry, myślniki i podkreślenia", + "change_id_online_warn": "Urządzenie jest online. Zmianę ID lepiej wykonać w kliencie RustDesk. Zmiana w panelu zachowuje tożsamość urządzenia, ale klient musi przyjąć nowe ID, aby pozostać połączony.", "change_id_success": "ID urządzenia zmienione", "id_length_error": "ID musi mieć 6-16 znaków", "id_format_error": "ID może zawierać tylko litery, cyfry, myślniki i podkreślenia", @@ -301,6 +302,41 @@ "group_invalid": "Nieprawidłowy identyfikator grupy", "group_not_found": "Nie znaleziono grupy", "group_saved": "Grupa urządzeń zapisana", + "strategies_title": "Strategie kontroli dostępu", + "strategies_hint": "Powiąż grupy użytkowników z grupami urządzeń i wymuszaj ustawienia bezpieczeństwa RustDesk Pro na pasujących klientach.", + "create_strategy": "Dodaj strategię", + "edit_strategy": "Edytuj strategię", + "delete_strategy": "Usuń strategię", + "delete_strategy_confirm": "Usunąć strategię {name}?", + "strategy_name": "Nazwa strategii", + "strategy_name_required": "Nazwa strategii jest wymagana", + "strategy_user_group": "Grupa użytkowników", + "strategy_device_group": "Grupa urządzeń", + "strategy_any_user_group": "Dowolna grupa użytkowników", + "strategy_any_device_group": "Dowolna grupa urządzeń", + "strategy_enabled": "Włączona", + "strategy_disabled": "Wyłączona", + "strategy_enabled_label": "Strategia włączona", + "strategy_permissions": "Ustawienia bezpieczeństwa klienta", + "strategy_permissions_hint": "Używa kluczy zaawansowanych ustawień RustDesk (Y/N). Domyślne pozostawia dziedziczenie po kliencie lub strategii.", + "strategy_permissions_count": "{count} ustawień", + "strategy_perm_default": "Domyślne", + "strategy_perm_allow": "Zezwól (Y)", + "strategy_perm_deny": "Odmów (N)", + "strategy_perm_file_transfer": "Transfer plików", + "strategy_perm_disable_clipboard": "Wyłącz schowek", + "strategy_perm_enable_clipboard": "Włącz schowek", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "Tunel TCP", + "strategy_perm_camera": "Kamera", + "strategy_perm_restart": "Zdalny restart", + "strategy_perm_block_input": "Blokada wejścia użytkownika", + "strategy_created": "Strategia utworzona", + "strategy_updated": "Strategia zaktualizowana", + "strategy_deleted": "Strategia usunięta", + "strategy_not_found": "Nie znaleziono strategii", + "no_strategies": "Brak strategii kontroli dostępu", + "loading_strategies": "Ładowanie strategii...", "groups_saved": "Grupy urządzenia zapisane", "group_membership_added": "Dodano do grupy", "group_membership_removed": "Usunięto z grupy", @@ -2684,7 +2720,19 @@ "address_book_saved": "Książka adresowa zapisana", "address_book_save_failed": "Nie udało się zapisać książki adresowej", "address_book_imported": "Urządzenia organizacji zaimportowane", - "address_book_import_none": "Brak nowych urządzeń organizacji do importu" + "address_book_import_none": "Brak nowych urządzeń organizacji do importu", + "address_book_contacts": "Współdzielone kontakty", + "address_book_add_peer": "Dodaj kontakt", + "address_book_peer_id": "ID urządzenia", + "address_book_alias": "Alias", + "address_book_peer_tags": "Tagi kontaktu", + "address_book_peer_tags_placeholder": "biuro, wsparcie", + "address_book_tags_label": "Tagi globalne", + "address_book_tags_placeholder": "Biuro, Wsparcie", + "address_book_tags_hint": "Tagi oddzielone przecinkami dostępne w książce adresowej klienta RustDesk.", + "address_book_no_peers": "Brak kontaktów — dodaj ID urządzenia lub zaimportuj urządzenia organizacji.", + "address_book_show_json": "Zaawansowany JSON", + "address_book_hide_json": "Edytor strukturalny" }, "chat": { "e2e_enabled": "Szyfrowanie end-to-end", diff --git a/web-nodejs/lang/pt.json b/web-nodejs/lang/pt.json index 7b6aad74..8e25c8c1 100644 --- a/web-nodejs/lang/pt.json +++ b/web-nodejs/lang/pt.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "pt", "name": "Portuguese", @@ -238,6 +238,7 @@ "change_id_title": "Alterar ID do dispositivo", "new_id": "Novo ID", "change_id_hint": "6-16 caracteres, apenas letras, números, hífens e underscores", + "change_id_online_warn": "O dispositivo está online. Prefira alterar o ID no cliente RustDesk. A alteração no painel mantém a identidade, mas o cliente deve adotar o novo ID.", "change_id_success": "ID do dispositivo alterado com sucesso", "id_length_error": "O ID deve ter 6-16 caracteres", "id_format_error": "O ID só pode conter letras, números, hífens e underscores", @@ -334,6 +335,41 @@ "group_invalid": "Identificador de grupo inválido", "group_not_found": "Grupo não encontrado", "group_saved": "Grupo de dispositivos salvo", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Grupos de dispositivos salvos", "group_membership_added": "Adicionado ao grupo", "group_membership_removed": "Removido do grupo", @@ -2600,7 +2636,19 @@ "address_book_saved": "Livro de endereços guardado", "address_book_save_failed": "Falha ao guardar o livro de endereços", "address_book_imported": "Dispositivos da organização importados", - "address_book_import_none": "Nenhum novo dispositivo da organização para importar" + "address_book_import_none": "Nenhum novo dispositivo da organização para importar", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Criptografia ponta a ponta", diff --git a/web-nodejs/lang/ro.json b/web-nodejs/lang/ro.json index eab7bc65..5eaaaf5f 100644 --- a/web-nodejs/lang/ro.json +++ b/web-nodejs/lang/ro.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Romanian", "native_name": "Română", @@ -244,6 +244,7 @@ "change_id_title": "Schimbați ID-ul dispozitivului", "new_id": "New ID", "change_id_hint": "Numai 6-16 caractere, litere, cifre, liniuțe și caractere de subliniere", + "change_id_online_warn": "Dispozitivul este online. Schimbați ID-ul preferabil în clientul RustDesk. Modificarea din panou păstrează identitatea, dar clientul trebuie să adopte noul ID.", "change_id_success": "ID-ul dispozitivului a fost schimbat cu succes", "id_length_error": "ID-ul trebuie să aibă 6-16 caractere", "id_format_error": "ID-ul poate conține doar litere, cifre, liniuțe și liniuțe de subliniere", @@ -340,6 +341,41 @@ "group_invalid": "Identificator de grup nevalid", "group_not_found": "Grupul nu a fost găsit", "group_saved": "Grupul de dispozitive a fost salvat", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Grupuri de dispozitive salvate", "group_membership_added": "Adăugat la grup", "group_membership_removed": "Eliminat din grup", @@ -2606,7 +2642,19 @@ "address_book_saved": "Agenda de adrese a fost salvată", "address_book_save_failed": "Nu s-a putut salva agenda de adrese", "address_book_imported": "Dispozitivele organizației au fost importate", - "address_book_import_none": "Nu există dispozitive noi ale organizației de importat" + "address_book_import_none": "Nu există dispozitive noi ale organizației de importat", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Criptat de la capăt la capăt", diff --git a/web-nodejs/lang/sv.json b/web-nodejs/lang/sv.json index 06f0cb26..07a1f720 100644 --- a/web-nodejs/lang/sv.json +++ b/web-nodejs/lang/sv.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Swedish", "native_name": "Svenska", @@ -244,6 +244,7 @@ "change_id_title": "Ändra enhets-ID", "new_id": "New ID", "change_id_hint": "Endast 6-16 tecken, bokstäver, siffror, bindestreck och understreck", + "change_id_online_warn": "Enheten är online. Byt ID helst i RustDesk-klienten. Ändring i panelen behåller identiteten, men klienten måste anta det nya ID:t.", "change_id_success": "Enhets-ID har ändrats", "id_length_error": "ID måste vara 6-16 tecken långt", "id_format_error": "ID kan bara innehålla bokstäver, siffror, bindestreck och understreck", @@ -340,6 +341,41 @@ "group_invalid": "Ogiltig gruppidentifierare", "group_not_found": "Gruppen hittades inte", "group_saved": "Enhetsgruppen har sparats", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Enhetsgrupper har sparats", "group_membership_added": "Tillagd i grupp", "group_membership_removed": "Borttagen från grupp", @@ -2606,7 +2642,19 @@ "address_book_saved": "Adressbok sparad", "address_book_save_failed": "Kunde inte spara adressboken", "address_book_imported": "Organisationsenheter importerade", - "address_book_import_none": "Inga nya organisationsenheter att importera" + "address_book_import_none": "Inga nya organisationsenheter att importera", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "End-to-end krypterad", diff --git a/web-nodejs/lang/th.json b/web-nodejs/lang/th.json index 3fa80bb2..83304747 100644 --- a/web-nodejs/lang/th.json +++ b/web-nodejs/lang/th.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Thai", "native_name": "ไทย", @@ -244,6 +244,7 @@ "change_id_title": "เปลี่ยนรหัสอุปกรณ์", "new_id": "New ID", "change_id_hint": "อักขระ 6-16 ตัว ตัวอักษร ตัวเลข ขีดกลาง และขีดล่างเท่านั้น", + "change_id_online_warn": "อุปกรณ์ออนไลน์ ควรเปลี่ยน ID ในไคลเอนต์ RustDesk การเปลี่ยนในแผงควบคุมยังคงตัวตนเดิม แต่ไคลเอนต์ต้องใช้ ID ใหม่", "change_id_success": "เปลี่ยนรหัสอุปกรณ์เรียบร้อยแล้ว", "id_length_error": "รหัสต้องมีความยาว 6-16 ตัวอักษร", "id_format_error": "รหัสมีได้เฉพาะตัวอักษร ตัวเลข ขีดกลาง และขีดล่างเท่านั้น", @@ -340,6 +341,41 @@ "group_invalid": "ตัวระบุกลุ่มไม่ถูกต้อง", "group_not_found": "ไม่พบกลุ่ม", "group_saved": "บันทึกกลุ่มอุปกรณ์แล้ว", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "บันทึกกลุ่มอุปกรณ์แล้ว", "group_membership_added": "เพิ่มในกลุ่มแล้ว", "group_membership_removed": "นำออกจากกลุ่มแล้ว", @@ -2606,7 +2642,19 @@ "address_book_saved": "บันทึกสมุดที่อยู่แล้ว", "address_book_save_failed": "ไม่สามารถบันทึกสมุดที่อยู่ได้", "address_book_imported": "นำเข้าอุปกรณ์ขององค์กรแล้ว", - "address_book_import_none": "ไม่มีอุปกรณ์ใหม่ขององค์กรให้นำเข้า" + "address_book_import_none": "ไม่มีอุปกรณ์ใหม่ขององค์กรให้นำเข้า", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "เข้ารหัสตั้งแต่ต้นทางถึงปลายทาง", diff --git a/web-nodejs/lang/tr.json b/web-nodejs/lang/tr.json index 0f2adf8c..5adc147a 100644 --- a/web-nodejs/lang/tr.json +++ b/web-nodejs/lang/tr.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Turkish", "native_name": "Türkçe", @@ -244,6 +244,7 @@ "change_id_title": "Cihaz Kimliğini Değiştir", "new_id": "New ID", "change_id_hint": "Yalnızca 6-16 karakter, harf, rakam, tire ve alt çizgi", + "change_id_online_warn": "Cihaz çevrimiçi. Mümkünse ID'yi RustDesk istemcisinde değiştirin. Panel değişikliği kimliği korur, ancak istemci yeni ID'yi benimsemelidir.", "change_id_success": "Cihaz kimliği başarıyla değiştirildi", "id_length_error": "Kimlik 6-16 karakter uzunluğunda olmalıdır", "id_format_error": "Kimlik yalnızca harf, rakam, tire ve alt çizgi içerebilir", @@ -340,6 +341,41 @@ "group_invalid": "Geçersiz grup tanımlayıcı", "group_not_found": "Grup bulunamadı", "group_saved": "Cihaz grubu kaydedildi", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Cihaz grupları kaydedildi", "group_membership_added": "Gruba eklendi", "group_membership_removed": "Gruptan kaldırıldı", @@ -2606,7 +2642,19 @@ "address_book_saved": "Adres defteri kaydedildi", "address_book_save_failed": "Adres defteri kaydedilemedi", "address_book_imported": "Kuruluş cihazları içe aktarıldı", - "address_book_import_none": "İçe aktarılacak yeni kuruluş cihazı yok" + "address_book_import_none": "İçe aktarılacak yeni kuruluş cihazı yok", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Uçtan uca şifrelenmiş", diff --git a/web-nodejs/lang/uk.json b/web-nodejs/lang/uk.json index d262f1a9..3d89aeea 100644 --- a/web-nodejs/lang/uk.json +++ b/web-nodejs/lang/uk.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Ukrainian", "native_name": "Українська", @@ -244,6 +244,7 @@ "change_id_title": "Змінити ідентифікатор пристрою", "new_id": "New ID", "change_id_hint": "6-16 символів, лише літери, цифри, тире та підкреслення", + "change_id_online_warn": "Пристрій онлайн. Краще змінювати ID у клієнті RustDesk. Зміна в панелі зберігає ідентичність, але клієнт має прийняти новий ID.", "change_id_success": "Ідентифікатор пристрою успішно змінено", "id_length_error": "ID має містити 6-16 символів", "id_format_error": "ID може містити лише літери, цифри, тире та підкреслення", @@ -340,6 +341,41 @@ "group_invalid": "Недійсний ідентифікатор групи", "group_not_found": "Групу не знайдено", "group_saved": "Групу пристроїв збережено", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Групи пристроїв збережено", "group_membership_added": "Додано до групи", "group_membership_removed": "Вилучено з групи", @@ -2606,7 +2642,19 @@ "address_book_saved": "Адресну книгу збережено", "address_book_save_failed": "Не вдалося зберегти адресну книгу", "address_book_imported": "Пристрої організації імпортовано", - "address_book_import_none": "Немає нових пристроїв організації для імпорту" + "address_book_import_none": "Немає нових пристроїв організації для імпорту", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Наскрізне шифрування", diff --git a/web-nodejs/lang/vi.json b/web-nodejs/lang/vi.json index 3d6c9a6f..c8e63284 100644 --- a/web-nodejs/lang/vi.json +++ b/web-nodejs/lang/vi.json @@ -1,4 +1,4 @@ -{ +{ "_meta": { "name": "Vietnamese", "native_name": "Tiếng Việt", @@ -244,6 +244,7 @@ "change_id_title": "Thay đổi ID thiết bị", "new_id": "New ID", "change_id_hint": "Chỉ 6-16 ký tự, chữ cái, số, dấu gạch ngang và dấu gạch dưới", + "change_id_online_warn": "Thiết bị đang trực tuyến. Nên đổi ID trong client RustDesk. Thay đổi trên panel giữ nguyên danh tính, nhưng client phải áp dụng ID mới.", "change_id_success": "ID thiết bị đã được thay đổi thành công", "id_length_error": "ID phải dài 6-16 ký tự", "id_format_error": "ID chỉ có thể chứa chữ cái, số, dấu gạch ngang và dấu gạch dưới", @@ -340,6 +341,41 @@ "group_invalid": "Mã định danh nhóm không hợp lệ", "group_not_found": "Không tìm thấy nhóm", "group_saved": "Đã lưu nhóm thiết bị", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "Đã lưu nhóm thiết bị", "group_membership_added": "Đã thêm vào nhóm", "group_membership_removed": "Đã xóa khỏi nhóm", @@ -2606,7 +2642,19 @@ "address_book_saved": "Đã lưu sổ địa chỉ", "address_book_save_failed": "Không thể lưu sổ địa chỉ", "address_book_imported": "Đã nhập thiết bị của tổ chức", - "address_book_import_none": "Không có thiết bị tổ chức mới để nhập" + "address_book_import_none": "Không có thiết bị tổ chức mới để nhập", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "Mã hóa đầu cuối", diff --git a/web-nodejs/lang/zh-TW.json b/web-nodejs/lang/zh-TW.json index 4d3f77bf..b84a1a56 100644 --- a/web-nodejs/lang/zh-TW.json +++ b/web-nodejs/lang/zh-TW.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "language": "繁體中文", "code": "zh-TW", @@ -244,6 +244,7 @@ "change_id_title": "更改設備 ID", "new_id": "新 ID", "change_id_hint": "6-16 個字符,僅限字母、數字、短橫線和下劃線", + "change_id_online_warn": "裝置在線。請優先在 RustDesk 用戶端中變更 ID。在面板中變更會保留裝置身分,但用戶端必須採用新 ID 才能保持連線。", "change_id_success": "設備 ID 更改成功", "id_length_error": "ID 必須爲 6-16 個字符", "id_format_error": "ID 只能包含字母、數字、短橫線和下劃線", @@ -307,6 +308,41 @@ "group_invalid": "分組標識符無效", "group_not_found": "未找到分組", "group_saved": "設備分組已保存", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "設備分組已保存", "group_membership_added": "已添加到分組", "group_membership_removed": "已從分組移除", @@ -2688,7 +2724,19 @@ "address_book_saved": "通訊錄已儲存", "address_book_save_failed": "無法儲存通訊錄", "address_book_imported": "已匯入組織裝置", - "address_book_import_none": "沒有新的組織裝置可匯入" + "address_book_import_none": "沒有新的組織裝置可匯入", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "端到端加密", diff --git a/web-nodejs/lang/zh.json b/web-nodejs/lang/zh.json index 2775653d..7f090a5b 100644 --- a/web-nodejs/lang/zh.json +++ b/web-nodejs/lang/zh.json @@ -1,4 +1,4 @@ -{ +{ "meta": { "lang": "zh", "name": "中文", @@ -238,6 +238,7 @@ "change_id_title": "更改设备 ID", "new_id": "新 ID", "change_id_hint": "6-16 个字符,仅限字母、数字、短横线和下划线", + "change_id_online_warn": "设备在线。请优先在 RustDesk 客户端中更改 ID。在面板中更改会保留设备身份,但客户端必须采用新 ID 才能保持连接。", "change_id_success": "设备 ID 更改成功", "id_length_error": "ID 必须为 6-16 个字符", "id_format_error": "ID 只能包含字母、数字、短横线和下划线", @@ -334,6 +335,41 @@ "group_invalid": "分组标识符无效", "group_not_found": "未找到分组", "group_saved": "设备分组已保存", + "strategies_title": "Access control strategies", + "strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.", + "create_strategy": "Add strategy", + "edit_strategy": "Edit strategy", + "delete_strategy": "Delete strategy", + "delete_strategy_confirm": "Delete strategy {name}?", + "strategy_name": "Strategy name", + "strategy_name_required": "Strategy name is required", + "strategy_user_group": "User group", + "strategy_device_group": "Device group", + "strategy_any_user_group": "Any user group", + "strategy_any_device_group": "Any device group", + "strategy_enabled": "Enabled", + "strategy_disabled": "Disabled", + "strategy_enabled_label": "Strategy enabled", + "strategy_permissions": "Client security settings", + "strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.", + "strategy_permissions_count": "{count} settings", + "strategy_perm_default": "Default", + "strategy_perm_allow": "Allow (Y)", + "strategy_perm_deny": "Deny (N)", + "strategy_perm_file_transfer": "File transfer", + "strategy_perm_disable_clipboard": "Disable clipboard", + "strategy_perm_enable_clipboard": "Enable clipboard", + "strategy_perm_audio": "Audio", + "strategy_perm_tunnel": "TCP tunnel", + "strategy_perm_camera": "Camera", + "strategy_perm_restart": "Remote restart", + "strategy_perm_block_input": "Block user input", + "strategy_created": "Strategy created", + "strategy_updated": "Strategy updated", + "strategy_deleted": "Strategy deleted", + "strategy_not_found": "Strategy not found", + "no_strategies": "No access control strategies yet", + "loading_strategies": "Loading strategies...", "groups_saved": "设备分组已保存", "group_membership_added": "已添加到分组", "group_membership_removed": "已从分组移除", @@ -2671,7 +2707,19 @@ "address_book_saved": "通讯录已保存", "address_book_save_failed": "无法保存通讯录", "address_book_imported": "已导入组织设备", - "address_book_import_none": "没有新的组织设备可导入" + "address_book_import_none": "没有新的组织设备可导入", + "address_book_contacts": "Shared contacts", + "address_book_add_peer": "Add contact", + "address_book_peer_id": "Device ID", + "address_book_alias": "Alias", + "address_book_peer_tags": "Contact tags", + "address_book_peer_tags_placeholder": "office, support", + "address_book_tags_label": "Global tags", + "address_book_tags_placeholder": "Office, Support", + "address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.", + "address_book_no_peers": "No contacts yet — add a device ID or import organization devices.", + "address_book_show_json": "Advanced JSON", + "address_book_hide_json": "Structured editor" }, "chat": { "e2e_enabled": "端到端加密", diff --git a/web-nodejs/public/css/devices.css b/web-nodejs/public/css/devices.css index 090b44bc..2e36f94b 100644 --- a/web-nodejs/public/css/devices.css +++ b/web-nodejs/public/css/devices.css @@ -1986,6 +1986,122 @@ td.column-hidden { color: var(--text-muted, #8b949e); margin-top: 2px; } + +/* Access control strategies (RustDesk Pro) */ +.devices-strategies-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--space-md) var(--space-lg); + margin: var(--space-md) 0; +} + +.devices-strategies-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-md); +} + +.devices-strategies-header h3 { + margin: 0 0 var(--space-xs) 0; + font-size: var(--font-size-lg); +} + +.devices-strategies-header p { + margin: 0; + color: var(--text-secondary); + font-size: var(--font-size-sm); +} + +.devices-strategies-list { + display: grid; + gap: var(--space-sm); +} + +.strategy-manager-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: var(--space-md); + padding: var(--space-sm) var(--space-md); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-primary); +} + +.strategy-manager-item.strategy-disabled { + opacity: 0.72; +} + +.strategy-manager-main { + display: flex; + align-items: center; + gap: var(--space-sm); + min-width: 0; +} + +.strategy-manager-main > .material-icons { + color: var(--accent-purple, #8b5cf6); + font-size: 20px; +} + +.strategy-manager-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.strategy-manager-text span { + color: var(--text-secondary); + font-size: var(--font-size-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.strategy-perm-count { + font-size: var(--font-size-xs) !important; +} + +.strategy-status-badge { + font-size: var(--font-size-xs); + padding: 2px 8px; + border-radius: var(--radius-full); + border: 1px solid var(--border-color); +} + +.strategy-status-badge.enabled { + color: var(--accent-green, #22c55e); + border-color: rgba(34, 197, 94, 0.35); +} + +.strategy-status-badge.disabled { + color: var(--text-secondary); +} + +.strategy-manager-actions { + display: flex; + gap: var(--space-xs); +} + +.strategy-perm-grid { + display: grid; + gap: var(--space-sm); +} + +.strategy-perm-option { + display: grid; + grid-template-columns: minmax(0, 1fr) 160px; + align-items: center; + gap: var(--space-sm); +} + +.strategy-perm-select { + padding: 6px 8px; +} .mesh-groups-list { margin-top: 8px; } .mesh-group-row { padding: 4px 0; } .mesh-recordings-table { margin-top: 8px; } diff --git a/web-nodejs/public/css/organizations.css b/web-nodejs/public/css/organizations.css index b2040cc4..0d2d7bc6 100644 --- a/web-nodejs/public/css/organizations.css +++ b/web-nodejs/public/css/organizations.css @@ -275,6 +275,42 @@ resize: vertical; } +.org-address-book-structured { + margin-top: var(--space-md); +} + +.org-ab-section-header { + margin-bottom: var(--space-sm); +} + +.org-ab-table-wrap { + margin-bottom: var(--space-md); +} + +.org-address-book-table .form-input { + width: 100%; + min-width: 0; +} + +.org-ab-actions { + width: 48px; + text-align: right; +} + +.org-ab-empty { + text-align: center; + color: var(--text-secondary); + padding: var(--space-md); +} + +.org-address-book-advanced { + margin-top: var(--space-md); +} + +.org-address-book-json-wrap.hidden { + display: none; +} + /* Empty state */ .empty-state { text-align: center; diff --git a/web-nodejs/public/js/deviceDetail.js b/web-nodejs/public/js/deviceDetail.js index a6ad1702..a1a5014d 100644 --- a/web-nodejs/public/js/deviceDetail.js +++ b/web-nodejs/public/js/deviceDetail.js @@ -22,10 +22,49 @@ const DeviceDetail = (function () { if (typeof detail.note === 'string') { device.note = detail.note; } + if (typeof detail.online === 'boolean' || detail.status || detail.live_status) { + device.online = detail.online ?? device.online; + device.status = detail.status || device.status; + device.live_status = detail.live_status || detail.status || device.live_status; + } _render(); }); + document.addEventListener('devices:live-status', function (event) { + const detail = event.detail || {}; + if (!device || !detail.id || device.id !== detail.id) return; + const normalized = String(detail.status || '').toLowerCase(); + device.live_status = normalized; + device.online = normalized === 'online'; + device.status = normalized; + const statusBar = overlayEl?.querySelector('.device-panel-status-bar'); + if (statusBar) { + const statusInfo = _resolveDeviceStatus(device); + const badge = statusBar.querySelector('.device-panel-status-badge'); + if (badge) { + badge.className = 'device-panel-status-badge ' + statusInfo.className; + if (statusInfo.title) badge.title = statusInfo.title; + else badge.removeAttribute('title'); + badge.innerHTML = '' + statusInfo.label; + } + } + }); + + document.addEventListener('devices:id-changed', function (event) { + const detail = event.detail || {}; + if (!device || !detail.old_id || device.id !== detail.old_id) return; + const newId = detail.new_id; + if (!newId) return; + device.id = newId; + _stopRefresh(); + _startRefresh(newId); + const titleEl = overlayEl?.querySelector('.device-panel-device-id'); + if (titleEl) titleEl.textContent = newId; + const copyBtn = overlayEl?.querySelector('.device-panel-copy-btn[data-copy]'); + if (copyBtn) copyBtn.dataset.copy = newId; + }); + // ────────────────────────────────────────────────────────────────────── // State // ────────────────────────────────────────────────────────────────────── @@ -1279,6 +1318,16 @@ const DeviceDetail = (function () { const deviceId = device && device.id; if (!deviceId) return; + if (device.online) { + const proceed = await Modal.confirm({ + title: _('devices.change_id_title'), + message: _('devices.change_id_online_warn'), + confirmLabel: _('devices.change_id'), + danger: false + }); + if (!proceed) return; + } + const newId = await Modal.prompt({ title: _('devices.change_id_title'), label: _('devices.new_id'), diff --git a/web-nodejs/public/js/devices.js b/web-nodejs/public/js/devices.js index 4906dcca..802c9521 100644 --- a/web-nodejs/public/js/devices.js +++ b/web-nodejs/public/js/devices.js @@ -54,6 +54,8 @@ let filteredDevices = []; let folders = []; let deviceGroups = []; + let accessStrategies = []; + let strategiesLoaded = false; let availableUserGroups = []; let userGroupsLoaded = false; let availableTags = []; @@ -90,6 +92,7 @@ loadFolders(); loadUserGroups(); loadDeviceGroups(); + loadStrategies(); loadTags(); loadDevices(); @@ -102,6 +105,7 @@ initSync(); initFolders(); initDeviceGroups(); + initStrategies(); initDragDrop(); attachFolderDropEvents(); // For static folder chips attachGroupDropEvents(); // For static group chips @@ -127,6 +131,7 @@ loadFolders(); loadUserGroups(); loadDeviceGroups(); + loadStrategies(); loadTags(); loadDevices(); }); @@ -166,6 +171,14 @@ const data = JSON.parse(event.data); if (data.type === 'device_status') { updateDeviceStatusInPlace(data.device_id, data.status); + document.dispatchEvent(new CustomEvent('devices:live-status', { + detail: { id: data.device_id, status: data.status } + })); + } else if (data.type === 'device_id_changed') { + document.dispatchEvent(new CustomEvent('devices:id-changed', { + detail: { old_id: data.old_id, new_id: data.new_id, source: data.source || '' } + })); + loadDevices(); } } catch (_) {} }; @@ -1655,6 +1668,17 @@ * Change device ID */ async function changeDeviceId(deviceId) { + const device = devices.find(d => d.id === deviceId); + if (device?.online) { + const proceed = await Modal.confirm({ + title: _('devices.change_id_title'), + message: _('devices.change_id_online_warn'), + confirmLabel: _('devices.change_id'), + danger: false + }); + if (!proceed) return; + } + const newId = await Modal.prompt({ title: _('devices.change_id_title'), label: _('devices.new_id'), @@ -2322,6 +2346,214 @@ document.getElementById('add-device-group-btn')?.addEventListener('click', () => showDeviceGroupModal()); document.querySelector('.group-chip[data-group="all"]')?.addEventListener('click', () => selectDeviceGroup('all')); } + + const RUSTDESK_STRATEGY_PERM_KEYS = [ + { key: 'enable-file-transfer', labelKey: 'devices.strategy_perm_file_transfer' }, + { key: 'disable-clipboard', labelKey: 'devices.strategy_perm_disable_clipboard' }, + { key: 'enable-clipboard', labelKey: 'devices.strategy_perm_enable_clipboard' }, + { key: 'enable-audio', labelKey: 'devices.strategy_perm_audio' }, + { key: 'enable-tunnel', labelKey: 'devices.strategy_perm_tunnel' }, + { key: 'enable-camera', labelKey: 'devices.strategy_perm_camera' }, + { key: 'enable-remote-restart', labelKey: 'devices.strategy_perm_restart' }, + { key: 'enable-block-input', labelKey: 'devices.strategy_perm_block_input' }, + ]; + + async function loadStrategies() { + try { + const response = await Utils.api('/api/panel/strategies'); + accessStrategies = response.strategies || []; + strategiesLoaded = true; + renderStrategies(); + } catch (error) { + console.error('Failed to load strategies:', error); + strategiesLoaded = true; + accessStrategies = []; + renderStrategies(); + } + } + + function renderStrategies() { + const container = document.getElementById('devices-strategies-list'); + if (!container) return; + if (!strategiesLoaded) { + container.innerHTML = `
${_('devices.loading_strategies')}
`; + return; + } + if (!accessStrategies.length) { + container.innerHTML = `
${_('devices.no_strategies')}
`; + return; + } + + const userGroupName = (guid) => { + const group = availableUserGroups.find(item => item.guid === guid); + return group ? group.name : (guid || _('devices.strategy_any_user_group')); + }; + const deviceGroupName = (guid) => { + const group = deviceGroups.find(item => item.guid === guid); + return group ? group.name : (guid || _('devices.strategy_any_device_group')); + }; + + container.innerHTML = accessStrategies.map(strategy => { + const enabled = strategy.enabled !== false; + const permCount = Object.keys(strategy.permissions || {}).length; + return `
+
+ policy +
+ ${Utils.escapeHtml(strategy.name)} + ${Utils.escapeHtml(userGroupName(strategy.user_group_guid))} → ${Utils.escapeHtml(deviceGroupName(strategy.device_group_guid))} + ${_('devices.strategy_permissions_count', { count: permCount }) || permCount + ' settings'} +
+
+ ${enabled ? _('devices.strategy_enabled') : _('devices.strategy_disabled')} +
+ + +
+
`; + }).join(''); + + container.querySelectorAll('[data-action="edit-strategy"]').forEach(btn => { + btn.addEventListener('click', () => { + const strategy = accessStrategies.find(item => item.guid === btn.dataset.guid); + if (strategy) showStrategyModal(strategy); + }); + }); + container.querySelectorAll('[data-action="delete-strategy"]').forEach(btn => { + btn.addEventListener('click', () => { + const strategy = accessStrategies.find(item => item.guid === btn.dataset.guid); + if (strategy) deleteStrategy(strategy); + }); + }); + } + + function renderStrategyPermissionOptions(permissions = {}) { + return RUSTDESK_STRATEGY_PERM_KEYS.map(item => { + const current = permissions[item.key]; + const checked = current === 'Y' || current === true; + const denied = current === 'N' || current === false; + return ``; + }).join(''); + } + + function readStrategyPermissionsFromForm() { + const permissions = {}; + document.querySelectorAll('.strategy-perm-select').forEach(select => { + const key = select.dataset.permKey; + const value = select.value; + if (key && (value === 'Y' || value === 'N')) permissions[key] = value; + }); + return permissions; + } + + async function showStrategyModal(strategy = null) { + await ensureUserGroupsLoaded(); + if (!deviceGroups.length) await loadDeviceGroups(); + const editing = !!strategy; + const userGroupOptions = [ + ``, + ...availableUserGroups.map(group => ``) + ].join(''); + const deviceGroupOptions = [ + ``, + ...deviceGroups.map(group => ``) + ].join(''); + + Modal.show({ + title: editing ? _('devices.edit_strategy') : _('devices.create_strategy'), + content: ` +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
${renderStrategyPermissionOptions(strategy?.permissions || {})}
+

${_('devices.strategy_permissions_hint')}

+
+
+ `, + size: 'medium', + buttons: [ + { label: _('actions.cancel'), class: 'btn-secondary', onClick: () => Modal.close() }, + { label: _('actions.save'), class: 'btn-primary', onClick: () => saveStrategy(strategy) } + ], + onOpen: () => document.getElementById('strategy-name')?.focus() + }); + } + + async function saveStrategy(strategy = null) { + const name = document.getElementById('strategy-name')?.value.trim() || ''; + if (!name) { + Notifications.error(_('devices.strategy_name_required')); + return; + } + const payload = { + name, + user_group_guid: document.getElementById('strategy-user-group')?.value || '', + device_group_guid: document.getElementById('strategy-device-group')?.value || '', + enabled: !!document.getElementById('strategy-enabled')?.checked, + permissions: readStrategyPermissionsFromForm(), + }; + try { + const url = strategy + ? `/api/panel/strategies/${encodeURIComponent(strategy.guid)}` + : '/api/panel/strategies'; + await Utils.api(url, { method: strategy ? 'PATCH' : 'POST', body: payload }); + Notifications.success(strategy ? _('devices.strategy_updated') : _('devices.strategy_created')); + Modal.close(); + strategiesLoaded = false; + await loadStrategies(); + } catch (error) { + Notifications.error(error.message || _('errors.server_error')); + } + } + + async function deleteStrategy(strategy) { + const confirmed = await Modal.confirm({ + title: _('devices.delete_strategy'), + message: (_('devices.delete_strategy_confirm') || 'Delete strategy {name}?').replace('{name}', strategy.name), + confirmLabel: _('actions.delete'), + danger: true + }); + if (!confirmed) return; + try { + await Utils.api(`/api/panel/strategies/${encodeURIComponent(strategy.guid)}`, { method: 'DELETE' }); + Notifications.success(_('devices.strategy_deleted')); + strategiesLoaded = false; + await loadStrategies(); + } catch (error) { + Notifications.error(error.message || _('errors.server_error')); + } + } + + function initStrategies() { + document.getElementById('add-strategy-btn')?.addEventListener('click', () => showStrategyModal()); + } // ==================== Folder Functions ==================== diff --git a/web-nodejs/public/js/organizationDetail.js b/web-nodejs/public/js/organizationDetail.js index e9281373..b0d4648d 100644 --- a/web-nodejs/public/js/organizationDetail.js +++ b/web-nodejs/public/js/organizationDetail.js @@ -608,6 +608,9 @@ // Shared organization address book // ----------------------------------------------------------------------- let addressBookEnabled = true; + let addressBookPeers = []; + let addressBookTags = []; + let addressBookShowJson = false; function parseAddressBook(raw) { if (!raw || raw === '{}') return { peers: [], tags: [] }; @@ -622,6 +625,64 @@ return JSON.stringify(normalized, null, 2); } + function buildAddressBookPayload() { + if (addressBookShowJson) { + return currentAddressBookJSON(); + } + const tagsRaw = document.getElementById('org-address-book-tags-input')?.value || ''; + const tags = tagsRaw.split(',').map(tag => tag.trim()).filter(Boolean); + const peers = addressBookPeers.map(peer => { + const entry = { id: String(peer.id || '').trim() }; + if (peer.alias) entry.alias = String(peer.alias).trim(); + if (Array.isArray(peer.tags) && peer.tags.length) entry.tags = peer.tags; + return entry; + }).filter(peer => peer.id); + return { peers, tags }; + } + + function renderAddressBookPeersTable() { + const tbody = document.getElementById('org-address-book-peers-body'); + if (!tbody) return; + if (!addressBookPeers.length) { + tbody.innerHTML = `${escHtml(t('address_book_no_peers'))}`; + return; + } + tbody.innerHTML = addressBookPeers.map((peer, index) => ` + + + + + + + + + `).join(''); + + tbody.querySelectorAll('.org-ab-remove-peer').forEach(btn => { + btn.addEventListener('click', () => { + const idx = parseInt(btn.dataset.index, 10); + if (!Number.isNaN(idx)) { + addressBookPeers.splice(idx, 1); + renderAddressBookPeersTable(); + } + }); + }); + } + + function syncAddressBookPeersFromTable() { + const rows = document.querySelectorAll('#org-address-book-peers-body tr[data-index]'); + addressBookPeers = Array.from(rows).map(row => { + const tagsRaw = row.querySelector('.org-ab-peer-tags')?.value || ''; + return { + id: row.querySelector('.org-ab-peer-id')?.value.trim() || '', + alias: row.querySelector('.org-ab-peer-alias')?.value.trim() || '', + tags: tagsRaw.split(',').map(tag => tag.trim()).filter(Boolean), + }; + }).filter(peer => peer.id); + } + function currentAddressBookJSON() { const textarea = document.getElementById('org-address-book-json'); const raw = textarea?.value?.trim() || '{}'; @@ -641,7 +702,8 @@ async function saveAddressBook() { let data; try { - data = currentAddressBookJSON(); + syncAddressBookPeersFromTable(); + data = buildAddressBookPayload(); } catch (err) { toast(err.message || t('address_book_invalid_json'), 'error'); return; @@ -659,7 +721,8 @@ async function importOrgDevicesIntoAddressBook() { try { - const data = currentAddressBookJSON(); + syncAddressBookPeersFromTable(); + const data = buildAddressBookPayload(); const deviceResp = await api('GET', '/devices'); const devices = deviceResp.devices || []; const peers = Array.isArray(data.peers) ? data.peers : []; @@ -677,7 +740,11 @@ data.peers = peers; data.tags = Array.isArray(data.tags) ? data.tags : []; - document.getElementById('org-address-book-json').value = formatAddressBook(data); + addressBookPeers = peers; + addressBookTags = data.tags; + renderAddressBookPeersTable(); + const tagsInput = document.getElementById('org-address-book-tags-input'); + if (tagsInput) tagsInput.value = data.tags.join(', '); toast(imported ? t('address_book_imported') : t('address_book_import_none'), imported ? 'success' : 'info'); } catch (err) { toast(err.message || t('loading_failed'), 'error'); @@ -691,19 +758,97 @@ const data = await api('GET', '/address-book'); const parsed = parseAddressBook(data.data); addressBookEnabled = data.enabled !== false; + addressBookPeers = Array.isArray(parsed.peers) ? parsed.peers.map(peer => ({ + id: String(peer.id || '').trim(), + alias: String(peer.alias || '').trim(), + tags: Array.isArray(peer.tags) ? peer.tags.map(String) : [], + })) : []; + addressBookTags = Array.isArray(parsed.tags) ? parsed.tags.map(String) : []; + addressBookShowJson = false; container.innerHTML = `

${escHtml(t('address_book_intro'))}

-
- - -

${escHtml(t('address_book_json_hint'))}

+
+
+

${escHtml(t('address_book_contacts'))}

+ +
+
+ + + + + + + + + + +
${escHtml(t('address_book_peer_id'))}${escHtml(t('address_book_alias'))}${escHtml(t('address_book_peer_tags'))}
+
+
+ + +

${escHtml(t('address_book_tags_hint'))}

+
+
+
+ +
`; + renderAddressBookPeersTable(); + + document.getElementById('org-address-book-add-peer-btn')?.addEventListener('click', () => { + syncAddressBookPeersFromTable(); + addressBookPeers.push({ id: '', alias: '', tags: [] }); + renderAddressBookPeersTable(); + }); + + document.getElementById('org-address-book-toggle-json')?.addEventListener('click', () => { + addressBookShowJson = !addressBookShowJson; + const wrap = document.getElementById('org-address-book-json-wrap'); + const btn = document.getElementById('org-address-book-toggle-json'); + const structured = document.getElementById('org-address-book-structured'); + if (addressBookShowJson) { + syncAddressBookPeersFromTable(); + const payload = buildAddressBookPayload(); + document.getElementById('org-address-book-json').value = formatAddressBook(payload); + wrap?.classList.remove('hidden'); + structured?.classList.add('hidden'); + if (btn) btn.innerHTML = `view_list ${escHtml(t('address_book_hide_json'))}`; + } else { + try { + const parsedJson = currentAddressBookJSON(); + addressBookPeers = Array.isArray(parsedJson.peers) ? parsedJson.peers : []; + addressBookTags = Array.isArray(parsedJson.tags) ? parsedJson.tags : []; + document.getElementById('org-address-book-tags-input').value = addressBookTags.join(', '); + renderAddressBookPeersTable(); + } catch (err) { + toast(err.message || t('address_book_invalid_json'), 'error'); + addressBookShowJson = true; + return; + } + wrap?.classList.add('hidden'); + structured?.classList.remove('hidden'); + if (btn) btn.innerHTML = `code ${escHtml(t('address_book_show_json'))}`; + } + }); + const saveBtn = document.getElementById('save-address-book-btn'); const importBtn = document.getElementById('import-org-devices-btn'); if (saveBtn) saveBtn.onclick = saveAddressBook; diff --git a/web-nodejs/routes/bd-api.routes.js b/web-nodejs/routes/bd-api.routes.js index 79a6a2b9..00cdb910 100644 --- a/web-nodejs/routes/bd-api.routes.js +++ b/web-nodejs/routes/bd-api.routes.js @@ -240,14 +240,29 @@ router.post('/register', identifyDevice, async (req, res) => { return res.status(400).json({ error: 'device_id is required' }); } - // Reject registration with a stale/renamed peer ID (Issue #97) - const newId = db.getRenamedPeerId ? await db.getRenamedPeerId(id) : null; - if (newId) { - return res.status(409).json({ - error: 'Device ID has been changed', - new_id: newId, - message: `This device ID was renamed to ${newId}. Please update your configuration.`, + // Reject registration with a stale/renamed peer ID unless same device (#97, #213) + if (typeof db.shouldRejectRenamedPeerRegistration === 'function') { + const renameCheck = await db.shouldRejectRenamedPeerRegistration(id, { + uuid: uuid || '', + pk: public_key || '', + ip, }); + if (renameCheck.reject && renameCheck.new_id) { + return res.status(409).json({ + error: 'Device ID has been changed', + new_id: renameCheck.new_id, + message: `This device ID was renamed to ${renameCheck.new_id}. Please update your configuration.`, + }); + } + } else { + const newId = db.getRenamedPeerId ? await db.getRenamedPeerId(id) : null; + if (newId) { + return res.status(409).json({ + error: 'Device ID has been changed', + new_id: newId, + message: `This device ID was renamed to ${newId}. Please update your configuration.`, + }); + } } // Build info JSON diff --git a/web-nodejs/routes/devices.routes.js b/web-nodejs/routes/devices.routes.js index 7ba57f35..1fd2cd58 100644 --- a/web-nodejs/routes/devices.routes.js +++ b/web-nodejs/routes/devices.routes.js @@ -457,6 +457,9 @@ router.delete('/api/devices/:id', requireAuth, requirePermission('device.delete' // Clean up local auth.db data for this peer try { await db.cleanupDeletedPeerData(id); + if (hard && typeof db.purgePanelPeerRecord === 'function') { + await db.purgePanelPeerRecord(id); + } } catch { /* non-critical: auth.db cleanup is secondary */ } // Log action @@ -614,6 +617,14 @@ router.post('/api/devices/:id/change-id', requireAuth, requirePermission('device error: mapChangeIdError(req, result?.error) }); } + + try { + if (typeof db.cascadePeerIdChange === 'function') { + await db.cascadePeerIdChange(oldId, newId.toUpperCase()); + } + } catch (cascadeErr) { + console.warn('Change ID panel cascade failed:', cascadeErr.message); + } // Log action await db.logAction(req.session.userId, 'device_id_changed', `Device ID changed from ${oldId} to ${newId}`, req.ip); @@ -1046,4 +1057,159 @@ router.post('/api/devices/:id/rename', requireAuth, requirePermission('device.ed } }); +// --------------------------------------------------------------------------- +// Panel access-control strategies (RustDesk Pro client policy rules) +// --------------------------------------------------------------------------- + +const RUSTDESK_STRATEGY_PERM_KEYS = [ + 'enable-file-transfer', + 'disable-clipboard', + 'enable-clipboard', + 'enable-audio', + 'enable-tunnel', + 'enable-camera', + 'enable-remote-restart', + 'enable-block-input', +]; + +function normalizeStrategyPermissions(raw) { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}; + const out = {}; + for (const [key, value] of Object.entries(raw)) { + if (value === true || value === 'Y' || value === 'y') out[key] = 'Y'; + else if (value === false || value === 'N' || value === 'n') out[key] = 'N'; + else if (typeof value === 'string' && value.trim()) out[key] = value.trim(); + } + return out; +} + +function serializeStrategy(row) { + if (!row) return null; + const perms = row.permissions && typeof row.permissions === 'object' ? row.permissions : {}; + return { + guid: row.guid, + name: row.name || '', + user_group_guid: row.user_group_guid || '', + device_group_guid: row.device_group_guid || '', + enabled: row.enabled === true || row.enabled === 1, + permissions: perms, + permission_keys: RUSTDESK_STRATEGY_PERM_KEYS, + created_at: row.created_at || null, + updated_at: row.updated_at || null, + }; +} + +function normalizeStrategyPayload(body = {}) { + const name = String(body.name || '').trim(); + if (!name) { + const err = new Error('Strategy name is required'); + err.status = 400; + throw err; + } + const userGroupGuid = String(body.user_group_guid || '').trim(); + const deviceGroupGuid = String(body.device_group_guid || '').trim(); + if (userGroupGuid && !isValidGroupGuid(userGroupGuid)) { + const err = new Error('Invalid user group'); + err.status = 400; + throw err; + } + if (deviceGroupGuid && !isValidGroupGuid(deviceGroupGuid)) { + const err = new Error('Invalid device group'); + err.status = 400; + throw err; + } + return { + name: name.slice(0, 80), + user_group_guid: userGroupGuid, + device_group_guid: deviceGroupGuid, + enabled: body.enabled !== false, + permissions: normalizeStrategyPermissions(body.permissions), + }; +} + +/** + * GET /api/panel/strategies — list strategies for panel UI / RustDesk sync. + */ +router.get('/api/panel/strategies', requireAuth, requirePermission('device.view'), async (req, res) => { + try { + const strategies = await db.getAllStrategies(); + res.json({ + success: true, + data: { + strategies: (strategies || []).map(serializeStrategy), + total: strategies.length, + }, + }); + } catch (err) { + console.error('Get strategies error:', err); + res.status(500).json({ success: false, error: req.t('errors.server_error') }); + } +}); + +/** + * POST /api/panel/strategies — create strategy. + */ +router.post('/api/panel/strategies', requireAuth, requirePermission('device.edit'), async (req, res) => { + try { + const payload = normalizeStrategyPayload(req.body || {}); + const created = await db.createStrategy(payload); + await db.logAction(req.session.userId, 'strategy_created', `Created strategy: ${created.name}`, req.ip); + res.json({ success: true, data: { strategy: serializeStrategy(created) } }); + } catch (err) { + console.error('Create strategy error:', err); + res.status(err.status || 500).json({ + success: false, + error: err.status === 400 ? err.message : req.t('errors.server_error'), + }); + } +}); + +/** + * PATCH /api/panel/strategies/:guid — update strategy. + */ +router.patch('/api/panel/strategies/:guid', requireAuth, requirePermission('device.edit'), async (req, res) => { + try { + const guid = String(req.params.guid || '').trim(); + if (!isValidGroupGuid(guid)) { + return res.status(400).json({ success: false, error: 'Invalid strategy identifier' }); + } + const existing = await db.getStrategyByGuid(guid); + if (!existing) { + return res.status(404).json({ success: false, error: req.t('devices.strategy_not_found') }); + } + const payload = normalizeStrategyPayload({ ...existing, ...req.body }); + const updated = await db.updateStrategy(guid, payload); + await db.logAction(req.session.userId, 'strategy_updated', `Updated strategy: ${updated.name}`, req.ip); + res.json({ success: true, data: { strategy: serializeStrategy(updated) } }); + } catch (err) { + console.error('Update strategy error:', err); + res.status(err.status || 500).json({ + success: false, + error: err.status === 400 ? err.message : req.t('errors.server_error'), + }); + } +}); + +/** + * DELETE /api/panel/strategies/:guid — delete strategy. + */ +router.delete('/api/panel/strategies/:guid', requireAuth, requirePermission('device.edit'), async (req, res) => { + try { + const guid = String(req.params.guid || '').trim(); + if (!isValidGroupGuid(guid)) { + return res.status(400).json({ success: false, error: 'Invalid strategy identifier' }); + } + const existing = await db.getStrategyByGuid(guid); + if (!existing) { + return res.status(404).json({ success: false, error: req.t('devices.strategy_not_found') }); + } + await db.deleteStrategy(guid); + await db.logAction(req.session.userId, 'strategy_deleted', `Deleted strategy: ${existing.name}`, req.ip); + res.json({ success: true }); + } catch (err) { + console.error('Delete strategy error:', err); + res.status(500).json({ success: false, error: req.t('errors.server_error') }); + } +}); + module.exports = router; diff --git a/web-nodejs/routes/rustdesk-api.routes.js b/web-nodejs/routes/rustdesk-api.routes.js index dbc4d4ee..ea16b1aa 100644 --- a/web-nodejs/routes/rustdesk-api.routes.js +++ b/web-nodejs/routes/rustdesk-api.routes.js @@ -638,14 +638,28 @@ async function getRustDeskPeerList(user, params = {}) { }; } -async function sendRustDeskDeviceGroups(req, res) { +function rustDeskAccessibleDeviceGroupPayload(group, index) { + const guid = String(group.guid || '').trim(); + return { + name: group.name || '', + guid, + note: group.note || '', + sort: typeof index === 'number' ? index : 0 + }; +} + +async function sendRustDeskDeviceGroups(req, res, accessibleOnly = null) { try { if (req.authUser && req.authUser.role === 'pro') { return res.json({ data: [], total: 0, msg: 'success' }); } + const useAccessible = accessibleOnly !== null + ? accessibleOnly + : String(req.path || '').includes('/device-group'); const groups = await getRustDeskDeviceGroups(req.authUser); + const payloadFn = useAccessible ? rustDeskAccessibleDeviceGroupPayload : rustDeskDeviceGroupPayload; return res.json({ - data: groups.map((g, i) => rustDeskDeviceGroupPayload(g, i)), + data: groups.map((g, i) => payloadFn(g, i)), total: groups.length, msg: 'success' }); @@ -1933,6 +1947,28 @@ router.post('/api/strategies', requireAuth, requireAdmin, async (req, res) => { } }); +/** + * DELETE /api/strategies/:guid + * Remove an access control strategy (admin only). + */ +router.delete('/api/strategies/:guid', requireAuth, requireAdmin, async (req, res) => { + try { + const guid = sanitizeStr(req.params.guid || '', 64); + if (!guid) { + return res.status(400).json({ error: 'Strategy guid is required' }); + } + const existing = await db.getStrategyByGuid(guid); + if (!existing) { + return res.status(404).json({ error: 'Strategy not found' }); + } + await db.deleteStrategy(guid); + return res.json({ status: 'deleted', guid }); + } catch (err) { + console.error('[API:STRATEGIES] Delete error:', err.message); + return res.status(500).json({ error: 'Server error' }); + } +}); + // ==================== Security: Peer Key Endpoint ==================== /** diff --git a/web-nodejs/services/database.js b/web-nodejs/services/database.js index 174b4fbe..2725e958 100644 --- a/web-nodejs/services/database.js +++ b/web-nodejs/services/database.js @@ -46,6 +46,15 @@ const facade = { softDeletePeer: (id) => adapter.softDeletePeer(id), deleteDevice: (id) => adapter.softDeletePeer(id), getRenamedPeerId: (id) => adapter.getRenamedPeerId ? adapter.getRenamedPeerId(id) : null, + cascadePeerIdChange: (oldId, newId) => adapter.cascadePeerIdChange + ? adapter.cascadePeerIdChange(oldId, newId) + : Promise.resolve(), + purgePanelPeerRecord: (id) => adapter.purgePanelPeerRecord + ? adapter.purgePanelPeerRecord(id) + : Promise.resolve(), + shouldRejectRenamedPeerRegistration: (oldId, identity) => adapter.shouldRejectRenamedPeerRegistration + ? adapter.shouldRejectRenamedPeerRegistration(oldId, identity) + : Promise.resolve({ reject: false }), setBanStatus: (id, banned, reason) => adapter.setBanStatus(id, banned, reason), getPeerStats: () => adapter.getPeerStats(), getStats: () => adapter.getPeerStats(), diff --git a/web-nodejs/services/dbAdapter.js b/web-nodejs/services/dbAdapter.js index 864ddd99..3b675ed6 100644 --- a/web-nodejs/services/dbAdapter.js +++ b/web-nodejs/services/dbAdapter.js @@ -28,6 +28,39 @@ const { hashAccessToken } = require('../lib/tokenHash'); // Lazy-loaded drivers — keeps startup fast when one backend isn't installed. let _sqlite = null; + +/** Match Go signal peerIPMatches — client host vs stored peer IP (may include port). */ +function peerIPMatches(clientHost, storedIP) { + if (!clientHost || !storedIP) return false; + let storedHost = storedIP; + const idx = storedIP.lastIndexOf(':'); + if (idx > 0) { + const hostPart = storedIP.slice(0, idx); + if (/^\d+\.\d+\.\d+\.\d+$/.test(hostPart)) storedHost = hostPart; + } + return clientHost === storedHost || storedIP.startsWith(`${clientHost}:`); +} + +function pkEqual(a, b) { + if (!a || !b) return false; + try { + const toBuf = (v) => { + if (Buffer.isBuffer(v)) return v; + const s = String(v); + return /^[0-9a-f]+$/i.test(s) ? Buffer.from(s, 'hex') : Buffer.from(s); + }; + const bufA = toBuf(a); + const bufB = toBuf(b); + return bufA.length === bufB.length && bufA.equals(bufB); + } catch (_) { + return false; + } +} + +function uuidEqual(a, b) { + if (!a || !b) return false; + return String(a).replace(/-/g, '').toLowerCase() === String(b).replace(/-/g, '').toLowerCase(); +} let _pg = null; function getSqliteDriver() { @@ -1021,6 +1054,112 @@ function createSqliteAdapter(config) { let _lastGoPeerSyncSqlite = 0; const GO_SYNC_INTERVAL_SQLITE_MS = 30_000; + function cascadePeerIdChangeSqlite(oldId, newId) { + if (!oldId || !newId || oldId === newId) return; + try { + openMain().prepare('UPDATE peer SET id = ? WHERE id = ?').run(newId, oldId); + } catch (err) { + console.warn('[DB] cascadePeerIdChange peer table:', err.message); + } + try { + const authDb = openAuth(); + const stmts = [ + ['UPDATE peer_sysinfo SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ['UPDATE peer_metrics SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ['UPDATE device_folder_assignments SET device_id = ? WHERE device_id = ?', [newId, oldId]], + ['UPDATE device_folder_assignments SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ['UPDATE device_group_peers SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ['UPDATE access_tokens SET client_id = ? WHERE client_id = ?', [newId, oldId]], + ['UPDATE audit_connections SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ['UPDATE audit_files SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ['UPDATE audit_alarms SET peer_id = ? WHERE peer_id = ?', [newId, oldId]], + ]; + for (const [sql, params] of stmts) { + try { + authDb.prepare(sql).run(...params); + } catch (err) { + if (!err.message.includes('no such table') && !err.message.includes('no such column')) { + console.warn('[DB] cascadePeerIdChange:', err.message); + } + } + } + } catch (err) { + console.warn('[DB] cascadePeerIdChange auth:', err.message); + } + } + + function syncPanelPeerIdRenamesFromGoSqlite() { + const db = openMain(); + try { + const histTbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='id_change_history'").get(); + const peersTbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='peers'").get(); + if (!histTbl || !peersTbl) return; + const rows = db.prepare(` + SELECT h.old_id, h.new_id FROM id_change_history h + INNER JOIN peers p ON p.id = h.new_id AND (p.soft_deleted IS NULL OR p.soft_deleted = 0) + WHERE EXISTS (SELECT 1 FROM peer WHERE id = h.old_id) + `).all(); + for (const row of rows) { + cascadePeerIdChangeSqlite(row.old_id, row.new_id); + } + } catch (err) { + if (!err.message.includes('no such table')) { + console.warn('[DB] syncPanelPeerIdRenamesFromGo:', err.message); + } + } + } + + function purgePanelPeerRecordSqlite(id) { + if (!id) return; + try { + openMain().prepare('DELETE FROM peer WHERE id = ?').run(id); + } catch (err) { + console.warn('[DB] purgePanelPeerRecord:', err.message); + } + } + + /** + * Identity-aware rename guard — mirrors Go signal rejectRenamedPeerRegistration (#213). + * @returns {{ reject: boolean, new_id?: string }} + */ + function shouldRejectRenamedPeerRegistrationSqlite(oldId, { uuid, pk, ip } = {}) { + const newId = (() => { + try { + const db = openMain(); + const tbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='id_change_history'").get(); + if (!tbl) return null; + const row = db.prepare('SELECT new_id FROM id_change_history WHERE old_id = ? ORDER BY rowid DESC LIMIT 1').get(oldId); + return row ? row.new_id : null; + } catch (_) { + return null; + } + })(); + if (!newId) return { reject: false }; + + const db = openMain(); + let successor = null; + try { + const peersTbl = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='peers'").get(); + if (peersTbl) { + successor = db.prepare('SELECT pk, uuid, ip FROM peers WHERE id = ? AND NOT soft_deleted').get(newId); + } + } catch (_) {} + if (!successor) { + try { + successor = db.prepare('SELECT pk, uuid, ip FROM peer WHERE id = ? AND is_deleted = 0').get(newId); + } catch (_) {} + } + if (!successor) return { reject: false }; + + const pkVal = pk || ''; + const uuidVal = uuid || ''; + if (pkVal && successor.pk && pkEqual(pkVal, successor.pk)) return { reject: false }; + if (uuidVal && successor.uuid && uuidEqual(uuidVal, successor.uuid)) return { reject: false }; + if (!pkVal && !uuidVal && ip && peerIPMatches(ip, successor.ip || '')) return { reject: false }; + + return { reject: true, new_id: newId }; + } + function syncGoPeersSqlite() { const now = Date.now(); if (now - _lastGoPeerSyncSqlite < GO_SYNC_INTERVAL_SQLITE_MS) return; @@ -1078,6 +1217,7 @@ function createSqliteAdapter(config) { console.log(`[DB] syncGoPeersSqlite: cleaned up ${result.changes} ghost peer(s) from ID changes`); } } + syncPanelPeerIdRenamesFromGoSqlite(); } catch (err) { if (!err.message.includes('no such table')) { console.warn('[DB] syncGoPeersSqlite error:', err.message); @@ -1235,6 +1375,21 @@ function createSqliteAdapter(config) { } catch (_) { return null; } }, + /** + * Update auth.db and panel peer rows when a device ID changes (#213). + */ + cascadePeerIdChange(oldId, newId) { + cascadePeerIdChangeSqlite(oldId, newId); + }, + + purgePanelPeerRecord(id) { + purgePanelPeerRecordSqlite(id); + }, + + shouldRejectRenamedPeerRegistration(oldId, identity) { + return shouldRejectRenamedPeerRegistrationSqlite(oldId, identity); + }, + async setBanStatus(id, banned, reason = '') { openMain().prepare(` UPDATE peer SET is_banned = ?, banned_at = CASE WHEN ? THEN datetime('now') ELSE NULL END, banned_reason = ? @@ -4048,6 +4203,94 @@ function createPostgresAdapter() { let _lastGoPeerSync = 0; const GO_SYNC_INTERVAL_MS = 30_000; // sync at most every 30 seconds + async function cascadePeerIdChangePg(oldId, newId) { + if (!oldId || !newId || oldId === newId) return; + try { + await q('UPDATE peer SET id = $1 WHERE id = $2', [newId, oldId]); + } catch (err) { + console.warn('[DB] cascadePeerIdChange peer table:', err.message); + } + const stmts = [ + ['UPDATE peer_sysinfo SET peer_id = $1 WHERE peer_id = $2', [newId, oldId]], + ['UPDATE peer_metrics SET peer_id = $1 WHERE peer_id = $2', [newId, oldId]], + ['UPDATE device_folder_assignments SET device_id = $1 WHERE device_id = $2', [newId, oldId]], + ['UPDATE device_group_peers SET peer_id = $1 WHERE peer_id = $2', [newId, oldId]], + ['UPDATE access_tokens SET client_id = $1 WHERE client_id = $2', [newId, oldId]], + ['UPDATE audit_connections SET peer_id = $1 WHERE peer_id = $2', [newId, oldId]], + ['UPDATE audit_files SET peer_id = $1 WHERE peer_id = $2', [newId, oldId]], + ['UPDATE audit_alarms SET peer_id = $1 WHERE peer_id = $2', [newId, oldId]], + ]; + for (const [sql, params] of stmts) { + try { + await q(sql, params); + } catch (err) { + if (!err.message.includes('does not exist')) { + console.warn('[DB] cascadePeerIdChange:', err.message); + } + } + } + } + + async function syncPanelPeerIdRenamesFromGoPg() { + try { + const rows = await all(` + SELECT h.old_id, h.new_id FROM id_change_history h + INNER JOIN peers p ON p.id = h.new_id AND NOT p.soft_deleted + WHERE EXISTS (SELECT 1 FROM peer WHERE id = h.old_id) + `); + for (const row of rows) { + await cascadePeerIdChangePg(row.old_id, row.new_id); + } + } catch (err) { + if (!err.message.includes('does not exist')) { + console.warn('[DB] syncPanelPeerIdRenamesFromGo:', err.message); + } + } + } + + async function purgePanelPeerRecordPg(id) { + if (!id) return; + try { + await q('DELETE FROM peer WHERE id = $1', [id]); + } catch (err) { + console.warn('[DB] purgePanelPeerRecord:', err.message); + } + } + + /** + * Identity-aware rename guard — mirrors Go signal rejectRenamedPeerRegistration (#213). + * @returns {Promise<{ reject: boolean, new_id?: string }>} + */ + async function shouldRejectRenamedPeerRegistrationPg(oldId, { uuid, pk, ip } = {}) { + let newId = null; + try { + const row = await q1('SELECT new_id FROM id_change_history WHERE old_id = $1 ORDER BY id DESC LIMIT 1', [oldId]); + newId = row ? row.new_id : null; + } catch (_) { + return { reject: false }; + } + if (!newId) return { reject: false }; + + let successor = null; + try { + successor = await q1('SELECT pk, uuid, ip FROM peers WHERE id = $1 AND NOT soft_deleted', [newId]); + } catch (_) {} + if (!successor) { + try { + successor = await q1('SELECT pk, uuid, ip FROM peer WHERE id = $1 AND is_deleted = FALSE', [newId]); + } catch (_) {} + } + if (!successor) return { reject: false }; + + const pkVal = pk || ''; + const uuidVal = uuid || ''; + if (pkVal && successor.pk && pkEqual(pkVal, successor.pk)) return { reject: false }; + if (uuidVal && successor.uuid && uuidEqual(uuidVal, successor.uuid)) return { reject: false }; + if (!pkVal && !uuidVal && ip && peerIPMatches(ip, successor.ip || '')) return { reject: false }; + + return { reject: true, new_id: newId }; + } + /** * Sync devices from Go server's "peers" table into Node.js "peer" table. * Called before bulk queries (getAllPeers, getPeerStats) to ensure the @@ -4088,6 +4331,14 @@ function createPostgresAdapter() { info = CASE WHEN peer.info IS NULL OR peer.info = '{}' THEN EXCLUDED.info ELSE peer.info END, is_deleted = FALSE `); + await q(` + DELETE FROM peer WHERE id IN ( + SELECT h.old_id FROM id_change_history h + LEFT JOIN peers p ON p.id = h.old_id AND NOT p.soft_deleted + WHERE p.id IS NULL + ) + `); + await syncPanelPeerIdRenamesFromGoPg(); } catch (err) { // 'peers' table might not exist when Go server is not used if (!err.message.includes('does not exist')) { @@ -4227,6 +4478,18 @@ function createPostgresAdapter() { } catch (_) { return null; } }, + async cascadePeerIdChange(oldId, newId) { + await cascadePeerIdChangePg(oldId, newId); + }, + + async purgePanelPeerRecord(id) { + await purgePanelPeerRecordPg(id); + }, + + async shouldRejectRenamedPeerRegistration(oldId, identity) { + return shouldRejectRenamedPeerRegistrationPg(oldId, identity); + }, + async setBanStatus(id, banned, reason = '') { await q(` UPDATE peer SET is_banned = $1, banned_at = CASE WHEN $1 THEN NOW() ELSE NULL END, banned_reason = $2 diff --git a/web-nodejs/services/deviceStatusPush.js b/web-nodejs/services/deviceStatusPush.js index a2d4aeca..5f5c6428 100644 --- a/web-nodejs/services/deviceStatusPush.js +++ b/web-nodejs/services/deviceStatusPush.js @@ -4,13 +4,14 @@ * Connects to Go server's WebSocket event bus and pushes device status * changes to browser clients in real time. * - * Go server endpoint: GET /api/ws/events?filter=peer_online + * Go server endpoint: GET /api/ws/events * Browser endpoint: WS /ws/device-status */ 'use strict'; const WebSocket = require('ws'); +const db = require('./database'); const log = { info: (...a) => console.log('[DeviceStatus]', ...a), @@ -91,7 +92,7 @@ function initDeviceStatusPush(httpServer, sessionMiddleware, goApiUrl, apiKey) { .replace(/^https:/, 'wss:') .replace(/\/api$/, ''); - const url = `${wsUrl}/api/ws/events?filter=peer_online&api_key=${encodeURIComponent(apiKey)}`; + const url = `${wsUrl}/api/ws/events?api_key=${encodeURIComponent(apiKey)}`; log.info('Connecting to Go event bus...'); @@ -111,13 +112,33 @@ function initDeviceStatusPush(httpServer, sessionMiddleware, goApiUrl, apiKey) { goWs.on('message', (data) => { try { const event = JSON.parse(data.toString()); + const payload = event.data || {}; + + if (event.type === 'peer_id_changed') { + const oldId = payload.old_id; + const newId = payload.new_id; + if (oldId && newId && typeof db.cascadePeerIdChange === 'function') { + Promise.resolve(db.cascadePeerIdChange(oldId, newId)).catch((err) => { + log.warn('Panel ID cascade failed:', err.message || err); + }); + } + broadcast({ + type: 'device_id_changed', + old_id: oldId, + new_id: newId, + source: payload.source || '', + timestamp: event.timestamp || Date.now(), + }); + return; + } + // Forward peer status events to browser clients if (event.type === 'peer_online' || event.type === 'peer_offline' || event.type === 'peer_status_changed' || event.type === 'peer_registered') { broadcast({ type: 'device_status', - device_id: event.peer_id || event.id || event.device_id, - status: event.status || (event.type === 'peer_online' ? 'online' : 'offline'), + device_id: payload.peer_id || payload.id || event.peer_id || event.id || event.device_id, + status: payload.status || event.status || (event.type === 'peer_online' ? 'online' : 'offline'), timestamp: event.timestamp || Date.now(), details: event, }); diff --git a/web-nodejs/tests/bd-api.routes.test.js b/web-nodejs/tests/bd-api.routes.test.js new file mode 100644 index 00000000..21e0648a --- /dev/null +++ b/web-nodejs/tests/bd-api.routes.test.js @@ -0,0 +1,58 @@ +'use strict'; + +const request = require('supertest'); +const express = require('express'); + +jest.mock('../services/authService', () => ({})); + +jest.mock('../services/database', () => ({ + shouldRejectRenamedPeerRegistration: jest.fn(), + getRenamedPeerId: jest.fn(), + upsertPeer: jest.fn().mockResolvedValue(undefined), + updatePeerOnlineStatus: jest.fn().mockResolvedValue(undefined), + getAccessToken: jest.fn(), + touchAccessToken: jest.fn(), +})); + +const db = require('../services/database'); +const bdApiRoutes = require('../routes/bd-api.routes'); + +describe('BD-API register rename guard', () => { + let app; + + beforeEach(() => { + app = express(); + app.use(express.json()); + app.use('/api/bd', bdApiRoutes); + jest.clearAllMocks(); + }); + + it('rejects stale renamed ID when identity does not match successor', async () => { + db.shouldRejectRenamedPeerRegistration.mockResolvedValue({ + reject: true, + new_id: 'NEWID123', + }); + + const res = await request(app) + .post('/api/bd/register') + .set('X-Device-Id', 'OLDID123') + .send({ device_id: 'OLDID123', uuid: 'other-uuid' }); + + expect(res.status).toBe(409); + expect(res.body.new_id).toBe('NEWID123'); + expect(db.upsertPeer).not.toHaveBeenCalled(); + }); + + it('allows registration when same device owns renamed ID', async () => { + db.shouldRejectRenamedPeerRegistration.mockResolvedValue({ reject: false }); + + const res = await request(app) + .post('/api/bd/register') + .set('X-Device-Id', 'OLDID123') + .send({ device_id: 'OLDID123', uuid: 'device-uuid', hostname: 'PC' }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(db.upsertPeer).toHaveBeenCalled(); + }); +}); diff --git a/web-nodejs/tests/devices.routes.test.js b/web-nodejs/tests/devices.routes.test.js index 2b15e78f..4844a764 100644 --- a/web-nodejs/tests/devices.routes.test.js +++ b/web-nodejs/tests/devices.routes.test.js @@ -26,7 +26,9 @@ jest.mock('../services/database', () => ({ removeDeviceFromGroup: jest.fn().mockResolvedValue(undefined), getAllFolders: jest.fn().mockResolvedValue([]), getAllFolderAssignments: jest.fn().mockResolvedValue({}), - cleanupDeletedPeerData: jest.fn().mockResolvedValue(undefined) + cleanupDeletedPeerData: jest.fn().mockResolvedValue(undefined), + cascadePeerIdChange: jest.fn().mockResolvedValue(undefined), + purgePanelPeerRecord: jest.fn().mockResolvedValue(undefined) })); jest.mock('../services/serverBackend', () => ({ @@ -396,6 +398,22 @@ describe('Devices Routes', () => { expect(res.body.success).toBe(false); expect(res.body.error).toBe('devices.id_reserved_deleted'); }); + + it('should cascade panel peer ID change into local databases', async () => { + serverBackend.getDeviceById.mockImplementation(async (id) => { + if (id === 'OLDCAS') return { id: 'OLDCAS', online: false }; + return null; + }); + serverBackend.changePeerId.mockResolvedValue({ success: true }); + + const res = await request(app) + .post('/api/devices/OLDCAS/change-id') + .send({ newId: 'NEWCAS' }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(db.cascadePeerIdChange).toHaveBeenCalledWith('OLDCAS', 'NEWCAS'); + }); }); describe('DELETE /api/devices/:id', () => { @@ -413,6 +431,7 @@ describe('Devices Routes', () => { cascade: false, hard: true }); + expect(db.purgePanelPeerRecord).toHaveBeenCalledWith('MACPRO'); }); it('should hard delete a soft-deleted device when active lookup misses', async () => { diff --git a/web-nodejs/views/devices.ejs b/web-nodejs/views/devices.ejs index c6f25f9a..4310eb3e 100644 --- a/web-nodejs/views/devices.ejs +++ b/web-nodejs/views/devices.ejs @@ -57,6 +57,23 @@
+ + +
+
+
+

${_('devices.strategies_title')}

+

${_('devices.strategies_hint')}

+
+ +
+
+
${_('devices.loading_strategies')}
+
+