import Database from "better-sqlite3"; import { migrateDatabase } from "@/lib/db-migrations"; import fs from "node:fs"; import path from "node:path"; import { encryptText } from "@/lib/crypto"; import { getEnv } from "@/lib/env"; import { createOpaqueToken, createStableId, hashPassword, } from "@/lib/password"; declare global { var __hy2PanelDb: Database.Database | undefined; var __hy2PanelDbInit: Promise | undefined; } function createDbInstance() { const env = getEnv(); const filePath = path.isAbsolute(env.DATABASE_PATH) ? env.DATABASE_PATH : path.join(/* turbopackIgnore: true */ process.cwd(), env.DATABASE_PATH); fs.mkdirSync(path.dirname(filePath), { recursive: true }); const db = new Database(filePath); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); return db; } async function initializeDb(db: Database.Database) { migrateDatabase(db); const env = getEnv(); const now = new Date().toISOString(); const passwordHash = await hashPassword(env.ADMIN_PASSWORD); db.prepare( ` INSERT INTO admins (id, username, password_hash, created_at, updated_at) VALUES (1, @username, @passwordHash, @now, @now) ON CONFLICT(id) DO UPDATE SET username = excluded.username, password_hash = excluded.password_hash, updated_at = excluded.updated_at `, ).run({ username: env.ADMIN_USERNAME, passwordHash, now, }); const templatePath = path.join(process.cwd(), "app", "config", "config.yaml"); const defaultTemplate = fs.readFileSync(templatePath, "utf8"); const settings = db.prepare( ` INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO NOTHING `, ); settings.run("clash_template", defaultTemplate, now); settings.run("extra_proxy_groups", "", now); settings.run("profile_name", "FeieProxy", now); settings.run("main_proxy_group_member_slugs_json", "[]", now); settings.run("proxy_groups_json", "[]", now); const existingAdminUser = db .prepare( ` SELECT id, auth_id, subscription_token FROM hy2_users WHERE is_admin = 1 OR username = ? ORDER BY is_admin DESC, id ASC LIMIT 1 `, ) .get(env.ADMIN_USERNAME) as | { id: number; auth_id: string; subscription_token: string | null } | undefined; if (existingAdminUser) { db.prepare( ` UPDATE hy2_users SET username = @username, password_hash = @passwordHash, auth_secret_encrypted = @secret, is_admin = 1, enabled = 1, expires_at = NULL, traffic_limit_bytes = NULL, subscription_token = COALESCE(subscription_token, @subscriptionToken), subscription_label = CASE WHEN subscription_label = '' THEN @username ELSE subscription_label END, updated_at = @now WHERE id = @id `, ).run({ id: existingAdminUser.id, username: env.ADMIN_USERNAME, passwordHash, secret: encryptText(env.ADMIN_PASSWORD), subscriptionToken: existingAdminUser.subscription_token ?? createOpaqueToken(), now, }); } else { db.prepare( ` INSERT INTO hy2_users ( auth_id, username, password_hash, enabled, expires_at, traffic_limit_bytes, used_tx_bytes, used_rx_bytes, notes, is_admin, auth_secret_encrypted, subscription_token, subscription_label, created_at, updated_at ) VALUES ( @authId, @username, @passwordHash, 1, NULL, NULL, 0, 0, 'System administrator account', 1, @secret, @subscriptionToken, @username, @now, @now ) `, ).run({ authId: `admin-${createStableId()}`, username: env.ADMIN_USERNAME, passwordHash, secret: encryptText(env.ADMIN_PASSWORD), subscriptionToken: createOpaqueToken(), now, }); } const usersMissingTokens = db .prepare( "SELECT id, username FROM hy2_users WHERE subscription_token IS NULL", ) .all() as Array<{ id: number; username: string }>; const updateUserToken = db.prepare( ` UPDATE hy2_users SET subscription_token = ?, subscription_label = CASE WHEN subscription_label = '' THEN ? ELSE subscription_label END, updated_at = ? WHERE id = ? `, ); for (const user of usersMissingTokens) { updateUserToken.run(createOpaqueToken(), user.username, now, user.id); } db.prepare( ` UPDATE hy2_users SET subscription_label = username, updated_at = ? WHERE subscription_label = '' `, ).run(now); } export function getDb() { if (!global.__hy2PanelDb) { global.__hy2PanelDb = createDbInstance(); } return global.__hy2PanelDb; } export async function ensureDb() { if (!global.__hy2PanelDbInit) { global.__hy2PanelDbInit = initializeDb(getDb()); } await global.__hy2PanelDbInit; return getDb(); }