233 lines
8.1 KiB
TypeScript
233 lines
8.1 KiB
TypeScript
import type Database from "better-sqlite3";
|
|
|
|
// Version 4 matches the schema already deployed on us3. Never reuse its number.
|
|
function ensureCustomNodeColumns(db: Database.Database) {
|
|
const columns = new Set(
|
|
(db.pragma("table_info(hy2_nodes)") as { name: string }[]).map(
|
|
(column) => column.name,
|
|
),
|
|
);
|
|
if (!columns.has("node_type"))
|
|
db.exec(
|
|
"ALTER TABLE hy2_nodes ADD COLUMN node_type TEXT NOT NULL DEFAULT 'hy2'",
|
|
);
|
|
if (!columns.has("custom_config_yaml"))
|
|
db.exec(
|
|
"ALTER TABLE hy2_nodes ADD COLUMN custom_config_yaml TEXT NOT NULL DEFAULT ''",
|
|
);
|
|
}
|
|
|
|
const MIGRATIONS: (string | ((db: Database.Database) => void))[] = [
|
|
`
|
|
CREATE TABLE IF NOT EXISTS admins (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
admin_id INTEGER NOT NULL,
|
|
session_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS hy2_users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
auth_id TEXT NOT NULL UNIQUE,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
expires_at TEXT,
|
|
traffic_limit_bytes INTEGER,
|
|
used_tx_bytes INTEGER NOT NULL DEFAULT 0,
|
|
used_rx_bytes INTEGER NOT NULL DEFAULT 0,
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS hy2_nodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
auth_token TEXT NOT NULL,
|
|
traffic_stats_url TEXT NOT NULL,
|
|
traffic_stats_secret TEXT NOT NULL,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
poll_interval_seconds INTEGER NOT NULL DEFAULT 15,
|
|
last_polled_at TEXT,
|
|
last_sync_ok_at TEXT,
|
|
last_error_at TEXT,
|
|
last_error_message TEXT,
|
|
last_online_users INTEGER NOT NULL DEFAULT 0,
|
|
last_stream_count INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS traffic_ledgers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
node_id INTEGER NOT NULL,
|
|
user_id INTEGER,
|
|
auth_id TEXT NOT NULL,
|
|
tx_bytes INTEGER NOT NULL,
|
|
rx_bytes INTEGER NOT NULL,
|
|
window_started_at TEXT NOT NULL,
|
|
window_ended_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS node_sync_records (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
node_id INTEGER NOT NULL,
|
|
started_at TEXT NOT NULL,
|
|
finished_at TEXT NOT NULL,
|
|
ok INTEGER NOT NULL,
|
|
traffic_entries INTEGER NOT NULL DEFAULT 0,
|
|
online_users INTEGER NOT NULL DEFAULT 0,
|
|
stream_count INTEGER NOT NULL DEFAULT 0,
|
|
kicked_auth_ids TEXT NOT NULL DEFAULT '',
|
|
error_message TEXT,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS auth_audits (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
node_id INTEGER,
|
|
user_id INTEGER,
|
|
auth_id TEXT,
|
|
username TEXT,
|
|
addr TEXT,
|
|
requested_tx INTEGER,
|
|
ok INTEGER NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS node_user_presence (
|
|
node_id INTEGER NOT NULL,
|
|
user_id INTEGER,
|
|
auth_id TEXT NOT NULL,
|
|
connections INTEGER NOT NULL DEFAULT 0,
|
|
updated_at TEXT NOT NULL,
|
|
PRIMARY KEY (node_id, auth_id),
|
|
FOREIGN KEY (node_id) REFERENCES hy2_nodes(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_hy2_users_auth_id ON hy2_users(auth_id);
|
|
CREATE INDEX IF NOT EXISTS idx_hy2_users_username ON hy2_users(username);
|
|
CREATE INDEX IF NOT EXISTS idx_hy2_nodes_enabled ON hy2_nodes(enabled);
|
|
CREATE INDEX IF NOT EXISTS idx_traffic_ledgers_auth_id ON traffic_ledgers(auth_id);
|
|
CREATE INDEX IF NOT EXISTS idx_traffic_ledgers_created_at ON traffic_ledgers(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_node_sync_records_node_id ON node_sync_records(node_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_auth_audits_created_at ON auth_audits(created_at DESC);
|
|
`,
|
|
`
|
|
ALTER TABLE hy2_users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE hy2_users ADD COLUMN auth_secret_encrypted TEXT;
|
|
ALTER TABLE hy2_users ADD COLUMN subscription_token TEXT;
|
|
ALTER TABLE hy2_users ADD COLUMN subscription_label TEXT NOT NULL DEFAULT '';
|
|
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
session_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES hy2_users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS app_settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
ALTER TABLE hy2_nodes ADD COLUMN server_host TEXT NOT NULL DEFAULT '';
|
|
ALTER TABLE hy2_nodes ADD COLUMN server_port INTEGER NOT NULL DEFAULT 443;
|
|
ALTER TABLE hy2_nodes ADD COLUMN client_name TEXT NOT NULL DEFAULT '';
|
|
ALTER TABLE hy2_nodes ADD COLUMN bandwidth_up_mbps INTEGER NOT NULL DEFAULT 30;
|
|
ALTER TABLE hy2_nodes ADD COLUMN bandwidth_down_mbps INTEGER NOT NULL DEFAULT 30;
|
|
ALTER TABLE hy2_nodes ADD COLUMN skip_cert_verify INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE hy2_nodes ADD COLUMN sni TEXT;
|
|
ALTER TABLE hy2_nodes ADD COLUMN traffic_stats_listen TEXT NOT NULL DEFAULT ':9999';
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_hy2_users_subscription_token ON hy2_users(subscription_token);
|
|
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
|
`,
|
|
`
|
|
ALTER TABLE hy2_nodes ADD COLUMN hy2_listen TEXT NOT NULL DEFAULT ':443';
|
|
`,
|
|
ensureCustomNodeColumns,
|
|
`
|
|
CREATE TABLE managed_certificates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
certificate_pem TEXT NOT NULL,
|
|
private_key_encrypted TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE node_installations (
|
|
id TEXT PRIMARY KEY,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
token_encrypted TEXT NOT NULL,
|
|
certificate_id INTEGER NOT NULL REFERENCES managed_certificates(id) ON DELETE RESTRICT,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
server_host TEXT NOT NULL,
|
|
sni TEXT NOT NULL,
|
|
origin TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
revoked_at TEXT,
|
|
completed_at TEXT,
|
|
ports_json TEXT,
|
|
node_id INTEGER REFERENCES hy2_nodes(id) ON DELETE CASCADE
|
|
);
|
|
`,
|
|
`
|
|
ALTER TABLE node_installations ADD COLUMN protocol TEXT NOT NULL DEFAULT 'hy2';
|
|
CREATE TABLE node_agent_counters (
|
|
node_id INTEGER NOT NULL REFERENCES hy2_nodes(id) ON DELETE CASCADE,
|
|
epoch TEXT NOT NULL,
|
|
auth_id TEXT NOT NULL,
|
|
tx INTEGER NOT NULL DEFAULT 0,
|
|
rx INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY(node_id, epoch, auth_id)
|
|
);
|
|
`,
|
|
];
|
|
|
|
export function migrateDatabase(db: Database.Database) {
|
|
// Read the version under the same write lock as the migration to avoid races.
|
|
db.transaction(() => {
|
|
const currentVersion = db.pragma("user_version", {
|
|
simple: true,
|
|
}) as number;
|
|
if (currentVersion > MIGRATIONS.length)
|
|
throw new Error("数据库版本高于当前程序,请使用匹配的程序版本。");
|
|
for (let index = currentVersion; index < MIGRATIONS.length; index += 1) {
|
|
const migration = MIGRATIONS[index];
|
|
if (typeof migration === "string") db.exec(migration);
|
|
else migration(db);
|
|
db.pragma(`user_version = ${index + 1}`);
|
|
}
|
|
// Also repairs a partially applied v4 without rewriting any existing data.
|
|
ensureCustomNodeColumns(db);
|
|
}).immediate();
|
|
}
|