Preserve DB config, PG compatibility & logging

Preserve existing DB config during update/repair and add PostgreSQL compatibility and reliability fixes. Added preserve_database_config()/Preserve-DatabaseConfig and invoked them before console reinstall in betterdesk.sh and betterdesk.ps1 to avoid unintentionally switching PostgreSQL → SQLite. Fixed folder/user route responses to use result.id (Postgres-compatible) in web-nodejs routes. Added automatic TOTP column migrations for both SQLite and Postgres in web-nodejs/services/dbAdapter.js. Improved relay error logging and write-error handling in betterdesk-server/relay (server.go, ws.go). Updated docs and tooling: added SELinux troubleshooting (DOCKER_TROUBLESHOOTING.md), Windows build/usage notes for the migrate tool (README.md), and updated changelog/instructions (.github/copilot-instructions.md) and last-updated date.
This commit is contained in:
UNITRONIX
2026-03-13 23:14:06 +01:00
parent 447c22af48
commit db46242023
10 changed files with 188 additions and 5 deletions
+11 -1
View File
@@ -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*
+14
View File
@@ -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
+5 -1
View File
@@ -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
}
+5 -1
View File
@@ -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
}
+36
View File
@@ -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 ""
+30
View File
@@ -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 ""
+59
View File
@@ -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**.
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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
}
+26
View File
@@ -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