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.
This commit is contained in:
UNITRONIX
2026-07-05 17:13:33 +02:00
parent 27344b5cb8
commit c752674139
51 changed files with 2997 additions and 109 deletions
+7 -1
View File
@@ -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.
---
+57 -1
View File
@@ -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"})
}
+14 -1
View File
@@ -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,
+5 -1
View File
@@ -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)
+64 -6
View File
@@ -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, `
+68 -6
View File
@@ -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()
+62 -5
View File
@@ -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.
+35 -14
View File
@@ -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)
}
+114
View File
@@ -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)
@@ -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+":")
}
+1 -2
View File
@@ -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
}
+50 -2
View File
@@ -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": "مشفرة من طرف إلى طرف",
+50 -2
View File
@@ -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í",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+49 -1
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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": "एंड-टू-एंड एन्क्रिप्टेड",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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": "エンドツーエンド暗号化",
+50 -2
View File
@@ -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": "종단 간 암호화",
+50 -2
View File
@@ -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 616 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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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": "เข้ารหัสตั้งแต่ต้นทางถึงปลายทาง",
+50 -2
View File
@@ -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ş",
+50 -2
View File
@@ -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": "Наскрізне шифрування",
+50 -2
View File
@@ -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",
+50 -2
View File
@@ -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": "端到端加密",
+50 -2
View File
@@ -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": "端到端加密",
+116
View File
@@ -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; }
+36
View File
@@ -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;
+49
View File
@@ -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 = '<span class="status-dot"></span>' + 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'),
+232
View File
@@ -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 = `<div class="empty-state-inline">${_('devices.loading_strategies')}</div>`;
return;
}
if (!accessStrategies.length) {
container.innerHTML = `<div class="empty-state-inline">${_('devices.no_strategies')}</div>`;
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 `<div class="strategy-manager-item ${enabled ? '' : 'strategy-disabled'}" data-guid="${Utils.escapeHtml(strategy.guid)}">
<div class="strategy-manager-main">
<span class="material-icons">policy</span>
<div class="strategy-manager-text">
<strong>${Utils.escapeHtml(strategy.name)}</strong>
<span>${Utils.escapeHtml(userGroupName(strategy.user_group_guid))} ${Utils.escapeHtml(deviceGroupName(strategy.device_group_guid))}</span>
<span class="strategy-perm-count">${_('devices.strategy_permissions_count', { count: permCount }) || permCount + ' settings'}</span>
</div>
</div>
<span class="strategy-status-badge ${enabled ? 'enabled' : 'disabled'}">${enabled ? _('devices.strategy_enabled') : _('devices.strategy_disabled')}</span>
<div class="strategy-manager-actions">
<button type="button" class="action-btn" data-action="edit-strategy" data-guid="${Utils.escapeHtml(strategy.guid)}" title="${_('devices.edit_strategy')}">
<span class="material-icons">edit</span>
</button>
<button type="button" class="action-btn danger" data-action="delete-strategy" data-guid="${Utils.escapeHtml(strategy.guid)}" title="${_('devices.delete_strategy')}">
<span class="material-icons">delete</span>
</button>
</div>
</div>`;
}).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 `<label class="strategy-perm-option">
<span>${Utils.escapeHtml(_(item.labelKey))}</span>
<select class="form-input strategy-perm-select" data-perm-key="${Utils.escapeHtml(item.key)}">
<option value="" ${!checked && !denied ? 'selected' : ''}>${_('devices.strategy_perm_default')}</option>
<option value="Y" ${checked ? 'selected' : ''}>${_('devices.strategy_perm_allow')}</option>
<option value="N" ${denied ? 'selected' : ''}>${_('devices.strategy_perm_deny')}</option>
</select>
</label>`;
}).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 = [
`<option value="">${Utils.escapeHtml(_('devices.strategy_any_user_group'))}</option>`,
...availableUserGroups.map(group => `<option value="${Utils.escapeHtml(group.guid)}" ${strategy?.user_group_guid === group.guid ? 'selected' : ''}>${Utils.escapeHtml(group.name)}</option>`)
].join('');
const deviceGroupOptions = [
`<option value="">${Utils.escapeHtml(_('devices.strategy_any_device_group'))}</option>`,
...deviceGroups.map(group => `<option value="${Utils.escapeHtml(group.guid)}" ${strategy?.device_group_guid === group.guid ? 'selected' : ''}>${Utils.escapeHtml(group.name)}</option>`)
].join('');
Modal.show({
title: editing ? _('devices.edit_strategy') : _('devices.create_strategy'),
content: `
<form id="strategy-form" class="device-group-form">
<div class="form-group">
<label for="strategy-name">${_('devices.strategy_name')}</label>
<input type="text" id="strategy-name" class="form-input" maxlength="80" required value="${Utils.escapeHtml(strategy?.name || '')}">
</div>
<div class="form-group">
<label for="strategy-user-group">${_('devices.strategy_user_group')}</label>
<select id="strategy-user-group" class="form-input">${userGroupOptions}</select>
</div>
<div class="form-group">
<label for="strategy-device-group">${_('devices.strategy_device_group')}</label>
<select id="strategy-device-group" class="form-input">${deviceGroupOptions}</select>
</div>
<label class="toggle-row">
<input type="checkbox" id="strategy-enabled" ${strategy?.enabled !== false ? 'checked' : ''}>
<span>${_('devices.strategy_enabled_label')}</span>
</label>
<div class="form-group">
<label>${_('devices.strategy_permissions')}</label>
<div class="strategy-perm-grid">${renderStrategyPermissionOptions(strategy?.permissions || {})}</div>
<p class="form-hint">${_('devices.strategy_permissions_hint')}</p>
</div>
</form>
`,
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 ====================
+152 -7
View File
@@ -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 = `<tr><td colspan="4" class="org-ab-empty">${escHtml(t('address_book_no_peers'))}</td></tr>`;
return;
}
tbody.innerHTML = addressBookPeers.map((peer, index) => `
<tr data-index="${index}">
<td><input type="text" class="form-input org-ab-peer-id" value="${escHtml(peer.id || '')}" maxlength="64" placeholder="123456789"></td>
<td><input type="text" class="form-input org-ab-peer-alias" value="${escHtml(peer.alias || '')}" maxlength="120" placeholder="${escHtml(t('address_book_alias'))}"></td>
<td><input type="text" class="form-input org-ab-peer-tags" value="${escHtml((peer.tags || []).join(', '))}" maxlength="200" placeholder="${escHtml(t('address_book_peer_tags_placeholder'))}"></td>
<td class="org-ab-actions">
<button type="button" class="action-btn danger org-ab-remove-peer" data-index="${index}" title="${escHtml(t('actions.delete'))}">
<span class="material-icons">delete</span>
</button>
</td>
</tr>
`).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 = `
<p class="form-hint">${escHtml(t('address_book_intro'))}</p>
<label class="org-address-book-toggle">
<input type="checkbox" id="org-address-book-enabled" ${addressBookEnabled ? 'checked' : ''}>
<span>${escHtml(t('address_book_enabled'))}</span>
</label>
<div class="form-group">
<label class="form-label" for="org-address-book-json">${escHtml(t('address_book_json'))}</label>
<textarea id="org-address-book-json" class="form-input org-address-book-json" spellcheck="false">${escHtml(formatAddressBook(parsed))}</textarea>
<p class="form-hint">${escHtml(t('address_book_json_hint'))}</p>
<div class="org-address-book-structured" id="org-address-book-structured">
<div class="org-section-header org-ab-section-header">
<h3>${escHtml(t('address_book_contacts'))}</h3>
<button type="button" class="btn btn-secondary btn-sm" id="org-address-book-add-peer-btn">
<span class="material-icons">person_add</span>
${escHtml(t('address_book_add_peer'))}
</button>
</div>
<div class="org-table-container org-ab-table-wrap">
<table class="org-table org-address-book-table">
<thead>
<tr>
<th>${escHtml(t('address_book_peer_id'))}</th>
<th>${escHtml(t('address_book_alias'))}</th>
<th>${escHtml(t('address_book_peer_tags'))}</th>
<th></th>
</tr>
</thead>
<tbody id="org-address-book-peers-body"></tbody>
</table>
</div>
<div class="form-group">
<label class="form-label" for="org-address-book-tags-input">${escHtml(t('address_book_tags_label'))}</label>
<input type="text" id="org-address-book-tags-input" class="form-input" value="${escHtml(addressBookTags.join(', '))}" placeholder="${escHtml(t('address_book_tags_placeholder'))}">
<p class="form-hint">${escHtml(t('address_book_tags_hint'))}</p>
</div>
</div>
<div class="org-address-book-advanced">
<button type="button" class="btn btn-secondary btn-sm" id="org-address-book-toggle-json">
<span class="material-icons">code</span>
${escHtml(t('address_book_show_json'))}
</button>
<div class="form-group org-address-book-json-wrap hidden" id="org-address-book-json-wrap">
<label class="form-label" for="org-address-book-json">${escHtml(t('address_book_json'))}</label>
<textarea id="org-address-book-json" class="form-input org-address-book-json" spellcheck="false">${escHtml(formatAddressBook(parsed))}</textarea>
<p class="form-hint">${escHtml(t('address_book_json_hint'))}</p>
</div>
</div>
`;
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 = `<span class="material-icons">view_list</span> ${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 = `<span class="material-icons">code</span> ${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;
+22 -7
View File
@@ -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
+166
View File
@@ -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;
+38 -2
View File
@@ -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 ====================
/**
+9
View File
@@ -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(),
+263
View File
@@ -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
+25 -4
View File
@@ -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,
});
+58
View File
@@ -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();
});
});
+20 -1
View File
@@ -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 () => {
+17
View File
@@ -57,6 +57,23 @@
</button>
</div>
</div>
<!-- Access control strategies (RustDesk Pro) -->
<div class="devices-strategies-card" id="devices-strategies-card">
<div class="devices-strategies-header">
<div>
<h3>${_('devices.strategies_title')}</h3>
<p>${_('devices.strategies_hint')}</p>
</div>
<button type="button" class="btn btn-secondary btn-sm" id="add-strategy-btn">
<span class="material-icons">policy</span>
${_('devices.create_strategy')}
</button>
</div>
<div class="devices-strategies-list" id="devices-strategies-list">
<div class="empty-state-inline">${_('devices.loading_strategies')}</div>
</div>
</div>
</div>
<!-- Toolbar -->