diff --git a/betterdesk-server/api/auth_handlers.go b/betterdesk-server/api/auth_handlers.go index c0b8e7a6..78f36750 100644 --- a/betterdesk-server/api/auth_handlers.go +++ b/betterdesk-server/api/auth_handlers.go @@ -452,6 +452,46 @@ func (s *Server) handleClientUsersList(w http.ResponseWriter, r *http.Request) { }) } +// handleUsersWithClientFallback detects RustDesk client requests (with +// ?accessible or ?pageSize) and returns the current user without requiring +// user.view permission. This prevents the client's group pull from failing +// for operator users. Admin panel requests fall through to the full +// permission-protected list handler. +// Issue #138: _getUsers() failure causes _getPeers() to never run. +func (s *Server) handleUsersWithClientFallback(w http.ResponseWriter, r *http.Request) { + // RustDesk client sends ?accessible=&status=1&pageSize=100 + if r.URL.Query().Has("accessible") || r.URL.Query().Has("pageSize") { + username := getUsernameFromCtx(r) + role := getRoleFromCtx(r) + + // If user has user.view permission, return full list + if auth.IsSuperAdminRole(role) || auth.RoleHasPermission(role, auth.PermUserView) { + s.handleClientUsersList(w, r) + return + } + + // For operators without user.view, return current user only + // so the client's _getUsers() succeeds and _getPeers() proceeds + statusInt := 1 + isAdmin := auth.IsSuperAdminRole(role) + writeJSON(w, http.StatusOK, map[string]any{ + "total": 1, + "data": []map[string]any{{ + "name": username, + "display_name": username, + "email": "", + "note": "", + "status": statusInt, + "is_admin": isAdmin, + }}, + }) + return + } + + // Admin panel request — require user.view permission + s.requirePermission(auth.PermUserView, s.handleListUsers)(w, r) +} + // handleCreateUser creates a new user account. // POST /api/users func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { diff --git a/betterdesk-server/api/server.go b/betterdesk-server/api/server.go index 01c89d34..24234f10 100644 --- a/betterdesk-server/api/server.go +++ b/betterdesk-server/api/server.go @@ -310,7 +310,12 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("POST /api/sysinfo_ver", s.handleClientSysinfoVer) // User management (permission-based) - mux.HandleFunc("GET /api/users", s.requirePermission(auth.PermUserView, s.handleListUsers)) + // Issue #138: RustDesk client calls GET /api/users?accessible&pageSize=100 + // with operator tokens. The _getUsers() result gates the entire group pull — + // if it fails (403), _getPeers() is never called and the Available Devices + // tab stays empty. Use a wrapper that detects client requests and returns + // only the current user before the permission check. + mux.HandleFunc("GET /api/users", s.handleUsersWithClientFallback) mux.HandleFunc("POST /api/users", s.requirePermission(auth.PermUserCreate, s.handleCreateUser)) mux.HandleFunc("PUT /api/users/{id}", s.requirePermission(auth.PermUserEdit, s.handleUpdateUser)) mux.HandleFunc("DELETE /api/users/{id}", s.requirePermission(auth.PermUserDelete, s.handleDeleteUser)) @@ -591,9 +596,14 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) { return } - // Enrich with live online status and status tier from memory map + // Enrich with live online status and status tier from memory map. + // Issue #138 hardening: override db.Peer.Status (string) with an int + // so any RustDesk client that reaches this handler without the + // ?accessible / ?pageSize detection still receives a valid int. type peerResponse struct { *db.Peer + Status int `json:"status"` // 1=active, 0=disabled (overrides db.Peer.Status string) + StatusText string `json:"status_text"` // Original string status for admin panel LiveOnline bool `json:"live_online"` LiveStatus peer.Status `json:"live_status"` Platform string `json:"platform"` @@ -615,8 +625,15 @@ func (s *Server) handleListPeers(w http.ResponseWriter, r *http.Request) { liveStatus = peer.StatusOnline } + statusInt := 1 + if p.Disabled { + statusInt = 0 + } + result[i] = peerResponse{ Peer: p, + Status: statusInt, + StatusText: p.Status, LiveOnline: liveOnline, LiveStatus: liveStatus, Platform: p.OS, @@ -740,14 +757,23 @@ func (s *Server) handleGetPeer(w http.ResponseWriter, r *http.Request) { type singlePeerResponse struct { *db.Peer + Status int `json:"status"` // 1=active, 0=disabled (overrides db.Peer.Status string) + StatusText string `json:"status_text"` // Original string status for admin panel LiveOnline bool `json:"live_online"` LiveStatus peer.Status `json:"live_status"` Platform string `json:"platform"` CDAPConnected bool `json:"cdap_connected"` } + statusInt := 1 + if p.Disabled { + statusInt = 0 + } + writeJSON(w, http.StatusOK, singlePeerResponse{ Peer: p, + Status: statusInt, + StatusText: p.Status, LiveOnline: liveOnline, LiveStatus: liveStatus, Platform: p.OS, diff --git a/web-nodejs/middleware/i18n.js b/web-nodejs/middleware/i18n.js index 0982519f..6449e929 100644 --- a/web-nodejs/middleware/i18n.js +++ b/web-nodejs/middleware/i18n.js @@ -82,7 +82,7 @@ function i18nMiddleware(req, res, next) { // Branding - inject dynamic app name and branding data const branding = brandingService.getBranding(); res.locals.appName = branding.appName || config.appName; - res.locals.appDescription = branding.appDescription || 'RustDesk Server Management'; + res.locals.appDescription = branding.appDescription || 'BetterDesk Server Management'; res.locals.branding = branding; // Full translations object for client-side JS diff --git a/web-nodejs/package.json b/web-nodejs/package.json index bb98098c..3630eefa 100644 --- a/web-nodejs/package.json +++ b/web-nodejs/package.json @@ -1,7 +1,7 @@ { "name": "betterdesk-console", - "version": "2.3.0", - "description": "BetterDesk Console - Professional Web Management Panel for RustDesk Server", + "version": "3.0.0", + "description": "BetterDesk Console - Professional Web Management Panel for BetterDesk Server", "main": "server.js", "scripts": { "start": "node server.js", diff --git a/web-nodejs/public/css/devices.css b/web-nodejs/public/css/devices.css index 670789ad..614e033d 100644 --- a/web-nodejs/public/css/devices.css +++ b/web-nodejs/public/css/devices.css @@ -230,11 +230,17 @@ text-align: center; gap: 6px 8px; border-style: dashed; + color: var(--text-secondary); } -.chip-add:hover { +.chip-add:hover, +.group-chip.chip-add:hover, +.folder-chip.chip-add:hover { border-color: var(--accent-blue); color: var(--accent-blue); + background: var(--bg-secondary, #161b22); + box-shadow: none; + transform: none; } .chip-add .material-icons { @@ -337,39 +343,9 @@ pointer-events: auto; } -.group-chip-action { - min-width: 0; - height: 24px; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 4px; - border: 1px solid transparent; - border-radius: var(--radius-sm, 4px); - background: rgba(255, 255, 255, 0.04); - color: var(--text-secondary); - cursor: pointer; - padding: 0 7px; - font-size: var(--font-size-xs, 0.75rem); - font-family: inherit; -} - -.group-chip-action:hover { - background: var(--bg-hover); - border-color: var(--border-hover, #484f58); - color: var(--text-primary); -} - -.group-chip-action.danger:hover { - color: var(--danger, #f85149); -} - -.group-chip-action .material-icons { - font-size: 14px; -} - -.group-chip-action-label { - line-height: 1; +/* Group chip actions now use unified .chip-action class (same as folders) */ +.group-chip .chip-action.group-delete:hover { + color: var(--accent-red); } .group-chip.active .chip-count { diff --git a/web-nodejs/public/js/devices.js b/web-nodejs/public/js/devices.js index e2153982..791e07a1 100644 --- a/web-nodejs/public/js/devices.js +++ b/web-nodejs/public/js/devices.js @@ -1618,13 +1618,11 @@ ${Utils.escapeHtml(group.name)} ${group.member_count || 0} - - `; diff --git a/web-nodejs/services/betterdeskApi.js b/web-nodejs/services/betterdeskApi.js index 9cec7591..b3f739fb 100644 --- a/web-nodejs/services/betterdeskApi.js +++ b/web-nodejs/services/betterdeskApi.js @@ -437,7 +437,7 @@ function normalisePeer(peer) { banned_at: peer.banned_at || null, folder_id: peer.folder_id || null, tags, - status_tier: peer.live_status || (peer.live_online ? 'online' : 'offline'), + status_tier: peer.live_status || peer.status_text || (peer.live_online ? 'online' : 'offline'), uuid: peer.uuid || '', nat_type: peer.nat_type || 0, disabled: !!(peer.disabled || peer.soft_deleted), diff --git a/web-nodejs/services/brandingService.js b/web-nodejs/services/brandingService.js index 840e854f..01bdd6de 100644 --- a/web-nodejs/services/brandingService.js +++ b/web-nodejs/services/brandingService.js @@ -90,7 +90,7 @@ function validateBrandingUrl(value) { const DEFAULT_BRANDING = { // Brand identity appName: 'BetterDesk', - appDescription: 'RustDesk Server Management', + appDescription: 'BetterDesk Server Management', // Logo configuration logoType: 'image', // 'icon' | 'svg' | 'image' | 'text' diff --git a/web-nodejs/themes/default.json b/web-nodejs/themes/default.json index 5a189c2c..58488c4b 100644 --- a/web-nodejs/themes/default.json +++ b/web-nodejs/themes/default.json @@ -3,7 +3,7 @@ "type": "betterdesk-theme", "branding": { "appName": "BetterDesk", - "appDescription": "RustDesk Server Management", + "appDescription": "BetterDesk Server Management", "logoType": "icon", "logoIcon": "dns", "logoSvg": "", diff --git a/web-nodejs/themes/light.json b/web-nodejs/themes/light.json index e8d43600..766eccd8 100644 --- a/web-nodejs/themes/light.json +++ b/web-nodejs/themes/light.json @@ -3,7 +3,7 @@ "type": "betterdesk-theme", "branding": { "appName": "BetterDesk", - "appDescription": "RustDesk Server Management", + "appDescription": "BetterDesk Server Management", "logoType": "icon", "logoIcon": "dns", "logoSvg": "", diff --git a/web-nodejs/views/settings.ejs b/web-nodejs/views/settings.ejs index 55266a4f..b752ba81 100644 --- a/web-nodejs/views/settings.ejs +++ b/web-nodejs/views/settings.ejs @@ -212,7 +212,7 @@
+ placeholder="BetterDesk Server Management">