Files
BetterDesk/web-nodejs/server.js
T
UNITRONIX 452c76069c fix(users): mirror Node panel users to Go server for org linking (#125)
Panel accounts were created only in the Node auth.db while the
Organization 'Add User -> Add Existing' dropdown queries the Go
server's users table via db.ListUsersNotInOrg(). This produced a
mismatch where only the seeded admin appeared as linkable.

Add a userSync service that mirrors create / update / delete /
password-reset operations from the Node panel to the Go server's
/api/users endpoints, plus a one-shot backfill at startup that
creates Go-side records for any pre-existing panel users (random
throwaway password; Node bcrypt remains authoritative for panel
login).

Reported-by: SterlynKong <SterlynKong@users.noreply.github.com>
2026-05-03 01:48:08 +02:00

715 lines
30 KiB
JavaScript

/**
* BetterDesk Console - Server Entry Point
* Professional Web Management Panel for RustDesk Server
*
* @author UNITRONIX
* @version 2.1.0
* @license Apache-2.0
*/
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');
const http = require('http');
const https = require('https');
const config = require('./config/config');
const securityMiddleware = require('./middleware/security');
const { initI18n } = require('./middleware/i18n');
const { apiLimiter } = require('./middleware/rateLimiter');
const { csrfTokenProvider, doubleCsrfProtection, downgradeToHttp: csrfDowngradeToHttp } = require('./middleware/csrf');
const { roleHasPermission, isSuperAdminRole } = require('./middleware/auth');
const authService = require('./services/authService');
const serverBackend = require('./services/serverBackend');
const db = require('./services/database');
const { initWsProxy } = require('./services/wsRelay');
const { initBdRelay } = require('./services/bdRelay');
const { initChatRelay } = require('./services/chatRelay');
const { apiClient: goApiClient } = require('./services/betterdeskApi');
const { initRemoteRelay } = require('./services/remoteRelay');
const { initCdapTerminalProxy } = require('./services/cdapTerminalProxy');
const { initCdapMediaProxies } = require('./services/cdapMediaProxy');
const { startDiscoveryService } = require('./services/lanDiscovery');
const { initDeviceStatusPush } = require('./services/deviceStatusPush');
const routes = require('./routes');
const rustdeskApiRoutes = require('./routes/rustdesk-api.routes');
const bdApiRoutes = require('./routes/bd-api.routes');
const { getWanMiddlewareStack } = require('./middleware/wanSecurity');
// Create Express app
const app = express();
// Trust proxy (for rate limiting behind reverse proxy)
// Configurable via TRUST_PROXY env var: 0=disabled, 1=single proxy, 'loopback'=localhost only
// Default: false (safest). Set TRUST_PROXY=1 when behind nginx/Apache/cloudflare
const trustProxy = process.env.TRUST_PROXY !== undefined ?
(isNaN(process.env.TRUST_PROXY) ? process.env.TRUST_PROXY : parseInt(process.env.TRUST_PROXY, 10)) : false;
app.set('trust proxy', trustProxy);
// View engine setup
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Ensure data directory exists
if (!fs.existsSync(config.dataDir)) {
fs.mkdirSync(config.dataDir, { recursive: true });
}
// ============ Middleware Pipeline ============
// Security headers (Helmet)
app.use(securityMiddleware);
// CORS for BetterDesk desktop clients (Tauri webview origins)
app.use('/api/', (req, res, next) => {
const origin = req.headers.origin || '';
const allowed = [
'http://localhost:1420', // Tauri dev
'tauri://localhost', // Tauri production (macOS/Linux)
'https://tauri.localhost', // Tauri production (Windows)
];
if (allowed.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-CSRF-Token');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
}
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
// Body parsing (2MB limit for base64 logo images)
app.use(express.json({ limit: '2mb' }));
app.use(express.urlencoded({ extended: false, limit: '2mb' }));
// Cookie parsing
app.use(cookieParser());
// Session management — also kept as a standalone middleware ref for WebSocket upgrades
// Use a different cookie name in HTTP mode to avoid collision with stale
// Secure cookies left over from a previous HTTPS configuration (Issue #82).
const SESSION_COOKIE = config.httpsEnabled ? 'betterdesk.sid' : 'bd.sid';
const sessionMiddleware = session({
secret: config.sessionSecret,
name: SESSION_COOKIE,
resave: false,
saveUninitialized: false,
cookie: {
secure: config.httpsEnabled,
httpOnly: true,
sameSite: 'lax',
maxAge: config.sessionMaxAge
}
});
app.use(sessionMiddleware);
// Cache version — changes on every restart/deployment, stable during runtime.
// Used in ?v= query strings so browsers cache assets per deployment.
app.locals.cacheVersion = config.appVersion + '.' + Date.now();
// Static files
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: config.isProduction ? '7d' : '0',
etag: true
}));
// Serve proto files for remote client (protobufjs dynamic loading)
app.use('/protos', express.static(path.join(__dirname, 'protos'), {
maxAge: config.isProduction ? '7d' : '0',
etag: true
}));
// Serve uploaded branding assets (logos etc.) from persistent data dir
const uploadsDir = path.join(config.dataDir || path.join(__dirname, 'data'), 'uploads');
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
app.use('/uploads', express.static(uploadsDir, {
maxAge: config.isProduction ? '30d' : '0',
etag: true
}));
// Serve desktop wallpapers
app.use('/wallpapers', express.static(path.join(__dirname, 'wallpapers'), {
maxAge: config.isProduction ? '30d' : '0',
etag: true,
immutable: true
}));
// Rate limiting for API
app.use('/api/', apiLimiter);
// RustDesk Client API — mounted BEFORE CSRF because desktop clients use Bearer
// token auth, not cookie-based CSRF. These routes are also served on the
// dedicated WAN-facing port (21121) with additional hardening.
app.use(rustdeskApiRoutes);
// BetterDesk Desktop Client API — device-facing endpoints that use
// Bearer token or X-Device-Id header, not browser CSRF cookies.
app.use('/api/bd', bdApiRoutes);
// i18n middleware
app.use(initI18n());
// Embed mode — when ?embed=1 is present, layout renders without sidebar/navbar
// Used by Desktop Mode to load pages inside floating windows (iframes)
app.use((req, res, next) => {
res.locals.embed = req.query.embed === '1';
// Inject permission helper for EJS templates (sidebar/button visibility)
const role = req.session?.user?.role;
res.locals.hasPermission = (perm) => role ? roleHasPermission(role, perm) : false;
res.locals.isSuperAdmin = role ? isSuperAdminRole(role) : false;
// Prevent HTML page caching — only static assets should be cached
if (!req.path.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|map|proto)$/)) {
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
}
next();
});
// CSRF protection — generate token for views, validate on POST/PUT/DELETE/PATCH
// Skip CSRF for device-facing API routes (/api/bd/*) — these use Bearer token
// or X-Device-Id header authentication, not browser cookie-based CSRF.
app.use(csrfTokenProvider);
app.use((req, res, next) => {
if (req.path.startsWith('/api/bd/')) {
return next();
}
// Skip CSRF for BetterDesk desktop clients (Tauri) — they are not
// vulnerable to CSRF attacks (not browser tabs). Identified by origin.
const origin = req.headers.origin || '';
const tauriOrigins = ['http://localhost:1420', 'tauri://localhost', 'https://tauri.localhost'];
if (req.path.startsWith('/api/') && tauriOrigins.includes(origin)) {
return next();
}
doubleCsrfProtection(req, res, next);
});
// ============ Routes ============
app.use('/', routes);
// ============ Error Handlers ============
// CSRF token mismatch
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN' || err.message?.includes('csrf') || err.message?.includes('CSRF')) {
res.status(403);
// Detect likely SSL→HTTP transition: cookie missing because browser held Secure cookie
const likelySslTransition = !config.httpsEnabled && !req.secure;
const hint = likelySslTransition
? ' If you recently disabled SSL, clear your browser cookies for this site and reload.'
: '';
// Always return JSON for API routes (fetch sends Accept: */*)
if (req.path.startsWith('/api/') || (req.headers['content-type'] && req.headers['content-type'].includes('application/json'))) {
return res.json({ success: false, error: 'Invalid CSRF token. Please refresh the page and try again.' + hint });
}
if (req.accepts('html')) {
return res.render('errors/500', {
title: 'Forbidden',
activePage: 'error',
error: 'Invalid or missing CSRF token. Please refresh the page and try again.' + hint
});
}
return res.json({ success: false, error: 'Invalid CSRF token' + hint });
}
next(err);
});
// 404 Not Found
app.use((req, res, next) => {
res.status(404);
// Log unmatched /api/* and /ws/* paths only (avoid noise from missing
// static assets like favicons). Diagnostics suggestion credit:
// progloto (PR #81).
if (req.originalUrl.startsWith('/api/') || req.originalUrl.startsWith('/ws/')) {
const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
const ua = String(req.headers['user-agent'] || '').slice(0, 80);
console.warn(`[panel] 404 ${req.method} ${req.originalUrl} from ${ip} ua="${ua}"`);
}
if (req.accepts('html')) {
res.render('errors/404', {
title: req.t ? req.t('errors.not_found') : 'Not Found',
activePage: 'error'
});
} else {
res.json({
success: false,
error: 'Not Found'
});
}
});
// 500 Server Error
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(err.status || 500);
// Always return JSON for API routes
if (req.path.startsWith('/api/') || (req.headers['content-type'] && req.headers['content-type'].includes('application/json'))) {
return res.json({
success: false,
error: config.isProduction ? 'Internal Server Error' : err.message
});
}
if (req.accepts('html')) {
res.render('errors/500', {
title: req.t ? req.t('errors.server_error') : 'Server Error',
activePage: 'error',
error: config.isProduction ? null : err.message
});
} else {
res.json({
success: false,
error: config.isProduction ? 'Internal Server Error' : err.message
});
}
});
// ============ Startup ============
/**
* Warn if the user set Go-server-only TLS env vars in the Node.js environment.
* These variables (TLS_CERT, TLS_KEY) are read exclusively by the Go server.
* The Node.js console uses SSL_CERT_PATH / SSL_KEY_PATH instead.
* Silently ignoring them causes issue #104 — port 21121 stays HTTP while the
* RustDesk client expects HTTPS, producing InvalidContentType errors.
*/
function warnGoTlsEnvVars() {
const hasTlsCert = !!process.env.TLS_CERT;
const hasTlsKey = !!process.env.TLS_KEY;
if (!hasTlsCert && !hasTlsKey) return;
const hasSslCertPath = !!process.env.SSL_CERT_PATH;
const hasSslKeyPath = !!process.env.SSL_KEY_PATH;
if (hasTlsCert || hasTlsKey) {
console.warn('');
console.warn(' ┌─────────────────────────────────────────────────────┐');
console.warn(' │ ⚠ MISCONFIGURATION WARNING — TLS / SSL │');
console.warn(' ├─────────────────────────────────────────────────────┤');
console.warn(' │ TLS_CERT / TLS_KEY are Go server environment │');
console.warn(' │ variables and are IGNORED by this Node.js console. │');
console.warn(' │ │');
console.warn(' │ To enable HTTPS on this console set: │');
console.warn(' │ SSL_CERT_PATH=/path/to/fullchain.pem │');
console.warn(' │ SSL_KEY_PATH=/path/to/privkey.pem │');
console.warn(' │ │');
if (!hasSslCertPath && !hasSslKeyPath) {
console.warn(' │ ❌ SSL_CERT_PATH and SSL_KEY_PATH are NOT set. │');
console.warn(' │ Port 21121 (RustDesk Client API) is HTTP. │');
console.warn(' │ Clients connecting via HTTPS will fail with │');
console.warn(' │ InvalidContentType errors. │');
} else {
console.warn(' │ ✅ SSL_CERT_PATH / SSL_KEY_PATH are set — OK. │');
}
console.warn(' └─────────────────────────────────────────────────────┘');
console.warn('');
}
}
/**
* Load SSL certificates for HTTPS
*/
function loadSslCertificates() {
const options = {};
if (!config.sslCertPath || !config.sslKeyPath) {
return null;
}
try {
if (!fs.existsSync(config.sslCertPath)) {
console.error(`SSL certificate not found: ${config.sslCertPath}`);
return null;
}
if (!fs.existsSync(config.sslKeyPath)) {
console.error(`SSL private key not found: ${config.sslKeyPath}`);
return null;
}
options.cert = fs.readFileSync(config.sslCertPath);
options.key = fs.readFileSync(config.sslKeyPath);
// Optional CA bundle (for Let's Encrypt chain)
if (config.sslCaPath && fs.existsSync(config.sslCaPath)) {
options.ca = fs.readFileSync(config.sslCaPath);
}
return options;
} catch (err) {
console.error('Failed to load SSL certificates:', err.message);
return null;
}
}
/**
* Create HTTP redirect server (redirects all HTTP to HTTPS)
*/
function createHttpRedirectServer() {
const redirectApp = express();
redirectApp.use((req, res) => {
const httpsUrl = `https://${req.hostname}:${config.httpsPort}${req.url}`;
res.redirect(301, httpsUrl);
});
return http.createServer(redirectApp);
}
async function startServer() {
// Warn early about common TLS misconfiguration (Go env vars used instead of Node.js vars)
warnGoTlsEnvVars();
try {
// Initialize database adapter (creates tables, runs migrations)
await db.init();
// Warm branding cache from database (must run after db.init)
const brandingService = require('./services/brandingService');
await brandingService.loadBranding();
// Ensure default admin exists
await authService.ensureDefaultAdmin();
// Mirror Node panel users into the Go users table so they can be
// linked to organizations via "Add User -> Add Existing" (Issue #125).
// Best-effort: failures are logged but never block startup.
try {
const userSync = require('./services/userSync');
await userSync.backfillFromNode();
} catch (err) {
console.warn('[startup] userSync.backfillFromNode failed:', err.message);
}
let server;
let protocol = 'http';
let displayPort = config.port;
// HTTPS mode
if (config.httpsEnabled) {
const sslOptions = loadSslCertificates();
if (sslOptions) {
// Create HTTPS server
server = https.createServer(sslOptions, app);
protocol = 'https';
displayPort = config.httpsPort;
server.listen(config.httpsPort, config.host, () => {
printStartupBanner(protocol, displayPort);
});
// Optionally start HTTP redirect server
if (config.httpRedirect) {
const redirectServer = createHttpRedirectServer();
redirectServer.listen(config.port, config.host, () => {
console.log(` HTTP -> HTTPS redirect active on port ${config.port}`);
console.log('');
});
// Graceful shutdown for redirect server too
const shutdownRedirect = () => { redirectServer.close(); };
process.on('SIGTERM', shutdownRedirect);
process.on('SIGINT', shutdownRedirect);
}
} else {
console.warn('WARNING: HTTPS enabled but certificates not found/invalid');
console.warn('Falling back to HTTP mode');
console.warn(' → Session and CSRF cookies downgraded to non-Secure');
console.warn(' → Fix: check SSL_CERT_PATH and SSL_KEY_PATH in .env');
// BD-2026-082: Downgrade cookie flags to match actual HTTP mode.
// Session middleware was initialised with secure:true at module
// load time. Without this fixup, browsers would ignore all
// cookies and every request would fail CSRF validation.
if (sessionMiddleware && sessionMiddleware.options) {
sessionMiddleware.options.cookie = sessionMiddleware.options.cookie || {};
sessionMiddleware.options.cookie.secure = false;
}
csrfDowngradeToHttp();
server = http.createServer(app);
server.listen(config.port, config.host, () => {
printStartupBanner(protocol, config.port);
});
}
} else {
// HTTP mode (default)
server = http.createServer(app);
server.listen(config.port, config.host, () => {
printStartupBanner(protocol, config.port);
});
}
// Initialize WebSocket proxy for remote desktop client
initWsProxy(server, sessionMiddleware);
// Initialize BetterDesk native relay (WebSocket)
initBdRelay(server);
// Initialize Chat relay (WebSocket — agent ↔ operator, persistent via Go API)
initChatRelay(server, sessionMiddleware, goApiClient);
// Initialize Remote Desktop relay (WebSocket — agent JPEG ↔ browser viewer)
initRemoteRelay(server, sessionMiddleware);
// Initialize CDAP Terminal WebSocket proxy (browser ↔ Go server)
initCdapTerminalProxy(server, sessionMiddleware);
// Initialize CDAP Media WebSocket proxies (desktop, video, file browser)
initCdapMediaProxies(server, sessionMiddleware);
// Initialize real-time device status push (Go event bus → browser)
initDeviceStatusPush(server, sessionMiddleware, config.betterdeskApiUrl, config.betterdeskApiKey);
// Start LAN Discovery UDP service
startDiscoveryService();
// ============ RustDesk Client API Server (dedicated port) ============
let apiServer = null;
if (config.apiEnabled) {
apiServer = startRustDeskApiServer();
}
// ============ Periodic Housekeeping ============
const housekeepingInterval = setInterval(async () => {
await authService.cleanupHousekeeping();
// Clean up old integration data (metrics >7d, audit >90d)
try {
await db.runIntegrationHousekeeping();
} catch (err) {
// Silent fail — don't crash the server for housekeeping
}
// Clean up old audit_log entries (>90 days)
try {
if (typeof db.cleanupOldAuditLogs === 'function') {
await db.cleanupOldAuditLogs(90);
}
} catch (err) {
// Silent fail
}
}, 60 * 60 * 1000); // Every hour
// ============ Periodic Online Status Sync ============
const syncInterval = parseInt(process.env.STATUS_SYNC_INTERVAL, 10) || 15; // seconds
const heartbeatStaleThreshold = parseInt(process.env.HEARTBEAT_STALE_THRESHOLD, 10) || 90; // seconds
const statusSyncInterval = setInterval(async () => {
try {
await serverBackend.syncOnlineStatus();
} catch (err) {
// Silent fail - don't crash the server
}
// Also clean up stale heartbeat-based online status
try {
if (typeof db.cleanupStaleOnlinePeers === 'function') {
await db.cleanupStaleOnlinePeers(heartbeatStaleThreshold);
}
} catch (err) {
// Silent fail
}
}, syncInterval * 1000);
// Initial sync on startup (after short delay for HBBS to be ready)
setTimeout(async () => {
try {
const result = await serverBackend.syncOnlineStatus();
if (result.synced > 0) {
console.log(`Initial status sync: ${result.synced} device(s) online`);
}
} catch (err) {
// Silent fail
}
}, 5000);
// Graceful shutdown
const shutdown = (signal) => {
console.log(`\n${signal} received. Shutting down gracefully...`);
clearInterval(housekeepingInterval);
clearInterval(statusSyncInterval);
const closePromises = [new Promise(r => server.close(r))];
if (apiServer) {
closePromises.push(new Promise(r => apiServer.close(r)));
}
Promise.all(closePromises).then(() => {
console.log('All servers closed.');
process.exit(0);
});
// Force exit after 10 seconds
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
/**
* Start the dedicated RustDesk Client API server on a separate port.
* This is a minimal, hardened Express app with only 4 endpoints.
* Designed for WAN/internet exposure with aggressive security.
*/
function startRustDeskApiServer() {
const apiApp = express();
// Trust proxy (use same configuration as main app — TRUST_PROXY env var)
apiApp.set('trust proxy', trustProxy);
// Apply WAN security middleware stack
const wanMiddleware = getWanMiddlewareStack();
for (const mw of wanMiddleware) {
apiApp.use(mw);
}
// JSON body parser with size limit (64KB for address book sync)
apiApp.use(express.json({ limit: '64kb', strict: true }));
// Mount RustDesk-compatible API routes
apiApp.use('/', rustdeskApiRoutes);
// Mount device-facing registration routes (LAN discovery pairing)
const registrationRoutes = require('./routes/registration.routes');
apiApp.use('/api/bd', registrationRoutes);
// Catch-all for any unmatched routes (should not reach here due to pathWhitelist).
// We log every miss so missing RustDesk client compatibility endpoints are
// easy to spot in operations. Diagnostics suggestion credit:
// progloto (PR #81).
apiApp.use((req, res) => {
const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
const ua = String(req.headers['user-agent'] || '').slice(0, 80);
console.warn(`[rustdesk-api] 404 ${req.method} ${req.originalUrl} from ${ip} ua="${ua}"`);
res.status(404).end();
});
// Error handler — never leak internal errors
apiApp.use((err, req, res, next) => {
if (err.type === 'entity.parse.failed') {
console.warn('RustDesk API: JSON parse error from', req.socket?.remoteAddress);
return res.status(400).json({ error: 'Invalid JSON' });
}
if (err.type === 'entity.too.large') {
return res.status(413).json({ error: 'Request too large' });
}
console.error('RustDesk API error:', err.message);
res.status(500).json({ error: 'Server error' });
});
// Start HTTP or HTTPS server for RustDesk Client API
// Port 21121 is internet-facing — always use TLS if valid certs are available,
// regardless of HTTPS_ENABLED (which controls the admin panel port).
let apiServerInstance;
const sslOptions = loadSslCertificates();
if (sslOptions) {
apiServerInstance = https.createServer(sslOptions, apiApp);
console.log(` ║ API TLS: Enabled (HTTPS on :${config.apiPort})`.padEnd(53) + '║');
} else {
if (config.sslCertPath || config.sslKeyPath) {
console.warn(`WARNING: SSL certs configured but invalid — API running insecure HTTP on :${config.apiPort}`);
}
apiServerInstance = http.createServer(apiApp);
}
apiServerInstance.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(` ║ API Port: ${config.apiPort} FAILED (port in use)`.padEnd(53) + '║');
console.error(` ║ Hint: Check if hbbs uses the same port, or`.padEnd(53) + '║');
console.error(` ║ set API_PORT env var (default: 21121)`.padEnd(53) + '║');
console.log(' ║ ║');
console.error(`WARNING: RustDesk Client API could not start on port ${config.apiPort}`);
console.error('Likely cause: hbbs API is on the same port. Client API default is 21121.');
console.error('The admin panel continues to run normally on port ' + config.port);
return; // Don't crash — let the panel continue running
}
throw err;
});
apiServerInstance.listen(config.apiPort, config.apiHost, () => {
console.log(` ║ API Port: ${config.apiPort} (RustDesk Client)`.padEnd(53) + '║');
console.log(' ║ ║');
});
// Set connection timeout (prevent slow loris)
apiServerInstance.headersTimeout = 15000;
apiServerInstance.requestTimeout = 10000;
apiServerInstance.keepAliveTimeout = 5000;
return apiServerInstance;
}
/**
* Print startup banner with server info
*/
function printStartupBanner(protocol, port) {
const sslStatus = config.httpsEnabled ? '🔒 HTTPS' : '🔓 HTTP';
// API port 21121 auto-enables TLS if valid certs exist (regardless of HTTPS_ENABLED)
const apiHasCerts = config.sslCertPath && config.sslKeyPath &&
fs.existsSync(config.sslCertPath) && fs.existsSync(config.sslKeyPath);
const apiProtocol = apiHasCerts ? 'HTTPS' : 'HTTP';
const apiStatus = config.apiEnabled ? `✅ Port ${config.apiPort} (${apiProtocol})` : '❌ Disabled';
const panelUrl = `${protocol}://${config.host}:${port}`;
console.log('');
console.log(' ╔══════════════════════════════════════════════════╗');
console.log(' ║ ║');
console.log(' ║ 🖥️ BetterDesk Console v' + config.appVersion.padEnd(23) + ' ║');
console.log(' ║ ║');
console.log(' ╠══════════════════════════════════════════════════╣');
console.log(' ║ ║');
console.log(` ║ Panel: ${panelUrl}`.padEnd(53) + '║');
if (config.httpsEnabled && config.httpRedirect) {
console.log(` ║ Redirect: http://${config.host}:${config.port} → :${config.httpsPort}`.padEnd(53) + '║');
}
console.log(` ║ Client API: ${apiStatus}`.padEnd(53) + '║');
console.log(` ║ Go API: http://localhost:21114/api`.padEnd(53) + '║');
console.log(` ║ Mode: ${config.nodeEnv}`.padEnd(53) + '║');
console.log(` ║ Security: ${sslStatus}`.padEnd(53) + '║');
const dbLabel = (db.DB_TYPE === 'postgres' || db.DB_TYPE === 'postgresql')
? `PostgreSQL (${process.env.DATABASE_URL ? new URL(process.env.DATABASE_URL).hostname : 'localhost'})`
: path.basename(config.dbPath);
console.log(` ║ Database: ${dbLabel}`.padEnd(53) + '║');
console.log(` ║ Keys: ${config.keysPath}`.padEnd(53) + '║');
console.log(' ║ ║');
console.log(' ╚══════════════════════════════════════════════════╝');
console.log('');
// BD-2026-006: Warn if panel is bound to all interfaces in non-Docker environments
if (config.host === '0.0.0.0' && !config.isDocker) {
console.log(' ⚠️ WARNING [SECURITY]: Panel bound to 0.0.0.0 (all interfaces).');
console.log(' Set HOST=127.0.0.1 in .env to restrict to localhost only.');
console.log('');
}
// BD-2026-008: Warn if plaintext credentials file exists
const credFile = path.join(config.keysPath, '.admin_credentials');
if (fs.existsSync(credFile)) {
console.log(' ⚠️ WARNING [SECURITY]: Plaintext .admin_credentials file detected.');
console.log(' Delete it after noting the password: ' + credFile);
console.log('');
}
// BD-2026-009: Warn when proxy trust is enabled
if (trustProxy && trustProxy !== false && trustProxy !== 0) {
console.log(' ⚠️ NOTICE [SECURITY]: TRUST_PROXY is enabled (' + trustProxy + ').');
console.log(' Ensure a trusted reverse proxy sets X-Forwarded-For correctly.');
console.log('');
}
}
// Start the server
startServer();
module.exports = app;