mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
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:
@@ -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*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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**.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user