Files
hy2-panel/lib/db.ts
T

189 lines
5.0 KiB
TypeScript

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<void> | 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();
}