2025-09-12 14:42:00 -05:00
import { drizzle } from "drizzle-orm/better-sqlite3" ;
import Database from "better-sqlite3" ;
import * as schema from "./schema.js" ;
import fs from "fs" ;
import path from "path" ;
import { databaseLogger } from "../../utils/logger.js" ;
2025-10-01 15:40:10 -05:00
import { DatabaseFileEncryption } from "../../utils/database-file-encryption.js" ;
import { SystemCrypto } from "../../utils/system-crypto.js" ;
import { DatabaseMigration } from "../../utils/database-migration.js" ;
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js" ;
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
const dataDir = process . env . DATA_DIR || "./db/data" ;
2025-08-07 02:20:27 -05:00
const dbDir = path . resolve ( dataDir );
if ( ! fs . existsSync ( dbDir )) {
2025-09-12 14:42:00 -05:00
fs . mkdirSync ( dbDir , { recursive : true });
2025-08-07 02:20:27 -05:00
}
2025-10-01 15:40:10 -05:00
const enableFileEncryption = process . env . DB_FILE_ENCRYPTION !== "false" ;
2025-09-12 14:42:00 -05:00
const dbPath = path . join ( dataDir , "db.sqlite" );
2025-10-01 15:40:10 -05:00
const encryptedDbPath = ` ${ dbPath } .encrypted` ;
2025-08-07 02:20:27 -05:00
2025-11-05 10:36:16 -06:00
const actualDbPath = ":memory:" ;
2025-10-01 15:40:10 -05:00
let memoryDatabase : Database.Database ;
let isNewDatabase = false ;
let sqlite : Database.Database ;
async function initializeDatabaseAsync () : Promise < void > {
const systemCrypto = SystemCrypto . getInstance ();
2025-11-05 10:36:16 -06:00
await systemCrypto . getDatabaseKey ();
2025-10-01 15:40:10 -05:00
if ( enableFileEncryption ) {
try {
if ( DatabaseFileEncryption . isEncryptedDatabaseFile ( encryptedDbPath )) {
const decryptedBuffer =
await DatabaseFileEncryption . decryptDatabaseToBuffer ( encryptedDbPath );
memoryDatabase = new Database ( decryptedBuffer );
2025-11-05 10:36:16 -06:00
try {
2026-03-08 18:02:14 -05:00
memoryDatabase
2025-11-05 10:36:16 -06:00
. prepare ( "SELECT COUNT(*) as count FROM sessions" )
. get () as { count : number };
2026-03-08 18:02:14 -05:00
} catch {
// expected - sessions table may not exist yet
2025-11-05 10:36:16 -06:00
}
2025-10-01 15:40:10 -05:00
} else {
const migration = new DatabaseMigration ( dataDir );
const migrationStatus = migration . checkMigrationStatus ();
if ( migrationStatus . needsMigration ) {
const migrationResult = await migration . migrateDatabase ();
if ( migrationResult . success ) {
migration . cleanupOldBackups ();
if (
DatabaseFileEncryption . isEncryptedDatabaseFile ( encryptedDbPath )
) {
const decryptedBuffer =
await DatabaseFileEncryption . decryptDatabaseToBuffer (
encryptedDbPath ,
);
memoryDatabase = new Database ( decryptedBuffer );
isNewDatabase = false ;
} else {
throw new Error (
"Migration completed but encrypted database file not found" ,
);
}
} else {
databaseLogger . error ( "Automatic database migration failed" , null , {
operation : "auto_migration_failed" ,
error : migrationResult.error ,
migratedTables : migrationResult.migratedTables ,
migratedRows : migrationResult.migratedRows ,
duration : migrationResult.duration ,
backupPath : migrationResult.backupPath ,
});
throw new Error (
`Database migration failed: ${ migrationResult . error } . Backup available at: ${ migrationResult . backupPath } ` ,
);
}
} else {
memoryDatabase = new Database ( ":memory:" );
isNewDatabase = true ;
}
}
} catch ( error ) {
databaseLogger . error ( "Failed to initialize memory database" , error , {
operation : "db_memory_init_failed" ,
errorMessage : error instanceof Error ? error . message : "Unknown error" ,
errorStack : error instanceof Error ? error.stack : undefined ,
encryptedDbExists :
DatabaseFileEncryption.isEncryptedDatabaseFile ( encryptedDbPath ),
databaseKeyAvailable : !! process . env . DATABASE_KEY ,
databaseKeyLength : process.env.DATABASE_KEY?.length || 0 ,
});
2025-11-17 09:46:05 -06:00
try {
const diagnosticInfo =
DatabaseFileEncryption . getDiagnosticInfo ( encryptedDbPath );
databaseLogger . error (
"Database encryption diagnostic completed - check logs above for details" ,
null ,
{
operation : "db_encryption_diagnostic_completed" ,
filesConsistent : diagnosticInfo.validation.filesConsistent ,
sizeMismatch : diagnosticInfo.validation.sizeMismatch ,
},
);
} catch ( diagError ) {
databaseLogger . warn ( "Failed to generate diagnostic information" , {
operation : "db_diagnostic_failed" ,
error :
diagError instanceof Error ? diagError . message : "Unknown error" ,
});
}
2025-10-01 15:40:10 -05:00
throw new Error (
`Database decryption failed: ${ error instanceof Error ? error . message : "Unknown error" } . This prevents data loss.` ,
2026-05-06 15:12:07 -05:00
{ cause : error },
2025-10-01 15:40:10 -05:00
);
}
} else {
memoryDatabase = new Database ( ":memory:" );
isNewDatabase = true ;
}
}
async function initializeCompleteDatabase () : Promise < void > {
await initializeDatabaseAsync ();
databaseLogger . info ( `Initializing SQLite database` , {
operation : "db_init" ,
path : actualDbPath ,
encrypted :
enableFileEncryption &&
DatabaseFileEncryption . isEncryptedDatabaseFile ( encryptedDbPath ),
inMemory : true ,
isNewDatabase ,
});
sqlite = memoryDatabase ;
2025-11-17 09:46:05 -06:00
sqlite . exec ( "PRAGMA foreign_keys = ON" );
2025-10-01 15:40:10 -05:00
db = drizzle ( sqlite , { schema });
sqlite . exec ( `
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
is_oidc INTEGER NOT NULL DEFAULT 0,
2025-10-01 15:40:10 -05:00
oidc_identifier TEXT,
client_id TEXT,
client_secret TEXT,
issuer_url TEXT,
authorization_url TEXT,
token_url TEXT,
identifier_path TEXT,
name_path TEXT,
scopes TEXT DEFAULT 'openid email profile',
totp_secret TEXT,
totp_enabled INTEGER NOT NULL DEFAULT 0,
totp_backup_codes TEXT
2025-08-07 02:20:27 -05:00
);
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
2025-08-07 02:20:27 -05:00
);
2025-11-05 10:36:16 -06:00
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
jwt_token TEXT NOT NULL,
device_type TEXT NOT NULL,
device_info TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
2025-11-05 10:36:16 -06:00
);
2026-03-08 18:02:14 -05:00
CREATE TABLE IF NOT EXISTS trusted_devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
device_fingerprint TEXT NOT NULL,
device_type TEXT NOT NULL,
device_info TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS ssh_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT,
ip TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT NOT NULL,
folder TEXT,
tags TEXT,
pin INTEGER NOT NULL DEFAULT 0,
auth_type TEXT NOT NULL,
password TEXT,
key TEXT,
key_password TEXT,
key_type TEXT,
enable_terminal INTEGER NOT NULL DEFAULT 1,
enable_tunnel INTEGER NOT NULL DEFAULT 1,
tunnel_connections TEXT,
enable_file_manager INTEGER NOT NULL DEFAULT 1,
2025-12-31 22:20:12 -06:00
enable_docker INTEGER NOT NULL DEFAULT 0,
2025-09-12 14:42:00 -05:00
default_path TEXT,
2025-11-05 10:36:16 -06:00
autostart_password TEXT,
autostart_key TEXT,
autostart_key_password TEXT,
force_keyboard_interactive TEXT,
stats_config TEXT,
2025-12-31 22:20:12 -06:00
docker_config TEXT,
2025-11-05 10:36:16 -06:00
terminal_config TEXT,
2025-12-31 22:20:12 -06:00
notes TEXT,
use_socks5 INTEGER,
socks5_host TEXT,
socks5_port INTEGER,
socks5_username TEXT,
socks5_password TEXT,
2025-09-12 14:42:00 -05:00
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS file_manager_recent (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
last_opened TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS file_manager_pinned (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
pinned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS file_manager_shortcuts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
2025-08-13 00:05:13 -05:00
2026-06-04 15:16:53 -04:00
CREATE TABLE IF NOT EXISTS transfer_recent (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
source_host_id INTEGER NOT NULL,
dest_host_id INTEGER NOT NULL,
dest_path TEXT NOT NULL,
dest_path_label TEXT NOT NULL,
last_used TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (source_host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
FOREIGN KEY (dest_host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
2025-09-12 14:42:00 -05:00
CREATE TABLE IF NOT EXISTS dismissed_alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
alert_id TEXT NOT NULL,
dismissed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
CREATE TABLE IF NOT EXISTS ssh_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
folder TEXT,
tags TEXT,
auth_type TEXT NOT NULL,
2026-02-12 22:28:13 -06:00
username TEXT,
2025-09-12 14:42:00 -05:00
password TEXT,
key TEXT,
key_password TEXT,
key_type TEXT,
usage_count INTEGER NOT NULL DEFAULT 0,
last_used TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
CREATE TABLE IF NOT EXISTS ssh_credential_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
credential_id INTEGER NOT NULL,
host_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
2025-09-12 14:42:00 -05:00
);
2025-10-01 15:40:10 -05:00
2025-11-05 10:36:16 -06:00
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
content TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
2026-05-06 15:12:07 -05:00
CREATE TABLE IF NOT EXISTS c2s_tunnel_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
config TEXT NOT NULL,
platform TEXT,
computer_name TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
2025-11-17 09:46:05 -06:00
CREATE TABLE IF NOT EXISTS ssh_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
color TEXT,
icon TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
2025-11-05 10:36:16 -06:00
);
CREATE TABLE IF NOT EXISTS recent_activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
host_id INTEGER NOT NULL,
2025-11-17 09:46:05 -06:00
host_name TEXT,
2025-11-05 10:36:16 -06:00
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
2025-11-17 09:46:05 -06:00
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS command_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
command TEXT NOT NULL,
executed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
2025-11-05 10:36:16 -06:00
);
2025-12-31 22:20:12 -06:00
CREATE TABLE IF NOT EXISTS host_access (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
user_id TEXT,
role_id INTEGER,
granted_by TEXT NOT NULL,
permission_level TEXT NOT NULL DEFAULT 'use',
expires_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_accessed_at TEXT,
access_count INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
description TEXT,
is_system INTEGER NOT NULL DEFAULT 0,
permissions TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
role_id INTEGER NOT NULL,
granted_by TEXT,
granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
username TEXT NOT NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
resource_name TEXT,
details TEXT,
ip_address TEXT,
user_agent TEXT,
success INTEGER NOT NULL,
error_message TEXT,
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS session_recordings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
access_id INTEGER,
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ended_at TEXT,
duration INTEGER,
commands TEXT,
dangerous_actions TEXT,
recording_path TEXT,
terminated_by_owner INTEGER DEFAULT 0,
termination_reason TEXT,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL
);
2026-05-06 15:12:07 -05:00
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_prefix TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT,
last_used_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
2026-05-28 22:05:25 -04:00
CREATE TABLE IF NOT EXISTS user_open_tabs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
tab_type TEXT NOT NULL,
host_id INTEGER,
label TEXT NOT NULL,
tab_order INTEGER NOT NULL DEFAULT 0,
backend_session_id TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_preferences (
user_id TEXT PRIMARY KEY,
reopen_tabs_on_login INTEGER NOT NULL DEFAULT 0,
2026-06-04 15:16:53 -04:00
theme TEXT,
font_size TEXT,
accent_color TEXT,
language TEXT,
2026-05-28 22:05:25 -04:00
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
2026-06-16 15:59:53 -05:00
CREATE TABLE IF NOT EXISTS host_metrics_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
layout TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_host_metrics_prefs_user_host
ON host_metrics_preferences (user_id, host_id);
CREATE TABLE IF NOT EXISTS host_health_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
checks TEXT NOT NULL,
interval_seconds INTEGER NOT NULL DEFAULT 300,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_host_health_checks_user_host
ON host_health_checks (user_id, host_id);
CREATE TABLE IF NOT EXISTS host_health_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
check_id TEXT NOT NULL,
ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ok INTEGER NOT NULL,
latency_ms INTEGER,
detail TEXT,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_host_health_history_lookup
ON host_health_history (user_id, host_id, check_id, ts);
CREATE TABLE IF NOT EXISTS sso_providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
display_order INTEGER NOT NULL DEFAULT 0,
config TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
2025-08-07 02:20:27 -05:00
` );
2026-05-28 22:05:25 -04:00
try {
sqlite . prepare ( "DELETE FROM user_open_tabs" ). run ();
databaseLogger . info ( "Open tabs cleared on startup" , {
operation : "db_init_open_tabs_cleanup" ,
});
} catch ( e ) {
databaseLogger . warn ( "Could not clear open tabs on startup" , {
operation : "db_init_open_tabs_cleanup_failed" ,
error : e ,
});
}
2025-11-05 10:36:16 -06:00
try {
2026-05-06 15:12:07 -05:00
const result = sqlite
. prepare ( "DELETE FROM sessions WHERE expires_at <= ?" )
. run ( new Date (). toISOString ());
if ( result . changes > 0 ) {
databaseLogger . info ( "Expired sessions cleaned up on startup" , {
operation : "db_init_session_cleanup" ,
deletedSessions : result.changes ,
});
}
2025-11-05 10:36:16 -06:00
} catch ( e ) {
2026-02-12 22:28:13 -06:00
databaseLogger . warn ( "Could not clear expired sessions on startup" , {
2025-11-05 10:36:16 -06:00
operation : "db_init_session_cleanup_failed" ,
error : e ,
});
}
2025-10-01 15:40:10 -05:00
migrateSchema ();
try {
const row = sqlite
. prepare ( "SELECT value FROM settings WHERE key = 'allow_registration'" )
. get ();
if ( ! row ) {
sqlite
. prepare (
"INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')" ,
)
. run ();
}
} catch ( e ) {
databaseLogger . warn ( "Could not initialize default settings" , {
operation : "db_init" ,
error : e ,
});
}
2025-11-05 10:36:16 -06:00
try {
const row = sqlite
. prepare ( "SELECT value FROM settings WHERE key = 'allow_password_login'" )
. get ();
if ( ! row ) {
sqlite
. prepare (
"INSERT INTO settings (key, value) VALUES ('allow_password_login', 'true')" ,
)
. run ();
}
} catch ( e ) {
databaseLogger . warn ( "Could not initialize allow_password_login setting" , {
operation : "db_init" ,
error : e ,
});
}
2026-03-14 20:05:05 -05:00
try {
const row = sqlite
. prepare ( "SELECT value FROM settings WHERE key = 'guac_enabled'" )
. get ();
if ( ! row ) {
sqlite
. prepare (
"INSERT INTO settings (key, value) VALUES ('guac_enabled', 'true')" ,
)
. run ();
}
} catch ( e ) {
databaseLogger . warn ( "Could not initialize guac_enabled setting" , {
operation : "db_init" ,
error : e ,
});
}
try {
const row = sqlite
. prepare ( "SELECT value FROM settings WHERE key = 'guac_url'" )
. get ();
if ( ! row ) {
2026-06-04 15:16:53 -04:00
const defaultGuacUrl = ` ${ process . env . GUACD_HOST || "localhost" } : ${ process . env . GUACD_PORT || "4822" } ` ;
2026-03-14 20:05:05 -05:00
sqlite
. prepare (
2026-06-04 15:16:53 -04:00
"INSERT INTO settings (key, value) VALUES ('guac_url', ?)" ,
2026-03-14 20:05:05 -05:00
)
2026-06-04 15:16:53 -04:00
. run ( defaultGuacUrl );
2026-03-14 20:05:05 -05:00
}
} catch ( e ) {
databaseLogger . warn ( "Could not initialize guac_url setting" , {
operation : "db_init" ,
error : e ,
});
}
2025-10-01 15:40:10 -05:00
}
2025-09-12 14:42:00 -05:00
const addColumnIfNotExists = (
table : string ,
column : string ,
definition : string ,
) => {
try {
sqlite
. prepare (
2025-11-17 09:46:05 -06:00
`SELECT " ${ column } "
2025-09-12 14:42:00 -05:00
FROM ${ table } LIMIT 1` ,
)
. get ();
2025-11-05 10:36:16 -06:00
} catch {
2025-08-07 02:20:27 -05:00
try {
2025-09-12 14:42:00 -05:00
sqlite . exec ( `ALTER TABLE ${ table }
2025-11-17 09:46:05 -06:00
ADD COLUMN " ${ column } " ${ definition } ;` );
2025-09-12 14:42:00 -05:00
} catch ( alterError ) {
databaseLogger . warn ( `Failed to add column ${ column } to ${ table } ` , {
operation : "schema_migration" ,
table ,
column ,
error : alterError ,
});
2025-08-07 02:20:27 -05:00
}
2025-09-12 14:42:00 -05:00
}
2025-08-07 02:20:27 -05:00
};
const migrateSchema = () => {
2026-06-04 15:16:53 -04:00
addColumnIfNotExists ( "user_preferences" , "theme" , "TEXT" );
addColumnIfNotExists ( "user_preferences" , "font_size" , "TEXT" );
addColumnIfNotExists ( "user_preferences" , "accent_color" , "TEXT" );
addColumnIfNotExists ( "user_preferences" , "language" , "TEXT" );
2026-06-16 15:59:53 -05:00
addColumnIfNotExists ( "user_preferences" , "storage_mode" , "TEXT" );
addColumnIfNotExists ( "user_preferences" , "command_autocomplete" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "command_palette_enabled" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "show_host_tags" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "host_tray_on_click" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "pin_app_rail" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "folders_collapsed" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "confirm_snippet_execution" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "disable_update_check" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "confirm_tab_close" , "INTEGER" );
addColumnIfNotExists ( "user_preferences" , "hidden_rail_tabs" , "TEXT" );
2026-06-21 15:55:41 -05:00
addColumnIfNotExists ( "user_preferences" , "compact_host_view" , "INTEGER" );
2026-06-04 15:16:53 -04:00
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "users" , "is_admin" , "INTEGER NOT NULL DEFAULT 0" );
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "users" , "is_oidc" , "INTEGER NOT NULL DEFAULT 0" );
addColumnIfNotExists ( "users" , "oidc_identifier" , "TEXT" );
addColumnIfNotExists ( "users" , "client_id" , "TEXT" );
addColumnIfNotExists ( "users" , "client_secret" , "TEXT" );
addColumnIfNotExists ( "users" , "issuer_url" , "TEXT" );
addColumnIfNotExists ( "users" , "authorization_url" , "TEXT" );
addColumnIfNotExists ( "users" , "token_url" , "TEXT" );
2025-08-18 00:13:21 -05:00
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "users" , "identifier_path" , "TEXT" );
addColumnIfNotExists ( "users" , "name_path" , "TEXT" );
addColumnIfNotExists ( "users" , "scopes" , "TEXT" );
2025-09-03 00:14:49 -05:00
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "users" , "totp_secret" , "TEXT" );
addColumnIfNotExists ( "users" , "totp_enabled" , "INTEGER NOT NULL DEFAULT 0" );
addColumnIfNotExists ( "users" , "totp_backup_codes" , "TEXT" );
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "ssh_data" , "name" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "folder" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "tags" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "pin" , "INTEGER NOT NULL DEFAULT 0" );
addColumnIfNotExists (
"ssh_data" ,
"auth_type" ,
'TEXT NOT NULL DEFAULT "password"' ,
);
addColumnIfNotExists ( "ssh_data" , "password" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "key" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "key_password" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "key_type" , "TEXT" );
addColumnIfNotExists (
"ssh_data" ,
"enable_terminal" ,
"INTEGER NOT NULL DEFAULT 1" ,
);
2026-06-16 15:59:53 -05:00
addColumnIfNotExists (
"ssh_data" ,
"enable_session_logging" ,
"INTEGER NOT NULL DEFAULT 1" ,
);
addColumnIfNotExists (
"ssh_data" ,
"enable_command_history" ,
"INTEGER NOT NULL DEFAULT 1" ,
);
2025-09-12 14:42:00 -05:00
addColumnIfNotExists (
"ssh_data" ,
"enable_tunnel" ,
"INTEGER NOT NULL DEFAULT 1" ,
);
addColumnIfNotExists ( "ssh_data" , "tunnel_connections" , "TEXT" );
2025-11-17 09:46:05 -06:00
addColumnIfNotExists ( "ssh_data" , "jump_hosts" , "TEXT" );
2025-09-12 14:42:00 -05:00
addColumnIfNotExists (
"ssh_data" ,
"enable_file_manager" ,
"INTEGER NOT NULL DEFAULT 1" ,
);
2026-06-21 15:55:41 -05:00
addColumnIfNotExists (
"ssh_data" ,
"scp_legacy" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "ssh_data" , "default_path" , "TEXT" );
addColumnIfNotExists (
"ssh_data" ,
"created_at" ,
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP" ,
);
addColumnIfNotExists (
"ssh_data" ,
"updated_at" ,
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP" ,
);
2025-11-05 10:36:16 -06:00
addColumnIfNotExists ( "ssh_data" , "force_keyboard_interactive" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "autostart_password" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "autostart_key" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "autostart_key_password" , "TEXT" );
2025-09-12 14:42:00 -05:00
addColumnIfNotExists (
"ssh_data" ,
"credential_id" ,
2025-11-17 09:46:05 -06:00
"INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL" ,
);
addColumnIfNotExists (
"ssh_data" ,
"override_credential_username" ,
"INTEGER" ,
2025-09-12 14:42:00 -05:00
);
2025-08-07 02:20:27 -05:00
2025-10-01 15:40:10 -05:00
addColumnIfNotExists ( "ssh_data" , "autostart_password" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "autostart_key" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "autostart_key_password" , "TEXT" );
2025-11-05 10:36:16 -06:00
addColumnIfNotExists ( "ssh_data" , "stats_config" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "terminal_config" , "TEXT" );
2025-11-17 09:46:05 -06:00
addColumnIfNotExists ( "ssh_data" , "quick_actions" , "TEXT" );
2025-12-31 22:20:12 -06:00
addColumnIfNotExists (
"ssh_data" ,
"enable_docker" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
addColumnIfNotExists ( "ssh_data" , "docker_config" , "TEXT" );
2026-06-16 15:59:53 -05:00
addColumnIfNotExists (
"ssh_data" ,
"enable_proxmox" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
addColumnIfNotExists ( "ssh_data" , "proxmox_config" , "TEXT" );
addColumnIfNotExists (
"ssh_data" ,
"enable_tmux_monitor" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
2025-12-31 22:20:12 -06:00
2026-03-14 20:05:05 -05:00
addColumnIfNotExists ( "ssh_data" , "connection_type" , 'TEXT NOT NULL DEFAULT "ssh"' );
addColumnIfNotExists ( "ssh_data" , "domain" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "security" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "ignore_cert" , "INTEGER NOT NULL DEFAULT 0" );
addColumnIfNotExists ( "ssh_data" , "guacamole_config" , "TEXT" );
2025-12-31 22:20:12 -06:00
addColumnIfNotExists ( "ssh_data" , "notes" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "use_socks5" , "INTEGER" );
addColumnIfNotExists ( "ssh_data" , "socks5_host" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "socks5_port" , "INTEGER" );
addColumnIfNotExists ( "ssh_data" , "socks5_username" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "socks5_password" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "socks5_proxy_chain" , "TEXT" );
2025-10-01 15:40:10 -05:00
2026-02-12 22:28:13 -06:00
addColumnIfNotExists ( "ssh_data" , "host_key_fingerprint" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "host_key_type" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "host_key_algorithm" , "TEXT DEFAULT 'sha256'" );
addColumnIfNotExists ( "ssh_data" , "host_key_first_seen" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "host_key_last_verified" , "TEXT" );
addColumnIfNotExists ( "ssh_data" , "host_key_changed_count" , "INTEGER DEFAULT 0" );
2026-01-24 19:49:42 -06:00
addColumnIfNotExists (
"ssh_data" ,
"show_terminal_in_sidebar" ,
"INTEGER NOT NULL DEFAULT 1" ,
);
addColumnIfNotExists (
"ssh_data" ,
"show_file_manager_in_sidebar" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
addColumnIfNotExists (
"ssh_data" ,
"show_tunnel_in_sidebar" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
addColumnIfNotExists (
"ssh_data" ,
"show_docker_in_sidebar" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
addColumnIfNotExists (
"ssh_data" ,
"show_server_stats_in_sidebar" ,
"INTEGER NOT NULL DEFAULT 0" ,
);
2025-10-01 15:40:10 -05:00
addColumnIfNotExists ( "ssh_credentials" , "private_key" , "TEXT" );
addColumnIfNotExists ( "ssh_credentials" , "public_key" , "TEXT" );
addColumnIfNotExists ( "ssh_credentials" , "detected_key_type" , "TEXT" );
2026-05-28 22:29:20 -05:00
addColumnIfNotExists ( "ssh_credentials" , "cert_public_key" , "TEXT" );
2025-12-31 22:20:12 -06:00
addColumnIfNotExists ( "ssh_credentials" , "system_password" , "TEXT" );
addColumnIfNotExists ( "ssh_credentials" , "system_key" , "TEXT" );
addColumnIfNotExists ( "ssh_credentials" , "system_key_password" , "TEXT" );
2026-02-12 22:28:13 -06:00
try {
const tableInfo = sqlite . prepare ( "PRAGMA table_info(ssh_credentials)" ). all () as Array < {
cid : number ;
name : string ;
type : string ;
notnull : number ;
dflt_value : string | null ;
pk : number ;
} > ;
const usernameCol = tableInfo . find (( col ) => col . name === "username" );
if ( usernameCol && usernameCol . notnull === 1 ) {
const tempTableName = "ssh_credentials_temp_migration" ;
const allColumns = tableInfo . map (( col ) => col . name ). join ( ", " );
sqlite . exec ( `PRAGMA foreign_keys = OFF` );
sqlite . exec ( `
CREATE TABLE ${ tempTableName } (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
folder TEXT,
tags TEXT,
auth_type TEXT NOT NULL,
username TEXT,
password TEXT,
key TEXT,
key_password TEXT,
key_type TEXT,
usage_count INTEGER NOT NULL DEFAULT 0,
last_used TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
private_key TEXT,
public_key TEXT,
detected_key_type TEXT,
system_password TEXT,
system_key TEXT,
system_key_password TEXT,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
INSERT INTO ${ tempTableName } SELECT ${ allColumns } FROM ssh_credentials;
DROP TABLE ssh_credentials;
ALTER TABLE ${ tempTableName } RENAME TO ssh_credentials;
` );
sqlite . exec ( `PRAGMA foreign_keys = ON` );
databaseLogger . info ( "Successfully migrated ssh_credentials table to remove username NOT NULL constraint" , {
operation : "schema_migration_username_nullable" ,
});
}
} catch ( migrationError ) {
databaseLogger . warn ( "Failed to migrate ssh_credentials username column" , {
operation : "schema_migration" ,
error : migrationError ,
});
}
2025-09-12 14:42:00 -05:00
addColumnIfNotExists ( "file_manager_recent" , "host_id" , "INTEGER NOT NULL" );
addColumnIfNotExists ( "file_manager_pinned" , "host_id" , "INTEGER NOT NULL" );
addColumnIfNotExists ( "file_manager_shortcuts" , "host_id" , "INTEGER NOT NULL" );
2025-11-17 09:46:05 -06:00
addColumnIfNotExists ( "snippets" , "folder" , "TEXT" );
addColumnIfNotExists ( "snippets" , "order" , "INTEGER NOT NULL DEFAULT 0" );
2026-05-28 22:29:20 -05:00
addColumnIfNotExists ( "snippets" , "host_filter" , "TEXT" );
2025-11-17 09:46:05 -06:00
try {
sqlite
. prepare ( "SELECT id FROM snippet_folders LIMIT 1" )
. get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS snippet_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
color TEXT,
icon TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create snippet_folders table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-04-22 16:55:23 -05:00
try {
sqlite . prepare ( "SELECT id FROM snippet_access LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS snippet_access (
id INTEGER PRIMARY KEY AUTOINCREMENT,
snippet_id INTEGER NOT NULL,
user_id TEXT,
role_id INTEGER,
granted_by TEXT NOT NULL,
permission_level TEXT NOT NULL DEFAULT 'view',
expires_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (snippet_id) REFERENCES snippets (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create snippet_access table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-05-06 15:12:07 -05:00
try {
sqlite . prepare ( "SELECT id FROM c2s_tunnel_presets LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS c2s_tunnel_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
config TEXT NOT NULL,
platform TEXT,
computer_name TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create c2s_tunnel_presets table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2025-11-05 10:36:16 -06:00
try {
sqlite
. prepare ( "SELECT id FROM sessions LIMIT 1" )
. get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
jwt_token TEXT NOT NULL,
device_type TEXT NOT NULL,
device_info TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create sessions table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-03-08 18:02:14 -05:00
try {
sqlite
. prepare ( "SELECT id FROM trusted_devices LIMIT 1" )
. get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS trusted_devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
device_fingerprint TEXT NOT NULL,
device_type TEXT NOT NULL,
device_info TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create trusted_devices table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-01-24 19:49:42 -06:00
try {
sqlite
. prepare ( "SELECT id FROM network_topology LIMIT 1" )
. get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS network_topology (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
topology TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create network_topology table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2025-12-31 22:20:12 -06:00
try {
sqlite . prepare ( "SELECT id FROM host_access LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS host_access (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
user_id TEXT,
role_id INTEGER,
granted_by TEXT NOT NULL,
permission_level TEXT NOT NULL DEFAULT 'use',
expires_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_accessed_at TEXT,
access_count INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create host_access table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
try {
sqlite . prepare ( "SELECT role_id FROM host_access LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( "ALTER TABLE host_access ADD COLUMN role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE" );
} catch ( alterError ) {
databaseLogger . warn ( "Failed to add role_id column" , {
operation : "schema_migration" ,
error : alterError ,
});
}
}
2026-05-28 22:29:20 -05:00
try {
sqlite . prepare ( "SELECT override_credential_id FROM host_access LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( "ALTER TABLE host_access ADD COLUMN override_credential_id INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL" );
} catch ( alterError ) {
databaseLogger . warn ( "Failed to add override_credential_id column" , {
operation : "schema_migration" ,
error : alterError ,
});
}
}
2025-12-31 22:20:12 -06:00
try {
sqlite . prepare ( "SELECT sudo_password FROM ssh_data LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( "ALTER TABLE ssh_data ADD COLUMN sudo_password TEXT" );
} catch ( alterError ) {
databaseLogger . warn ( "Failed to add sudo_password column" , {
operation : "schema_migration" ,
error : alterError ,
});
}
}
2026-03-15 00:52:21 -05:00
const sshDataMigrations : Array < { column : string ; sql : string } > = [
{ column : "connection_type" , sql : "ALTER TABLE ssh_data ADD COLUMN connection_type TEXT NOT NULL DEFAULT 'ssh'" },
{ column : "credential_id" , sql : "ALTER TABLE ssh_data ADD COLUMN credential_id INTEGER" },
{ column : "override_credential_username" , sql : "ALTER TABLE ssh_data ADD COLUMN override_credential_username INTEGER" },
{ column : "jump_hosts" , sql : "ALTER TABLE ssh_data ADD COLUMN jump_hosts TEXT" },
{ column : "show_terminal_in_sidebar" , sql : "ALTER TABLE ssh_data ADD COLUMN show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1" },
{ column : "show_file_manager_in_sidebar" , sql : "ALTER TABLE ssh_data ADD COLUMN show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column : "show_tunnel_in_sidebar" , sql : "ALTER TABLE ssh_data ADD COLUMN show_tunnel_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column : "show_docker_in_sidebar" , sql : "ALTER TABLE ssh_data ADD COLUMN show_docker_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column : "show_server_stats_in_sidebar" , sql : "ALTER TABLE ssh_data ADD COLUMN show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0" },
{ column : "quick_actions" , sql : "ALTER TABLE ssh_data ADD COLUMN quick_actions TEXT" },
{ column : "domain" , sql : "ALTER TABLE ssh_data ADD COLUMN domain TEXT" },
{ column : "security" , sql : "ALTER TABLE ssh_data ADD COLUMN security TEXT" },
{ column : "ignore_cert" , sql : "ALTER TABLE ssh_data ADD COLUMN ignore_cert INTEGER NOT NULL DEFAULT 0" },
{ column : "guacamole_config" , sql : "ALTER TABLE ssh_data ADD COLUMN guacamole_config TEXT" },
{ column : "socks5_proxy_chain" , sql : "ALTER TABLE ssh_data ADD COLUMN socks5_proxy_chain TEXT" },
{ column : "host_key_fingerprint" , sql : "ALTER TABLE ssh_data ADD COLUMN host_key_fingerprint TEXT" },
{ column : "host_key_type" , sql : "ALTER TABLE ssh_data ADD COLUMN host_key_type TEXT" },
{ column : "host_key_algorithm" , sql : "ALTER TABLE ssh_data ADD COLUMN host_key_algorithm TEXT NOT NULL DEFAULT 'sha256'" },
{ column : "host_key_first_seen" , sql : "ALTER TABLE ssh_data ADD COLUMN host_key_first_seen TEXT" },
{ column : "host_key_last_verified" , sql : "ALTER TABLE ssh_data ADD COLUMN host_key_last_verified TEXT" },
{ column : "host_key_changed_count" , sql : "ALTER TABLE ssh_data ADD COLUMN host_key_changed_count INTEGER NOT NULL DEFAULT 0" },
2026-04-22 16:55:23 -05:00
{ column : "mac_address" , sql : "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" },
{ column : "port_knock_sequence" , sql : "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" },
2026-05-11 01:23:11 -05:00
{ column : "enable_ssh" , sql : "ALTER TABLE ssh_data ADD COLUMN enable_ssh INTEGER NOT NULL DEFAULT 1" },
{ column : "enable_rdp" , sql : "ALTER TABLE ssh_data ADD COLUMN enable_rdp INTEGER NOT NULL DEFAULT 0" },
{ column : "enable_vnc" , sql : "ALTER TABLE ssh_data ADD COLUMN enable_vnc INTEGER NOT NULL DEFAULT 0" },
{ column : "enable_telnet" , sql : "ALTER TABLE ssh_data ADD COLUMN enable_telnet INTEGER NOT NULL DEFAULT 0" },
{ column : "ssh_port" , sql : "ALTER TABLE ssh_data ADD COLUMN ssh_port INTEGER DEFAULT 22" },
{ column : "rdp_port" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_port INTEGER DEFAULT 3389" },
{ column : "vnc_port" , sql : "ALTER TABLE ssh_data ADD COLUMN vnc_port INTEGER DEFAULT 5900" },
{ column : "telnet_port" , sql : "ALTER TABLE ssh_data ADD COLUMN telnet_port INTEGER DEFAULT 23" },
{ column : "rdp_user" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_user TEXT" },
{ column : "rdp_password" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_password TEXT" },
{ column : "rdp_domain" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_domain TEXT" },
{ column : "rdp_security" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_security TEXT" },
{ column : "rdp_ignore_cert" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_ignore_cert INTEGER DEFAULT 0" },
{ column : "vnc_password" , sql : "ALTER TABLE ssh_data ADD COLUMN vnc_password TEXT" },
{ column : "vnc_user" , sql : "ALTER TABLE ssh_data ADD COLUMN vnc_user TEXT" },
{ column : "telnet_user" , sql : "ALTER TABLE ssh_data ADD COLUMN telnet_user TEXT" },
{ column : "telnet_password" , sql : "ALTER TABLE ssh_data ADD COLUMN telnet_password TEXT" },
2026-06-21 15:55:41 -05:00
{ column : "rdp_credential_id" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_credential_id INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL" },
{ column : "vnc_credential_id" , sql : "ALTER TABLE ssh_data ADD COLUMN vnc_credential_id INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL" },
{ column : "wol_broadcast_address" , sql : "ALTER TABLE ssh_data ADD COLUMN wol_broadcast_address TEXT" },
{ column : "use_warpgate" , sql : "ALTER TABLE ssh_data ADD COLUMN use_warpgate INTEGER NOT NULL DEFAULT 0" },
{ column : "telnet_credential_id" , sql : "ALTER TABLE ssh_data ADD COLUMN telnet_credential_id INTEGER REFERENCES ssh_credentials(id) ON DELETE SET NULL" },
{ column : "rdp_auth_type" , sql : "ALTER TABLE ssh_data ADD COLUMN rdp_auth_type TEXT" },
{ column : "vnc_auth_type" , sql : "ALTER TABLE ssh_data ADD COLUMN vnc_auth_type TEXT" },
{ column : "telnet_auth_type" , sql : "ALTER TABLE ssh_data ADD COLUMN telnet_auth_type TEXT" },
2026-03-15 00:52:21 -05:00
];
for ( const migration of sshDataMigrations ) {
try {
sqlite . prepare ( `SELECT ${ migration . column } FROM ssh_data LIMIT 1` ). get ();
} catch {
try {
sqlite . exec ( migration . sql );
} catch ( alterError ) {
databaseLogger . warn ( `Failed to add ${ migration . column } column` , {
operation : "schema_migration" ,
error : alterError ,
});
}
}
}
2026-06-21 15:55:41 -05:00
// Migrate legacy authType="warpgate" hosts to useWarpgate=1 with authType="none"
try {
const result = sqlite
. prepare ( "UPDATE ssh_data SET use_warpgate = 1, auth_type = 'none' WHERE auth_type = 'warpgate'" )
. run ();
if ( result . changes > 0 ) {
databaseLogger . info ( `Migrated ${ result . changes } host(s) from authType='warpgate' to useWarpgate=true` , {
operation : "warpgate_auth_migration" ,
});
}
} catch ( e ) {
databaseLogger . warn ( "Failed to migrate legacy warpgate authType hosts" , {
operation : "warpgate_auth_migration" ,
error : e ,
});
}
2026-05-28 22:29:20 -05:00
// Copy unencrypted username/domain into protocol-specific columns for old guac hosts.
// Passwords are handled via the legacy field name fallback in lazy-field-encryption.ts.
const usernameDomainBackfills = [
{
protocol : "rdp" ,
sql : "UPDATE ssh_data SET rdp_user = username, rdp_password = password, rdp_domain = domain WHERE connection_type = 'rdp' AND rdp_user IS NULL AND rdp_password IS NULL" ,
},
{
protocol : "vnc" ,
sql : "UPDATE ssh_data SET vnc_user = username, vnc_password = password WHERE connection_type = 'vnc' AND vnc_user IS NULL AND vnc_password IS NULL" ,
},
{
protocol : "telnet" ,
sql : "UPDATE ssh_data SET telnet_user = username, telnet_password = password WHERE connection_type = 'telnet' AND telnet_user IS NULL AND telnet_password IS NULL" ,
},
];
for ( const backfill of usernameDomainBackfills ) {
try {
const result = sqlite . prepare ( backfill . sql ). run ();
if ( result . changes > 0 ) {
databaseLogger . info (
`Backfilled ${ result . changes } ${ backfill . protocol } host credential(s)` ,
{ operation : "guac_credential_backfill" },
);
}
} catch ( e ) {
databaseLogger . warn ( `Failed to backfill ${ backfill . protocol } host credentials` , {
operation : "guac_credential_backfill" ,
error : e ,
});
}
}
2026-06-16 15:59:53 -05:00
// Rename the legacy "stats" tab type to "host-metrics" so previously saved
// open tabs restore correctly after the Server Stats -> Host Metrics rename.
try {
sqlite . prepare ( "SELECT id FROM user_open_tabs LIMIT 1" ). get ();
const result = sqlite
. prepare (
"UPDATE user_open_tabs SET tab_type = 'host-metrics' WHERE tab_type = 'stats'" ,
)
. run ();
if ( result . changes > 0 ) {
databaseLogger . info (
`Migrated ${ result . changes } open tab(s) from 'stats' to 'host-metrics'` ,
{ operation : "open_tabs_tab_type_migration" },
);
}
} catch {
// user_open_tabs table not present yet; nothing to migrate.
}
2025-12-31 22:20:12 -06:00
try {
sqlite . prepare ( "SELECT id FROM roles LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
description TEXT,
is_system INTEGER NOT NULL DEFAULT 0,
permissions TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create roles table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
try {
sqlite . prepare ( "SELECT id FROM user_roles LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
role_id INTEGER NOT NULL,
granted_by TEXT,
granted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE SET NULL
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create user_roles table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
try {
sqlite . prepare ( "SELECT id FROM audit_logs LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
username TEXT NOT NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
resource_name TEXT,
details TEXT,
ip_address TEXT,
user_agent TEXT,
success INTEGER NOT NULL,
error_message TEXT,
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create audit_logs table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
try {
sqlite . prepare ( "SELECT id FROM session_recordings LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS session_recordings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
access_id INTEGER,
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
ended_at TEXT,
duration INTEGER,
commands TEXT,
dangerous_actions TEXT,
recording_path TEXT,
terminated_by_owner INTEGER DEFAULT 0,
termination_reason TEXT,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create session_recordings table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
try {
sqlite . prepare ( "SELECT id FROM shared_credentials LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS shared_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_access_id INTEGER NOT NULL,
original_credential_id INTEGER NOT NULL,
target_user_id TEXT NOT NULL,
encrypted_username TEXT NOT NULL,
encrypted_auth_type TEXT NOT NULL,
encrypted_password TEXT,
encrypted_key TEXT,
encrypted_key_password TEXT,
encrypted_key_type TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
needs_re_encryption INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (host_access_id) REFERENCES host_access (id) ON DELETE CASCADE,
FOREIGN KEY (original_credential_id) REFERENCES ssh_credentials (id) ON DELETE CASCADE,
FOREIGN KEY (target_user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create shared_credentials table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-02-12 22:28:13 -06:00
try {
sqlite . prepare ( "SELECT id FROM opkssh_tokens LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS opkssh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
ssh_cert TEXT NOT NULL,
private_key TEXT NOT NULL,
email TEXT,
sub TEXT,
issuer TEXT,
audience TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT NOT NULL,
last_used TEXT,
UNIQUE(user_id, host_id),
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create opkssh_tokens table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-05-06 15:12:07 -05:00
try {
sqlite . prepare ( "SELECT id FROM api_keys LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_prefix TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TEXT,
last_used_at TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create api_keys table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
2026-06-16 15:59:53 -05:00
// --- tmux-monitor begin ---
try {
sqlite . prepare ( "SELECT id FROM tmux_session_tags LIMIT 1" ). get ();
} catch {
try {
sqlite . exec ( `
CREATE TABLE IF NOT EXISTS tmux_session_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
session_name TEXT NOT NULL,
tag TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
` );
} catch ( createError ) {
databaseLogger . warn ( "Failed to create tmux_session_tags table" , {
operation : "schema_migration" ,
error : createError ,
});
}
}
// Rebuild pre-release tables created without the host FK so deleting a
// host cascades to its tags (SQLite cannot add a FK via ALTER TABLE).
try {
const tagFks = sqlite
. prepare ( "PRAGMA foreign_key_list(tmux_session_tags)" )
. all () as Array < { table : string ; from : string } > ;
const hasHostFk = tagFks . some (
( fk ) => fk . from === "host_id" && fk . table === "ssh_data" ,
);
if ( ! hasHostFk ) {
sqlite . exec ( `
BEGIN;
CREATE TABLE tmux_session_tags_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
host_id INTEGER NOT NULL,
session_name TEXT NOT NULL,
tag TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (host_id) REFERENCES ssh_data (id) ON DELETE CASCADE
);
INSERT INTO tmux_session_tags_new (id, user_id, host_id, session_name, tag, created_at)
SELECT id, user_id, host_id, session_name, tag, created_at
FROM tmux_session_tags
WHERE user_id IN (SELECT id FROM users)
AND host_id IN (SELECT id FROM ssh_data);
DROP TABLE tmux_session_tags;
ALTER TABLE tmux_session_tags_new RENAME TO tmux_session_tags;
COMMIT;
` );
databaseLogger . info ( "Rebuilt tmux_session_tags with host FK" , {
operation : "schema_migration" ,
});
}
} catch ( rebuildError ) {
try {
sqlite . exec ( "ROLLBACK;" );
} catch {
// no transaction open
}
databaseLogger . warn ( "Failed to add host FK to tmux_session_tags" , {
operation : "schema_migration" ,
error : rebuildError ,
});
}
// --- tmux-monitor end ---
2025-12-31 22:20:12 -06:00
try {
const existingRoles = sqlite . prepare ( "SELECT name, is_system FROM roles" ). all () as Array < { name : string ; is_system : number } > ;
try {
const validSystemRoles = [ 'admin' , 'user' ];
const unwantedRoleNames = [ 'superAdmin' , 'powerUser' , 'readonly' , 'member' ];
const deleteByName = sqlite . prepare ( "DELETE FROM roles WHERE name = ?" );
for ( const roleName of unwantedRoleNames ) {
2026-03-08 18:02:14 -05:00
deleteByName . run ( roleName );
2025-12-31 22:20:12 -06:00
}
const deleteOldSystemRole = sqlite . prepare ( "DELETE FROM roles WHERE name = ? AND is_system = 1" );
for ( const role of existingRoles ) {
if ( role . is_system === 1 && ! validSystemRoles . includes ( role . name ) && ! unwantedRoleNames . includes ( role . name )) {
2026-03-08 18:02:14 -05:00
deleteOldSystemRole . run ( role . name );
2025-12-31 22:20:12 -06:00
}
}
} catch ( cleanupError ) {
databaseLogger . warn ( "Failed to clean up old system roles" , {
operation : "schema_migration" ,
error : cleanupError ,
});
}
const systemRoles = [
{
name : "admin" ,
displayName : "rbac.roles.admin" ,
description : "Administrator with full access" ,
permissions : null ,
},
{
name : "user" ,
displayName : "rbac.roles.user" ,
description : "Regular user" ,
permissions : null ,
},
];
for ( const role of systemRoles ) {
const existingRole = sqlite . prepare ( "SELECT id FROM roles WHERE name = ?" ). get ( role . name );
if ( ! existingRole ) {
try {
sqlite . prepare ( `
INSERT INTO roles (name, display_name, description, is_system, permissions)
VALUES (?, ?, ?, 1, ?)
` ). run ( role . name , role . displayName , role . description , role . permissions );
} catch ( insertError ) {
databaseLogger . warn ( `Failed to create system role: ${ role . name } ` , {
operation : "schema_migration" ,
error : insertError ,
});
}
}
}
try {
const adminUsers = sqlite . prepare ( "SELECT id FROM users WHERE is_admin = 1" ). all () as { id : string }[];
const normalUsers = sqlite . prepare ( "SELECT id FROM users WHERE is_admin = 0" ). all () as { id : string }[];
const adminRole = sqlite . prepare ( "SELECT id FROM roles WHERE name = 'admin'" ). get () as { id : number } | undefined ;
const userRole = sqlite . prepare ( "SELECT id FROM roles WHERE name = 'user'" ). get () as { id : number } | undefined ;
if ( adminRole ) {
const insertUserRole = sqlite . prepare ( `
INSERT OR IGNORE INTO user_roles (user_id, role_id, granted_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
` );
for ( const admin of adminUsers ) {
try {
insertUserRole . run ( admin . id , adminRole . id );
2026-03-08 18:02:14 -05:00
} catch {
2025-12-31 22:20:12 -06:00
// Ignore duplicate errors
}
}
}
if ( userRole ) {
const insertUserRole = sqlite . prepare ( `
INSERT OR IGNORE INTO user_roles (user_id, role_id, granted_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
` );
for ( const user of normalUsers ) {
try {
insertUserRole . run ( user . id , userRole . id );
2026-03-08 18:02:14 -05:00
} catch {
2025-12-31 22:20:12 -06:00
// Ignore duplicate errors
}
}
}
} catch ( migrationError ) {
databaseLogger . warn ( "Failed to migrate existing users to roles" , {
operation : "schema_migration" ,
error : migrationError ,
});
}
} catch ( seedError ) {
databaseLogger . warn ( "Failed to seed system roles" , {
operation : "schema_migration" ,
error : seedError ,
});
}
2026-06-16 15:59:53 -05:00
addColumnIfNotExists ( "users" , "sso_provider_id" , "INTEGER" );
// Migrate legacy single oidc_config settings blob into sso_providers table
try {
const migrationDone = sqlite
. prepare ( "SELECT value FROM settings WHERE key = 'sso_migration_v1'" )
. get ();
if ( ! migrationDone ) {
const providerCount = (
sqlite . prepare ( "SELECT COUNT(*) as c FROM sso_providers" ). get () as { c : number }
). c ;
if ( providerCount === 0 ) {
const legacyRow = sqlite
. prepare ( "SELECT value FROM settings WHERE key = 'oidc_config'" )
. get () as { value : string } | undefined ;
if ( legacyRow ) {
sqlite
. prepare (
"INSERT INTO sso_providers (name, type, enabled, display_order, config) VALUES (?, 'oidc', 1, 0, ?)" ,
)
. run ( "OIDC" , legacyRow . value );
databaseLogger . info ( "Migrated legacy oidc_config into sso_providers table" , {
operation : "sso_migration_v1" ,
});
}
}
sqlite
. prepare ( "INSERT OR REPLACE INTO settings (key, value) VALUES ('sso_migration_v1', 'true')" )
. run ();
}
} catch ( e ) {
databaseLogger . warn ( "Failed to run SSO migration v1" , {
operation : "sso_migration_v1" ,
error : e ,
});
}
2025-09-12 14:42:00 -05:00
databaseLogger . success ( "Schema migration completed" , {
operation : "schema_migration" ,
});
2025-08-07 02:20:27 -05:00
};
2026-04-22 16:55:23 -05:00
async function saveMemoryDatabaseToFile () : Promise < void > {
2025-10-01 15:40:10 -05:00
if ( ! memoryDatabase ) return ;
2025-08-07 02:20:27 -05:00
2025-09-12 14:42:00 -05:00
try {
2025-10-01 15:40:10 -05:00
const buffer = memoryDatabase . serialize ();
if ( ! fs . existsSync ( dataDir )) {
fs . mkdirSync ( dataDir , { recursive : true });
2025-08-07 02:20:27 -05:00
}
2025-10-01 15:40:10 -05:00
2025-11-05 10:36:16 -06:00
try {
2026-03-08 18:02:14 -05:00
memoryDatabase
2025-11-05 10:36:16 -06:00
. prepare ( "SELECT COUNT(*) as count FROM sessions" )
. get () as { count : number };
2026-03-08 18:02:14 -05:00
} catch {
// expected - sessions table may not exist yet
2025-11-05 10:36:16 -06:00
}
2025-10-01 15:40:10 -05:00
if ( enableFileEncryption ) {
await DatabaseFileEncryption . encryptDatabaseFromBuffer (
buffer ,
encryptedDbPath ,
);
} else {
fs . writeFileSync ( dbPath , buffer );
}
2026-03-08 18:02:14 -05:00
DatabaseSaveTrigger . markClean ();
2025-10-01 15:40:10 -05:00
} catch ( error ) {
databaseLogger . error ( "Failed to save in-memory database" , error , {
operation : "memory_db_save_failed" ,
enableFileEncryption ,
2025-09-12 14:42:00 -05:00
});
}
2025-10-01 15:40:10 -05:00
}
async function handlePostInitFileEncryption() {
if ( ! enableFileEncryption ) return ;
try {
if ( memoryDatabase ) {
await saveMemoryDatabaseToFile ();
2026-03-08 18:02:14 -05:00
setInterval (() => {
if ( DatabaseSaveTrigger . isDirty ) {
saveMemoryDatabaseToFile ();
}
}, 5 * 60 * 1000 );
2025-10-01 15:40:10 -05:00
DatabaseSaveTrigger . initialize ( saveMemoryDatabaseToFile );
}
try {
const migration = new DatabaseMigration ( dataDir );
migration . cleanupOldBackups ();
} catch ( cleanupError ) {
databaseLogger . warn ( "Failed to cleanup old migration files" , {
operation : "migration_cleanup_startup_failed" ,
error :
cleanupError instanceof Error
? cleanupError . message
: "Unknown error" ,
});
}
} catch ( error ) {
databaseLogger . error (
"Failed to handle database file encryption setup" ,
error ,
{
operation : "db_encrypt_setup_failed" ,
},
);
}
}
async function initializeDatabase () : Promise < void > {
await initializeCompleteDatabase ();
await handlePostInitFileEncryption ();
}
export { initializeDatabase };
async function cleanupDatabase() {
if ( memoryDatabase ) {
try {
await saveMemoryDatabaseToFile ();
} catch ( error ) {
databaseLogger . error (
"Failed to save in-memory database before shutdown" ,
error ,
{
operation : "shutdown_save_failed" ,
},
);
}
}
try {
if ( sqlite ) {
sqlite . close ();
}
} catch ( error ) {
databaseLogger . warn ( "Error closing database connection" , {
operation : "db_close_error" ,
error : error instanceof Error ? error . message : "Unknown error" ,
});
}
try {
const tempDir = path . join ( dataDir , ".temp" );
if ( fs . existsSync ( tempDir )) {
const files = fs . readdirSync ( tempDir );
for ( const file of files ) {
try {
fs . unlinkSync ( path . join ( tempDir , file ));
2025-11-05 10:36:16 -06:00
} catch {
2026-03-08 18:02:14 -05:00
// expected - file cleanup best effort
2025-11-05 10:36:16 -06:00
}
2025-10-01 15:40:10 -05:00
}
try {
fs . rmdirSync ( tempDir );
2025-11-05 10:36:16 -06:00
} catch {
2026-03-08 18:02:14 -05:00
// expected - dir cleanup best effort
2025-11-05 10:36:16 -06:00
}
2025-10-01 15:40:10 -05:00
}
2025-11-05 10:36:16 -06:00
} catch {
2026-03-08 18:02:14 -05:00
// expected - temp dir cleanup best effort
2025-11-05 10:36:16 -06:00
}
2025-10-01 15:40:10 -05:00
}
process . on ( "exit" , () => {
if ( sqlite ) {
try {
sqlite . close ();
2025-11-05 10:36:16 -06:00
} catch {
2026-03-08 18:02:14 -05:00
// expected - database may already be closed
2025-11-05 10:36:16 -06:00
}
2025-10-01 15:40:10 -05:00
}
});
process . on ( "SIGINT" , async () => {
databaseLogger . info ( "Received SIGINT, cleaning up..." , {
operation : "shutdown" ,
});
await cleanupDatabase ();
process . exit ( 0 );
});
process . on ( "SIGTERM" , async () => {
databaseLogger . info ( "Received SIGTERM, cleaning up..." , {
operation : "shutdown" ,
});
await cleanupDatabase ();
process . exit ( 0 );
});
let db : ReturnType < typeof drizzle < typeof schema >>;
export function getDb () : ReturnType < typeof drizzle < typeof schema >> {
if ( ! db ) {
throw new Error (
"Database not initialized. Ensure initializeDatabase() is called before accessing db." ,
);
}
return db ;
}
export function getSqlite () : Database . Database {
if ( ! sqlite ) {
throw new Error (
"SQLite not initialized. Ensure initializeDatabase() is called before accessing sqlite." ,
);
}
return sqlite ;
}
export { db };
export { DatabaseFileEncryption };
export const databasePaths = {
main : actualDbPath ,
encrypted : encryptedDbPath ,
directory : dbDir ,
inMemory : true ,
2025-09-12 14:42:00 -05:00
};
2025-08-07 02:20:27 -05:00
2025-10-01 15:40:10 -05:00
export { saveMemoryDatabaseToFile };
2025-09-12 14:42:00 -05:00
2025-10-01 15:40:10 -05:00
export { DatabaseSaveTrigger };