diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8ec508f8..5af14815 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -422,6 +422,12 @@ sudo apt-get install -y build-essential libsqlite3-dev pkg-config libssl-dev git 69. [x] **Users page 401 error (Issue #42)**: Route conflict in `rustdesk-api.routes.js`: `GET /api/users` handler for RustDesk desktop client (Bearer token auth) was intercepting panel requests (session cookie auth), returning 401. Fixed by detecting absent Bearer token and calling `next('route')` to allow panel routes to handle the request. 70. [x] **Peers route conflict (Issue #42)**: Same fix applied to `GET /api/peers` — fallthrough to panel routes when no Bearer token present. +#### ALL-IN-ONE Scripts — Database Config Preservation (Phase 11) ✅ COMPLETED 2026-03-13 +71. [x] **PostgreSQL→SQLite switch on UPDATE**: `betterdesk.sh` and `betterdesk.ps1` were overwriting `.env` with default SQLite config during UPDATE/REPAIR, losing PostgreSQL DSN. Added `preserve_database_config()` / `Preserve-DatabaseConfig` functions that read existing `.env` before reinstall. +72. [x] **betterdesk.sh fix**: Added `preserve_database_config()` after `detect_installation()` in `do_update()` and `do_repair()`. Reads `DB_TYPE` and `DATABASE_URL` from existing `.env`, sets `USE_POSTGRESQL` and `POSTGRESQL_URI` global vars. +73. [x] **betterdesk.ps1 fix**: Added `Preserve-DatabaseConfig` PowerShell function with same logic. Called in `Do-Update` and `Do-Repair` before any reinstallation. +74. [x] **Root cause**: `install_nodejs_console()` always created new `.env` based on `USE_POSTGRESQL` var which defaults to `false`. During UPDATE, this var was never set from existing config. + --- ## 🔄 System Statusu v3.0 @@ -583,6 +589,10 @@ Pełna dokumentacja budowania: [BUILD_GUIDE.md](../docs/BUILD_GUIDE.md) 16. ~~**GetPeer missing live status**~~ ✅ ROZWIĄZANE - `handleGetPeer` now returns `live_online` + `live_status` from memory map — Phase 8 17. ~~**Hostname/Platform columns empty (Issue #37)**~~ ✅ ROZWIĄZANE - Go server was missing `/api/heartbeat`, `/api/sysinfo`, `/api/sysinfo_ver` endpoints. RustDesk client sends hostname/os/version via HTTP API to signal_port-2 (21114), but Go server had no handlers. Added all 3 endpoints + `UpdatePeerSysinfo` DB method — Phase 9 18. ~~**Users page 401 error (Issue #42)**~~ ✅ ROZWIĄZANE - Route conflict in `rustdesk-api.routes.js`: `/api/users` and `/api/peers` handlers were blocking panel requests (expecting Bearer token). Fixed by adding `next('route')` fallthrough when no Bearer token present, allowing session-based panel requests to reach `users.routes.js` — Phase 10 +19. ~~**PostgreSQL→SQLite switch on UPDATE**~~ ✅ ROZWIĄZANE - `betterdesk.sh` and `betterdesk.ps1` were overwriting `.env` with default SQLite config during UPDATE/REPAIR. Added `preserve_database_config()` function to read existing DB config before reinstalling console — Phase 11 +20. ~~**Folders not working with PostgreSQL (Issue #48)**~~ ✅ ROZWIĄZANE - `folders.routes.js` and `users.routes.js` used SQLite-specific `result.lastInsertRowid` instead of `result.id`. Fixed for PostgreSQL compatibility — Phase 12 +21. ~~**TOTP column missing on upgrade (Issue #38)**~~ ✅ ROZWIĄZANE - Added automatic migration of `totp_secret`, `totp_enabled`, `totp_recovery_codes` columns to existing `users` table for both SQLite and PostgreSQL — Phase 12 +22. ~~**SELinux volume mount issues (Issue #31)**~~ ✅ ROZWIĄZANE - Added SELinux documentation to DOCKER_TROUBLESHOOTING.md with 4 solutions (named volumes, `:z` flag, chcon, setenforce) — Phase 12 --- @@ -672,4 +682,4 @@ All code changes MUST include a security review as part of the implementation pr --- -*Ostatnia aktualizacja: 2026-03-08 (Users page 401 fix — Phase 10) przez GitHub Copilot* +*Ostatnia aktualizacja: 2026-03-13 (PostgreSQL config preservation fix — Phase 11) przez GitHub Copilot* diff --git a/README.md b/README.md index 3c68ed7d..ac498c63 100644 --- a/README.md +++ b/README.md @@ -1038,6 +1038,7 @@ ws.onmessage = (event) => { The migration tool (`betterdesk-server/tools/migrate/`) supports multiple migration paths: +**Linux/macOS:** ```bash # Compile migration tool cd betterdesk-server/tools/migrate @@ -1059,6 +1060,19 @@ go build -o migrate . ./migrate -mode backup -src /opt/betterdesk/db_v2.sqlite3 ``` +**Windows (PowerShell):** +```powershell +# Compile migration tool (requires Go installed) +cd betterdesk-server\tools\migrate +go build -o migrate.exe . + +# Usage (same modes as Linux) +.\migrate.exe -mode rust2go -src C:\BetterDesk\db_v2.sqlite3 -dst C:\BetterDesk\db_v2_new.sqlite3 +.\migrate.exe -mode sqlite2pg -src C:\BetterDesk\db_v2.sqlite3 -dst "postgres://user:pass@localhost:5432/betterdesk" +``` + +> **Note:** Windows users need [Go](https://go.dev/dl/) installed to compile the migration tool. Pre-built binaries are available in [GitHub Releases](https://github.com/UNITRONIX/BetterDesk/releases) (when available). + The migration tool auto-detects the source schema (original RustDesk `peer` table vs BetterDesk `peers` table) and maps columns accordingly. Ed25519 keys, UUIDs, ID history, bans, and tags are fully preserved. ### Using ALL-IN-ONE Scripts diff --git a/betterdesk-server/relay/server.go b/betterdesk-server/relay/server.go index 60640f79..88fa5fd5 100644 --- a/betterdesk-server/relay/server.go +++ b/betterdesk-server/relay/server.go @@ -150,6 +150,7 @@ func (s *Server) handleConn(conn net.Conn) { // Read the relay request directly — no KeyExchange for relay msg, err := codec.ReadRawProto(conn, config.RelayPairTimeout) if err != nil { + log.Printf("[relay] ReadRawProto failed from %s: %v", conn.RemoteAddr(), err) conn.Close() return } @@ -163,7 +164,9 @@ func (s *Server) handleConn(conn net.Conn) { Hc: &pb.HealthCheck{Token: hc.Token}, }, } - codec.WriteRawProto(conn, resp) + if err := codec.WriteRawProto(conn, resp); err != nil { + log.Printf("[relay] Health check response failed to %s: %v", conn.RemoteAddr(), err) + } } conn.Close() return @@ -171,6 +174,7 @@ func (s *Server) handleConn(conn net.Conn) { uuid := rr.Uuid if uuid == "" { + log.Printf("[relay] Empty UUID in RequestRelay from %s (rejecting)", conn.RemoteAddr()) conn.Close() return } diff --git a/betterdesk-server/relay/ws.go b/betterdesk-server/relay/ws.go index dd58cd08..3aa15ed9 100644 --- a/betterdesk-server/relay/ws.go +++ b/betterdesk-server/relay/ws.go @@ -83,6 +83,7 @@ func (s *Server) handleWSRelayUpgrade(w http.ResponseWriter, r *http.Request) { // Read the first message — must be RequestRelay or HealthCheck msg, err := wsc.ReadMessage() if err != nil { + log.Printf("[relay] WS ReadMessage failed from %s: %v", r.RemoteAddr, err) wsc.Close() return } @@ -94,13 +95,16 @@ func (s *Server) handleWSRelayUpgrade(w http.ResponseWriter, r *http.Request) { Hc: &pb.HealthCheck{Token: hc.Token}, }, } - wsc.WriteMessage(resp) + if err := wsc.WriteMessage(resp); err != nil { + log.Printf("[relay] WS health check response failed to %s: %v", r.RemoteAddr, err) + } wsc.Close() return } rr := msg.GetRequestRelay() if rr == nil || rr.Uuid == "" { + log.Printf("[relay] WS missing or empty UUID from %s (rejecting)", r.RemoteAddr) wsc.Close() return } diff --git a/betterdesk.ps1 b/betterdesk.ps1 index 27ba8389..1d9a0ad5 100644 --- a/betterdesk.ps1 +++ b/betterdesk.ps1 @@ -340,6 +340,33 @@ function Detect-Installation { } } +# Preserve database configuration from existing .env file +# This MUST be called before Install-NodeJsConsole during UPDATE/REPAIR +# to prevent switching from PostgreSQL to SQLite +function Preserve-DatabaseConfig { + $envFile = Join-Path $script:CONSOLE_PATH ".env" + + if (Test-Path $envFile) { + # Read existing DB_TYPE + $dbTypeLine = Select-String -Path $envFile -Pattern '^DB_TYPE=' -SimpleMatch | Select-Object -First 1 + $existingDbType = if ($dbTypeLine) { ($dbTypeLine.Line -split '=', 2)[1].Trim() } else { "" } + + # Read existing DATABASE_URL + $dbUrlLine = Select-String -Path $envFile -Pattern '^DATABASE_URL=' -SimpleMatch | Select-Object -First 1 + $existingDbUrl = if ($dbUrlLine) { ($dbUrlLine.Line -split '=', 2)[1].Trim() } else { "" } + + if ($existingDbType -eq "postgres" -and $existingDbUrl) { + $script:USE_POSTGRESQL = $true + $script:POSTGRESQL_URI = $existingDbUrl + Print-Info "Preserving PostgreSQL configuration from existing .env" + } elseif ($existingDbType -eq "sqlite") { + $script:USE_POSTGRESQL = $false + $script:POSTGRESQL_URI = "" + Print-Info "Preserving SQLite configuration from existing .env" + } + } +} + function Auto-DetectPaths { $found = $false @@ -2097,6 +2124,10 @@ function Do-Update { return } + # CRITICAL: Preserve database configuration before reinstalling console + # This prevents PostgreSQL → SQLite switch during updates + Preserve-DatabaseConfig + Print-Info "Creating backup before update..." Do-BackupSilent @@ -2128,6 +2159,11 @@ function Do-Repair { Write-Host "" Detect-Installation + + # CRITICAL: Preserve database configuration before any repair operation + # This prevents PostgreSQL → SQLite switch when regenerating service files + Preserve-DatabaseConfig + Print-Status Write-Host "" diff --git a/betterdesk.sh b/betterdesk.sh index a58d9426..cf610019 100644 --- a/betterdesk.sh +++ b/betterdesk.sh @@ -530,6 +530,27 @@ detect_installation() { fi } +# Preserve database configuration from existing .env file +# This MUST be called before install_nodejs_console() during UPDATE/REPAIR +# to prevent switching from PostgreSQL to SQLite +preserve_database_config() { + if [ -f "$CONSOLE_PATH/.env" ]; then + local existing_db_type existing_db_url + existing_db_type=$(grep -m1 '^DB_TYPE=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2 | tr -d '[:space:]') + existing_db_url=$(grep -m1 '^DATABASE_URL=' "$CONSOLE_PATH/.env" 2>/dev/null | cut -d= -f2-) + + if [ "$existing_db_type" = "postgres" ] && [ -n "$existing_db_url" ]; then + USE_POSTGRESQL="true" + POSTGRESQL_URI="$existing_db_url" + print_info "Preserving PostgreSQL configuration from existing .env" + elif [ "$existing_db_type" = "sqlite" ]; then + USE_POSTGRESQL="false" + POSTGRESQL_URI="" + print_info "Preserving SQLite configuration from existing .env" + fi + fi +} + detect_architecture() { ARCH=$(uname -m) case "$ARCH" in @@ -2004,6 +2025,10 @@ do_update() { return fi + # CRITICAL: Preserve database configuration before reinstalling console + # This prevents PostgreSQL → SQLite switch during updates + preserve_database_config + print_info "Creating backup before update..." do_backup_silent @@ -2038,6 +2063,11 @@ do_repair() { echo "" detect_installation + + # CRITICAL: Preserve database configuration before any repair operation + # This prevents PostgreSQL → SQLite switch when regenerating service files + preserve_database_config + print_status echo "" diff --git a/docs/DOCKER_TROUBLESHOOTING.md b/docs/DOCKER_TROUBLESHOOTING.md index a4aef09f..6a4beec6 100644 --- a/docs/DOCKER_TROUBLESHOOTING.md +++ b/docs/DOCKER_TROUBLESHOOTING.md @@ -196,6 +196,65 @@ docker compose up -d --- +## Problem: Volume Mount Permission Denied (SELinux — AlmaLinux/RHEL/CentOS) + +### Symptom +``` +Error: EACCES: permission denied, open '/opt/rustdesk/db_v2.sqlite3' +Error: cannot open database file +``` + +Or containers fail to start with permission errors when using bind mounts. + +### Cause +SELinux-enabled systems (AlmaLinux, RHEL, CentOS, Rocky Linux) require special volume mount options or SELinux context changes for bind mounts. + +### ✅ Solutions + +**Option 1: Use Named Volumes (recommended)** + +The default docker-compose.yml uses named volumes which work correctly with SELinux: +```yaml +volumes: + - rustdesk-data:/opt/rustdesk # Named volume - SELinux compatible + - console-data:/app/data # Named volume - SELinux compatible +``` + +**Option 2: Add `:z` flag for Bind Mounts** + +If you must use bind mounts (host paths), add the `:z` suffix: +```yaml +volumes: + - /opt/betterdesk:/opt/rustdesk:z # :z makes it SELinux-compatible + - /opt/console-data:/app/data:z +``` + +**Option 3: Apply SELinux Context Manually** +```bash +# Apply container-compatible SELinux context to directories +sudo chcon -Rt svirt_sandbox_file_t /path/to/data/directory + +# Example for BetterDesk +sudo chcon -Rt svirt_sandbox_file_t /opt/betterdesk +sudo chcon -Rt svirt_sandbox_file_t /opt/console-data +``` + +**Option 4: Temporarily Disable SELinux (not recommended for production)** +```bash +# Set SELinux to permissive mode temporarily +sudo setenforce 0 + +# Start containers +docker compose up -d + +# Re-enable SELinux +sudo setenforce 1 +``` + +> **Note:** The `betterdesk-docker.sh` script automatically handles SELinux contexts for RHEL-based systems. + +--- + ## Problem: Missing Admin Login Credentials If you started BetterDesk Console using Docker Compose following "Option 2" and don't see admin login credentials in the logs, it means the **database migration was not automatically executed**. diff --git a/web-nodejs/routes/folders.routes.js b/web-nodejs/routes/folders.routes.js index e4066e01..a41a2c36 100644 --- a/web-nodejs/routes/folders.routes.js +++ b/web-nodejs/routes/folders.routes.js @@ -75,7 +75,7 @@ router.post('/api/folders', requireAuth, async (req, res) => { res.json({ success: true, data: { - id: result.lastInsertRowid, + id: result.id, name: name.trim(), color: color || '#6366f1', icon: icon || 'folder' diff --git a/web-nodejs/routes/users.routes.js b/web-nodejs/routes/users.routes.js index a85a139b..0ed355f8 100644 --- a/web-nodejs/routes/users.routes.js +++ b/web-nodejs/routes/users.routes.js @@ -110,7 +110,7 @@ router.post('/api/users', requireAuth, requireAdmin, passwordChangeLimiter, asyn res.json({ success: true, data: { - id: result.lastInsertRowid, + id: result.id, username, role: userRole } diff --git a/web-nodejs/services/dbAdapter.js b/web-nodejs/services/dbAdapter.js index 97c11a92..2bb8dd8f 100644 --- a/web-nodejs/services/dbAdapter.js +++ b/web-nodejs/services/dbAdapter.js @@ -458,6 +458,19 @@ function createSqliteAdapter(config) { UNIQUE(device_id) ); `); + + // Migration: Add TOTP columns to existing users table (for upgrades from older versions) + const userCols = [ + { name: 'totp_secret', sql: 'TEXT DEFAULT NULL' }, + { name: 'totp_enabled', sql: 'INTEGER DEFAULT 0' }, + { name: 'totp_recovery_codes', sql: 'TEXT DEFAULT NULL' }, + ]; + const existingUserCols = new Set(db.prepare('PRAGMA table_info(users)').all().map(c => c.name)); + for (const c of userCols) { + if (!existingUserCols.has(c.name)) { + try { db.exec(`ALTER TABLE users ADD COLUMN ${c.name} ${c.sql}`); } catch (_) {} + } + } } function ensureActivityTables(db) { @@ -2964,6 +2977,19 @@ function createPostgresAdapter() { const crypto = require('crypto'); await q('INSERT INTO device_groups (guid, name, note) VALUES ($1, $2, $3)', [crypto.randomUUID(), 'Default', 'Default device group']); } + + // Migration: Add TOTP columns to existing users table (for upgrades from older versions) + const columnCheck = await all(`SELECT column_name FROM information_schema.columns WHERE table_name = 'users'`); + const existingCols = new Set(columnCheck.map(c => c.column_name)); + if (!existingCols.has('totp_secret')) { + await q('ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_secret TEXT DEFAULT NULL'); + } + if (!existingCols.has('totp_enabled')) { + await q('ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_enabled BOOLEAN DEFAULT FALSE'); + } + if (!existingCols.has('totp_recovery_codes')) { + await q('ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_recovery_codes TEXT DEFAULT NULL'); + } } // Parse helpers