modularize db code.

This commit is contained in:
KingKDot
2026-05-29 22:30:11 -05:00
parent 329d1e2981
commit 206ade731d
6 changed files with 2772 additions and 2759 deletions
+16
View File
@@ -0,0 +1,16 @@
import Database from "bun:sqlite";
import type { Statement } from "bun:sqlite";
import { resolve } from "path";
import { ensureDataDir } from "../paths";
export interface TypedDatabase extends Omit<Database, "run" | "query" | "prepare"> {
run(sql: string, ...params: any[]): import("bun:sqlite").Changes;
query<T = any>(sql: string): Statement<T>;
prepare<T = any>(sql: string): Statement<T>;
}
const dataDir = ensureDataDir();
export const dbPath = resolve(dataDir, "overlord.db");
export const db: TypedDatabase = new Database(dbPath) as any;
console.log(`[db] Using database at: ${dbPath}`);
File diff suppressed because it is too large Load Diff
+388
View File
@@ -0,0 +1,388 @@
import { db } from "./connection";
db.run(`
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
hwid TEXT,
role TEXT,
ip TEXT,
host TEXT,
os TEXT,
arch TEXT,
version TEXT,
user TEXT,
nickname TEXT,
custom_tag TEXT,
custom_tag_note TEXT,
monitors INTEGER,
country TEXT,
last_seen INTEGER,
online INTEGER,
ping_ms INTEGER,
bookmarked INTEGER NOT NULL DEFAULT 0,
build_tag TEXT,
built_by_user_id INTEGER
);
`);
try {
db.run(`ALTER TABLE clients ADD COLUMN role TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN hwid TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN ip TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN nickname TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN custom_tag TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN custom_tag_note TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN bookmarked INTEGER NOT NULL DEFAULT 0`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN build_tag TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN built_by_user_id INTEGER`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN enrollment_status TEXT NOT NULL DEFAULT 'pending'`);
} catch {}
try {
db.run(`UPDATE clients SET enrollment_status='pending' WHERE enrollment_status IS NULL`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN public_key TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN key_fingerprint TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN enrolled_at INTEGER`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN enrolled_by TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN cpu TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN gpu TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN ram TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0`);
} catch {}
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_public_key ON clients(public_key);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_key_fingerprint ON clients(key_fingerprint);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_enrollment_status ON clients(enrollment_status);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_online_last_seen ON clients(online, last_seen DESC);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_os_last_seen ON clients(os, last_seen DESC);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_ping_ms ON clients(ping_ms);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_clients_built_by_user_id ON clients(built_by_user_id);`,
);
try {
db.run(`ALTER TABLE clients ADD COLUMN disconnect_reason TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN disconnect_detail TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN elevation TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN permissions TEXT`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN notifications_muted INTEGER NOT NULL DEFAULT 0`);
} catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN deny_reason TEXT`);
} catch {}
db.run(`
CREATE TABLE IF NOT EXISTS client_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
color TEXT NOT NULL DEFAULT '#3b82f6',
created_at INTEGER NOT NULL
);
`);
try {
db.run(`ALTER TABLE clients ADD COLUMN group_id INTEGER REFERENCES client_groups(id) ON DELETE SET NULL`);
} catch {}
db.run(`CREATE INDEX IF NOT EXISTS idx_clients_group_id ON clients(group_id);`);
db.run(`
CREATE TABLE IF NOT EXISTS banned_ips (
ip TEXT PRIMARY KEY,
reason TEXT,
created_at INTEGER NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_banned_ips_created_at ON banned_ips(created_at DESC);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS revoked_tokens (
token_hash TEXT PRIMARY KEY,
expires_at INTEGER NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_revoked_tokens_expires ON revoked_tokens(expires_at);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL,
ip TEXT,
user_agent TEXT,
created_at INTEGER NOT NULL,
last_activity INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);`);
db.run(`
CREATE TABLE IF NOT EXISTS builds (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
start_time INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
files TEXT NOT NULL,
build_tag TEXT,
built_by_user_id INTEGER
);
`);
try { db.run(`ALTER TABLE builds ADD COLUMN build_tag TEXT`); } catch {}
try { db.run(`ALTER TABLE builds ADD COLUMN built_by_user_id INTEGER`); } catch {}
try { db.run(`ALTER TABLE builds ADD COLUMN blocked INTEGER NOT NULL DEFAULT 0`); } catch {}
db.run(`CREATE INDEX IF NOT EXISTS idx_builds_build_tag ON builds(build_tag);`);
db.run(`
CREATE TABLE IF NOT EXISTS build_claims (
build_id TEXT NOT NULL,
key_fingerprint TEXT NOT NULL,
claimed_at INTEGER NOT NULL,
PRIMARY KEY (build_id, key_fingerprint)
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_build_claims_build ON build_claims(build_id);`);
db.run(`
CREATE TABLE IF NOT EXISTS build_profiles (
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
profile_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (user_id, name)
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_build_profiles_user_updated ON build_profiles(user_id, updated_at DESC);`);
db.run(`
CREATE TABLE IF NOT EXISTS saved_scripts (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
content TEXT NOT NULL,
script_type TEXT NOT NULL DEFAULT 'powershell',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_saved_scripts_user ON saved_scripts(user_id, updated_at DESC);`);
db.run(`
CREATE TABLE IF NOT EXISTS notification_screenshots (
id TEXT PRIMARY KEY,
notification_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
format TEXT NOT NULL,
width INTEGER,
height INTEGER,
bytes BLOB NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_notification_id ON notification_screenshots(notification_id);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_notification_screenshots_ts ON notification_screenshots(ts DESC);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS notifications (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
host TEXT,
user TEXT,
os TEXT,
title TEXT NOT NULL,
process TEXT,
process_path TEXT,
pid INTEGER,
keyword TEXT,
category TEXT NOT NULL DEFAULT 'active_window',
ts INTEGER NOT NULL,
screenshot_id TEXT
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_notifications_ts ON notifications(ts DESC);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_notifications_client_id ON notifications(client_id);`);
db.run(`
CREATE TABLE IF NOT EXISTS push_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
created_at INTEGER NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id);`,
);
db.run(
`CREATE UNIQUE INDEX IF NOT EXISTS idx_push_subscriptions_endpoint ON push_subscriptions(endpoint);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS auto_scripts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
trigger TEXT NOT NULL,
script TEXT NOT NULL,
script_type TEXT NOT NULL,
enabled INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_auto_scripts_trigger ON auto_scripts(trigger, enabled);`,
);
try {
db.run(`ALTER TABLE auto_scripts ADD COLUMN os_filter TEXT NOT NULL DEFAULT '[]'`);
} catch { /* column already exists */ }
db.run(`
CREATE TABLE IF NOT EXISTS auto_script_runs (
script_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
PRIMARY KEY (script_id, client_id)
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_auto_script_runs_ts ON auto_script_runs(ts DESC);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS auto_deploys (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
trigger TEXT NOT NULL,
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
file_os TEXT NOT NULL,
args TEXT NOT NULL DEFAULT '',
hide_window INTEGER NOT NULL DEFAULT 1,
enabled INTEGER NOT NULL,
os_filter TEXT NOT NULL DEFAULT '[]',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_auto_deploys_trigger ON auto_deploys(trigger, enabled);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS auto_deploy_runs (
deploy_id TEXT NOT NULL,
client_id TEXT NOT NULL,
ts INTEGER NOT NULL,
PRIMARY KEY (deploy_id, client_id)
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_auto_deploy_runs_ts ON auto_deploy_runs(ts DESC);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
username TEXT NOT NULL,
user_role TEXT NOT NULL,
message TEXT NOT NULL,
created_at INTEGER NOT NULL
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON chat_messages(created_at DESC);`,
);
db.run(`
CREATE TABLE IF NOT EXISTS shared_files (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
stored_path TEXT NOT NULL,
size INTEGER NOT NULL,
mime_type TEXT NOT NULL,
uploaded_by INTEGER NOT NULL,
uploaded_by_username TEXT NOT NULL,
password_hash TEXT,
max_downloads INTEGER,
download_count INTEGER NOT NULL DEFAULT 0,
expires_at INTEGER,
created_at INTEGER NOT NULL,
description TEXT
);
`);
db.run(
`CREATE INDEX IF NOT EXISTS idx_shared_files_created_at ON shared_files(created_at DESC);`,
);
db.run(
`CREATE INDEX IF NOT EXISTS idx_shared_files_uploaded_by ON shared_files(uploaded_by);`,
);
+255
View File
@@ -0,0 +1,255 @@
import { logger } from "../logger";
import { db } from "./connection";
function addColumnIfMissing(table: string, definition: string): void {
try {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition}`);
} catch (err: any) {
if (!err?.message?.includes("duplicate column name")) throw err;
}
}
type Migration = { id: string; run: () => void };
function runMigrations(migrations: ReadonlyArray<Migration>): void {
db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at INTEGER NOT NULL
)`);
const appliedRows = db
.prepare("SELECT id FROM schema_migrations")
.all() as Array<{ id: string }>;
const applied = new Set(appliedRows.map((r) => r.id));
const markApplied = db.prepare(
"INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)",
);
for (const migration of migrations) {
if (applied.has(migration.id)) continue;
try {
migration.run();
markApplied.run(migration.id, Date.now());
logger.info(`[migration] Applied ${migration.id}`);
} catch (err) {
logger.error(`[migration] Failed: ${migration.id}`, err);
throw err;
}
}
}
export function initializeUserSchema(): void {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'operator', 'viewer')),
created_at INTEGER NOT NULL,
last_login INTEGER,
created_by TEXT,
must_change_password INTEGER DEFAULT 0,
client_scope TEXT NOT NULL DEFAULT 'none' CHECK(client_scope IN ('none', 'allowlist', 'denylist', 'all'))
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS user_client_access_rules (
user_id INTEGER NOT NULL,
client_id TEXT NOT NULL,
access TEXT NOT NULL CHECK(access IN ('allow', 'deny')),
created_at INTEGER NOT NULL,
PRIMARY KEY (user_id, client_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_user_client_access_rules_user ON user_client_access_rules(user_id)`,
);
db.exec(`
CREATE TABLE IF NOT EXISTS user_feature_permissions (
user_id INTEGER NOT NULL,
feature TEXT NOT NULL,
allowed INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (user_id, feature),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS user_plugin_access_rules (
user_id INTEGER NOT NULL,
plugin_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (user_id, plugin_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_user_plugin_access_rules_user ON user_plugin_access_rules(user_id)`,
);
const userMigrations: Migration[] = [
{
id: "001_users_must_change_password",
run: () => addColumnIfMissing("users", `must_change_password INTEGER DEFAULT 0`),
},
{
id: "002_users_client_scope",
run: () => {
addColumnIfMissing(
"users",
`client_scope TEXT NOT NULL DEFAULT 'none' CHECK(client_scope IN ('none', 'allowlist', 'denylist', 'all'))`,
);
db.exec(`UPDATE users SET client_scope='all' WHERE role='admin'`);
},
},
{
id: "003_users_can_build",
run: () => {
addColumnIfMissing("users", `can_build INTEGER NOT NULL DEFAULT 0`);
db.exec(`UPDATE users SET can_build=1 WHERE role='admin' OR role='operator'`);
},
},
{
id: "004_users_telegram_chat_id",
run: () => addColumnIfMissing("users", `telegram_chat_id TEXT DEFAULT NULL`),
},
{
id: "005_users_notification_delivery",
run: () => {
addColumnIfMissing("users", `webhook_enabled INTEGER DEFAULT 0`);
addColumnIfMissing("users", `webhook_url TEXT DEFAULT NULL`);
addColumnIfMissing("users", `webhook_template TEXT DEFAULT NULL`);
addColumnIfMissing("users", `telegram_enabled INTEGER DEFAULT 0`);
addColumnIfMissing("users", `telegram_bot_token TEXT DEFAULT NULL`);
addColumnIfMissing("users", `telegram_template TEXT DEFAULT NULL`);
},
},
{
id: "006_users_can_upload_files",
run: () => {
addColumnIfMissing("users", `can_upload_files INTEGER NOT NULL DEFAULT 0`);
db.exec(`UPDATE users SET can_upload_files=1 WHERE role='admin'`);
},
},
{
id: "007_users_client_event_filters",
run: () => {
addColumnIfMissing("users", `client_event_webhook INTEGER DEFAULT 1`);
addColumnIfMissing("users", `client_event_telegram INTEGER DEFAULT 1`);
addColumnIfMissing("users", `client_event_push INTEGER DEFAULT 1`);
},
},
{
id: "008_users_chat_write",
run: () => addColumnIfMissing("users", `chat_write INTEGER DEFAULT NULL`),
},
{
id: "009_users_registered_via",
run: () => addColumnIfMissing("users", `registered_via TEXT DEFAULT NULL`),
},
{
id: "010_users_plugin_scope",
run: () => {
addColumnIfMissing(
"users",
`plugin_scope TEXT NOT NULL DEFAULT 'none' CHECK(plugin_scope IN ('none', 'allowlist', 'all'))`,
);
db.exec(`UPDATE users SET plugin_scope='all' WHERE role='admin'`);
},
},
{
id: "011_permission_groups",
run: () => {
db.exec(`
CREATE TABLE IF NOT EXISTS permission_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
created_at INTEGER NOT NULL,
created_by INTEGER,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS permission_group_permissions (
group_id INTEGER NOT NULL,
permission TEXT NOT NULL,
PRIMARY KEY (group_id, permission),
FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS user_permission_groups (
user_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
PRIMARY KEY (user_id, group_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE
)
`);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_user_permission_groups_user ON user_permission_groups(user_id)`,
);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_permission_group_permissions_group ON permission_group_permissions(group_id)`,
);
},
},
{
id: "012_user_extra_permissions",
run: () => {
db.exec(`
CREATE TABLE IF NOT EXISTS user_extra_permissions (
user_id INTEGER NOT NULL,
permission TEXT NOT NULL,
PRIMARY KEY (user_id, permission),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_user_extra_permissions_user ON user_extra_permissions(user_id)`,
);
},
},
];
runMigrations(userMigrations);
db.exec(`
CREATE TABLE IF NOT EXISTS registration_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
label TEXT,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER,
used_by INTEGER,
used_at INTEGER,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_registration_keys_key ON registration_keys("key")`);
db.exec(`
CREATE TABLE IF NOT EXISTS pending_registrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
requested_at INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'approved', 'denied')),
reviewed_by INTEGER,
reviewed_at INTEGER,
key_used INTEGER,
FOREIGN KEY (key_used) REFERENCES registration_keys(id)
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_pending_registrations_status ON pending_registrations(status)`);
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_registrations_username_pending ON pending_registrations(username) WHERE status = 'pending'`);
}