mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
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:
+7
-1
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, `
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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+":")
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
{
|
||||
{
|
||||
"_meta": {
|
||||
"name": "Norwegian",
|
||||
"native_name": "Norsk",
|
||||
@@ -244,6 +244,7 @@
|
||||
"change_id_title": "Endre enhets-ID",
|
||||
"new_id": "New ID",
|
||||
"change_id_hint": "Kun 6-16 tegn, bokstaver, tall, bindestreker og understreker",
|
||||
"change_id_online_warn": "Enheten er online. Endre ID helst i RustDesk-klienten. Endring i panelet beholder identiteten, men klienten må ta i bruk ny ID.",
|
||||
"change_id_success": "Enhets-ID ble endret",
|
||||
"id_length_error": "ID må bestå av 6–16 tegn",
|
||||
"id_format_error": "ID kan bare inneholde bokstaver, tall, bindestreker og understreker",
|
||||
@@ -340,6 +341,41 @@
|
||||
"group_invalid": "Ugyldig gruppeidentifikator",
|
||||
"group_not_found": "Finner ikke gruppen",
|
||||
"group_saved": "Enhetsgruppen er lagret",
|
||||
"strategies_title": "Access control strategies",
|
||||
"strategies_hint": "Link user groups to device groups and push RustDesk Pro security settings to matching clients.",
|
||||
"create_strategy": "Add strategy",
|
||||
"edit_strategy": "Edit strategy",
|
||||
"delete_strategy": "Delete strategy",
|
||||
"delete_strategy_confirm": "Delete strategy {name}?",
|
||||
"strategy_name": "Strategy name",
|
||||
"strategy_name_required": "Strategy name is required",
|
||||
"strategy_user_group": "User group",
|
||||
"strategy_device_group": "Device group",
|
||||
"strategy_any_user_group": "Any user group",
|
||||
"strategy_any_device_group": "Any device group",
|
||||
"strategy_enabled": "Enabled",
|
||||
"strategy_disabled": "Disabled",
|
||||
"strategy_enabled_label": "Strategy enabled",
|
||||
"strategy_permissions": "Client security settings",
|
||||
"strategy_permissions_hint": "Uses RustDesk advanced-setting keys (Y/N). Leave Default to keep client or strategy inheritance.",
|
||||
"strategy_permissions_count": "{count} settings",
|
||||
"strategy_perm_default": "Default",
|
||||
"strategy_perm_allow": "Allow (Y)",
|
||||
"strategy_perm_deny": "Deny (N)",
|
||||
"strategy_perm_file_transfer": "File transfer",
|
||||
"strategy_perm_disable_clipboard": "Disable clipboard",
|
||||
"strategy_perm_enable_clipboard": "Enable clipboard",
|
||||
"strategy_perm_audio": "Audio",
|
||||
"strategy_perm_tunnel": "TCP tunnel",
|
||||
"strategy_perm_camera": "Camera",
|
||||
"strategy_perm_restart": "Remote restart",
|
||||
"strategy_perm_block_input": "Block user input",
|
||||
"strategy_created": "Strategy created",
|
||||
"strategy_updated": "Strategy updated",
|
||||
"strategy_deleted": "Strategy deleted",
|
||||
"strategy_not_found": "Strategy not found",
|
||||
"no_strategies": "No access control strategies yet",
|
||||
"loading_strategies": "Loading strategies...",
|
||||
"groups_saved": "Enhetsgrupper lagret",
|
||||
"group_membership_added": "Lagt til i gruppe",
|
||||
"group_membership_removed": "Fjernet fra gruppe",
|
||||
@@ -2606,7 +2642,19 @@
|
||||
"address_book_saved": "Adressebok lagret",
|
||||
"address_book_save_failed": "Kunne ikke lagre adresseboken",
|
||||
"address_book_imported": "Organisasjonsenheter importert",
|
||||
"address_book_import_none": "Ingen nye organisasjonsenheter å importere"
|
||||
"address_book_import_none": "Ingen nye organisasjonsenheter å importere",
|
||||
"address_book_contacts": "Shared contacts",
|
||||
"address_book_add_peer": "Add contact",
|
||||
"address_book_peer_id": "Device ID",
|
||||
"address_book_alias": "Alias",
|
||||
"address_book_peer_tags": "Contact tags",
|
||||
"address_book_peer_tags_placeholder": "office, support",
|
||||
"address_book_tags_label": "Global tags",
|
||||
"address_book_tags_placeholder": "Office, Support",
|
||||
"address_book_tags_hint": "Comma-separated tags available in the RustDesk client address book.",
|
||||
"address_book_no_peers": "No contacts yet — add a device ID or import organization devices.",
|
||||
"address_book_show_json": "Advanced JSON",
|
||||
"address_book_hide_json": "Structured editor"
|
||||
},
|
||||
"chat": {
|
||||
"e2e_enabled": "Ende-til-ende kryptert",
|
||||
|
||||
+50
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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": "端到端加密",
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user