modularize db code.
This commit is contained in:
+1
-2504
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);`,
|
||||||
|
);
|
||||||
|
|
||||||
@@ -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'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
import { Database } from "bun:sqlite";
|
|
||||||
import { resolve } from "path";
|
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { ensureDataDir } from "./paths";
|
import { db } from "./db/connection";
|
||||||
|
import { initializeUserSchema } from "./db/user-schema";
|
||||||
import { getConfig } from "./config";
|
import { getConfig } from "./config";
|
||||||
|
|
||||||
const dataDir = ensureDataDir();
|
|
||||||
const dbPath = resolve(dataDir, "overlord.db");
|
|
||||||
const db = new Database(dbPath);
|
|
||||||
|
|
||||||
export type UserRole = "admin" | "operator" | "viewer";
|
export type UserRole = "admin" | "operator" | "viewer";
|
||||||
export type ClientAccessScope = "none" | "allowlist" | "denylist" | "all";
|
export type ClientAccessScope = "none" | "allowlist" | "denylist" | "all";
|
||||||
export type ClientAccessRuleKind = "allow" | "deny";
|
export type ClientAccessRuleKind = "allow" | "deny";
|
||||||
@@ -72,254 +67,7 @@ function withAdminBypass<E extends any[]>(check: AccessCheck<E>): AccessCheck<E>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
db.exec(`
|
initializeUserSchema();
|
||||||
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)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 };
|
|
||||||
|
|
||||||
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)`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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'`);
|
|
||||||
|
|
||||||
|
|
||||||
const userCount = db.prepare("SELECT COUNT(*) as count FROM users").get() as {
|
const userCount = db.prepare("SELECT COUNT(*) as count FROM users").get() as {
|
||||||
count: number;
|
count: number;
|
||||||
@@ -1762,3 +1510,5 @@ export function getTotalUserCount(): number {
|
|||||||
const row = db.prepare("SELECT COUNT(*) as count FROM users").get() as { count: number };
|
const row = db.prepare("SELECT COUNT(*) as count FROM users").get() as { count: number };
|
||||||
return row.count;
|
return row.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user