hy2-panel/lib/store.ts
2026-04-14 18:05:32 +08:00

1063 lines
26 KiB
TypeScript

import type Database from "better-sqlite3";
import { encryptText } from "@/lib/crypto";
import { ensureDb, getDb } from "@/lib/db";
import { getEnv } from "@/lib/env";
import { createOpaqueToken, createStableId, hashPassword } from "@/lib/password";
export type Hy2UserRow = {
id: number;
auth_id: string;
username: string;
password_hash: string;
auth_secret_encrypted: string | null;
enabled: number;
is_admin: number;
expires_at: string | null;
traffic_limit_bytes: number | null;
used_tx_bytes: number;
used_rx_bytes: number;
notes: string;
subscription_token: string | null;
subscription_label: string;
created_at: string;
updated_at: string;
};
export type Hy2NodeRow = {
id: number;
slug: string;
name: string;
auth_token: string;
hy2_listen: string;
traffic_stats_url: string;
traffic_stats_secret: string;
traffic_stats_listen: string;
server_host: string;
server_port: number;
client_name: string;
bandwidth_up_mbps: number;
bandwidth_down_mbps: number;
skip_cert_verify: number;
sni: string | null;
enabled: number;
poll_interval_seconds: number;
last_polled_at: string | null;
last_sync_ok_at: string | null;
last_error_at: string | null;
last_error_message: string | null;
last_online_users: number;
last_stream_count: number;
created_at: string;
updated_at: string;
};
export type ProxyGroupConfig = {
id: string;
name: string;
type: "select";
memberSlugs: string[];
};
type AuditRecordInput = {
nodeId?: number | null;
userId?: number | null;
authId?: string | null;
username?: string | null;
addr?: string | null;
requestedTx?: number | null;
ok: boolean;
reason: string;
};
function dbNow() {
return new Date().toISOString();
}
function asBoolean(value: number) {
return value === 1;
}
export function formatBytes(bytes: number | null | undefined) {
if (bytes == null) return "0 B";
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB", "TB", "PB"];
let value = bytes / 1024;
let unit = units[0];
for (let index = 1; index < units.length && value >= 1024; index += 1) {
value /= 1024;
unit = units[index];
}
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${unit}`;
}
export function toBytesFromGiB(input: string | null | undefined) {
if (!input) return null;
const value = Number(input);
if (!Number.isFinite(value) || value <= 0) return null;
return Math.round(value * 1024 * 1024 * 1024);
}
export function toDatetimeLocalValue(value: string | null) {
if (!value) return "";
return value.slice(0, 16);
}
export function buildNodeAuthUrl(slug: string, token: string, origin = getEnv().APP_URL) {
return `${origin}/api/hy2/nodes/${encodeURIComponent(slug)}/auth?token=${encodeURIComponent(token)}`;
}
export function buildTrafficStatsUrl(serverHost: string, listen: string) {
const portMatch = listen.match(/:?(\d{2,5})$/);
const port = portMatch?.[1] ?? "9999";
return `http://${serverHost}:${port}`;
}
export function getUserLifecycleState(user: {
enabled: number;
expires_at: string | null;
traffic_limit_bytes: number | null;
used_tx_bytes: number;
used_rx_bytes: number;
}) {
if (!asBoolean(user.enabled)) {
return { ok: false, reason: "user_disabled" };
}
if (user.expires_at && new Date(user.expires_at).getTime() <= Date.now()) {
return { ok: false, reason: "user_expired" };
}
const total = user.used_tx_bytes + user.used_rx_bytes;
if (
user.traffic_limit_bytes != null &&
Number.isFinite(user.traffic_limit_bytes) &&
total >= user.traffic_limit_bytes
) {
return { ok: false, reason: "user_quota_exceeded" };
}
return { ok: true, reason: "ok" };
}
export async function getAdminByUsername(username: string) {
const db = await ensureDb();
return db
.prepare("SELECT * FROM admins WHERE username = ?")
.get(username) as
| {
id: number;
username: string;
password_hash: string;
}
| undefined;
}
export async function listUsers() {
const db = await ensureDb();
return db
.prepare(
`
SELECT
u.*,
COALESCE(SUM(p.connections), 0) AS online_connections
FROM hy2_users u
LEFT JOIN node_user_presence p ON p.user_id = u.id
GROUP BY u.id
ORDER BY u.created_at DESC
`,
)
.all() as Array<Hy2UserRow & { online_connections: number }>;
}
export async function getUserByUsername(username: string) {
const db = await ensureDb();
return db
.prepare("SELECT * FROM hy2_users WHERE username = ?")
.get(username) as Hy2UserRow | undefined;
}
export async function getUserById(id: number) {
const db = await ensureDb();
return db
.prepare("SELECT * FROM hy2_users WHERE id = ?")
.get(id) as Hy2UserRow | undefined;
}
export async function getUserByAuthId(authId: string) {
const db = await ensureDb();
return db
.prepare("SELECT * FROM hy2_users WHERE auth_id = ?")
.get(authId) as Hy2UserRow | undefined;
}
export async function getUserBySubscriptionToken(token: string) {
const db = await ensureDb();
return db
.prepare("SELECT * FROM hy2_users WHERE subscription_token = ?")
.get(token) as Hy2UserRow | undefined;
}
export async function getAdminMirrorUser() {
const db = await ensureDb();
return db
.prepare(
`
SELECT *
FROM hy2_users
WHERE is_admin = 1 OR username = ?
ORDER BY is_admin DESC, id ASC
LIMIT 1
`,
)
.get(getEnv().ADMIN_USERNAME) as Hy2UserRow | undefined;
}
export async function createUser(input: {
username: string;
password: string;
enabled: boolean;
isAdmin?: boolean;
expiresAt: string | null;
trafficLimitBytes: number | null;
notes: string;
}) {
const db = await ensureDb();
const now = dbNow();
const passwordHash = await hashPassword(input.password);
const authId = createStableId();
db.prepare(
`
INSERT INTO hy2_users (
auth_id, username, password_hash, auth_secret_encrypted, enabled, is_admin,
expires_at, traffic_limit_bytes, notes, subscription_token, subscription_label,
created_at, updated_at
)
VALUES (
@authId, @username, @passwordHash, @authSecretEncrypted, @enabled, @isAdmin,
@expiresAt, @trafficLimitBytes, @notes, @subscriptionToken, @subscriptionLabel,
@now, @now
)
`,
).run({
authId,
username: input.username,
passwordHash,
authSecretEncrypted: encryptText(input.password),
enabled: input.enabled ? 1 : 0,
isAdmin: input.isAdmin ? 1 : 0,
expiresAt: input.expiresAt,
trafficLimitBytes: input.trafficLimitBytes,
notes: input.notes,
subscriptionToken: createOpaqueToken(),
subscriptionLabel: input.username,
now,
});
}
export async function updateUser(input: {
id: number;
username: string;
enabled: boolean;
subscriptionLabel: string;
expiresAt: string | null;
trafficLimitBytes: number | null;
notes: string;
}) {
const db = await ensureDb();
db.prepare(
`
UPDATE hy2_users
SET
username = @username,
enabled = @enabled,
subscription_label = @subscriptionLabel,
expires_at = @expiresAt,
traffic_limit_bytes = @trafficLimitBytes,
notes = @notes,
updated_at = @updatedAt
WHERE id = @id
`,
).run({
id: input.id,
username: input.username,
enabled: input.enabled ? 1 : 0,
subscriptionLabel: input.subscriptionLabel,
expiresAt: input.expiresAt,
trafficLimitBytes: input.trafficLimitBytes,
notes: input.notes,
updatedAt: dbNow(),
});
}
export async function deleteUser(id: number) {
const db = await ensureDb();
db.prepare("DELETE FROM hy2_users WHERE id = ?").run(id);
}
export async function resetUserPassword(id: number, password: string) {
const db = await ensureDb();
const passwordHash = await hashPassword(password);
db.prepare(
`
UPDATE hy2_users
SET password_hash = ?, auth_secret_encrypted = ?, updated_at = ?
WHERE id = ?
`,
).run(passwordHash, encryptText(password), dbNow(), id);
}
export async function listNodes() {
const db = await ensureDb();
return db
.prepare(
`
SELECT *
FROM hy2_nodes
ORDER BY created_at DESC
`,
)
.all() as Hy2NodeRow[];
}
export async function getNodeBySlug(slug: string) {
const db = await ensureDb();
return db
.prepare("SELECT * FROM hy2_nodes WHERE slug = ?")
.get(slug) as Hy2NodeRow | undefined;
}
export async function createNode(input: {
slug: string;
name: string;
authToken: string | null;
hy2Listen: string;
serverHost: string;
serverPort: number;
clientName: string;
bandwidthUpMbps: number;
bandwidthDownMbps: number;
skipCertVerify: boolean;
sni: string | null;
trafficStatsUrl: string;
trafficStatsListen: string;
trafficStatsSecret: string;
enabled: boolean;
pollIntervalSeconds: number;
}) {
const db = await ensureDb();
const now = dbNow();
db.prepare(
`
INSERT INTO hy2_nodes (
slug, name, auth_token, hy2_listen, server_host, server_port, client_name,
bandwidth_up_mbps, bandwidth_down_mbps, skip_cert_verify, sni,
traffic_stats_url, traffic_stats_listen, traffic_stats_secret, enabled,
poll_interval_seconds, created_at, updated_at
)
VALUES (
@slug, @name, @authToken, @hy2Listen, @serverHost, @serverPort, @clientName,
@bandwidthUpMbps, @bandwidthDownMbps, @skipCertVerify, @sni,
@trafficStatsUrl, @trafficStatsListen, @trafficStatsSecret, @enabled,
@pollIntervalSeconds, @now, @now
)
`,
).run({
slug: input.slug,
name: input.name,
authToken: input.authToken || createOpaqueToken(),
hy2Listen: input.hy2Listen || `:${input.serverPort}`,
serverHost: input.serverHost,
serverPort: input.serverPort,
clientName: input.clientName,
bandwidthUpMbps: input.bandwidthUpMbps,
bandwidthDownMbps: input.bandwidthDownMbps,
skipCertVerify: input.skipCertVerify ? 1 : 0,
sni: input.sni,
trafficStatsUrl:
input.trafficStatsUrl || buildTrafficStatsUrl(input.serverHost, input.trafficStatsListen),
trafficStatsListen: input.trafficStatsListen,
trafficStatsSecret: input.trafficStatsSecret || createOpaqueToken(),
enabled: input.enabled ? 1 : 0,
pollIntervalSeconds: input.pollIntervalSeconds,
now,
});
}
export async function updateNode(input: {
id: number;
slug: string;
name: string;
authToken: string;
hy2Listen: string;
serverHost: string;
serverPort: number;
clientName: string;
bandwidthUpMbps: number;
bandwidthDownMbps: number;
skipCertVerify: boolean;
sni: string | null;
trafficStatsUrl: string;
trafficStatsListen: string;
trafficStatsSecret: string;
enabled: boolean;
pollIntervalSeconds: number;
}) {
const db = await ensureDb();
db.prepare(
`
UPDATE hy2_nodes
SET
slug = @slug,
name = @name,
auth_token = @authToken,
hy2_listen = @hy2Listen,
server_host = @serverHost,
server_port = @serverPort,
client_name = @clientName,
bandwidth_up_mbps = @bandwidthUpMbps,
bandwidth_down_mbps = @bandwidthDownMbps,
skip_cert_verify = @skipCertVerify,
sni = @sni,
traffic_stats_url = @trafficStatsUrl,
traffic_stats_listen = @trafficStatsListen,
traffic_stats_secret = CASE
WHEN @trafficStatsSecret = '' THEN traffic_stats_secret
ELSE @trafficStatsSecret
END,
enabled = @enabled,
poll_interval_seconds = @pollIntervalSeconds,
updated_at = @updatedAt
WHERE id = @id
`,
).run({
...input,
hy2Listen: input.hy2Listen || `:${input.serverPort}`,
trafficStatsUrl:
input.trafficStatsUrl || buildTrafficStatsUrl(input.serverHost, input.trafficStatsListen),
enabled: input.enabled ? 1 : 0,
skipCertVerify: input.skipCertVerify ? 1 : 0,
updatedAt: dbNow(),
});
}
export async function deleteNode(id: number) {
const db = await ensureDb();
db.prepare("DELETE FROM hy2_nodes WHERE id = ?").run(id);
}
export async function listAudits(limit = 100) {
const db = await ensureDb();
return db
.prepare(
`
SELECT
a.*,
n.name AS node_name
FROM auth_audits a
LEFT JOIN hy2_nodes n ON n.id = a.node_id
ORDER BY a.created_at DESC
LIMIT ?
`,
)
.all(limit) as Array<{
id: number;
node_name: string | null;
auth_id: string | null;
username: string | null;
addr: string | null;
requested_tx: number | null;
ok: number;
reason: string;
created_at: string;
}>;
}
export async function getDashboardMetrics() {
const db = await ensureDb();
const totals = db
.prepare(
`
SELECT
(SELECT COUNT(*) FROM hy2_users) AS total_users,
(SELECT COUNT(*) FROM hy2_users WHERE enabled = 1) AS enabled_users,
(SELECT COUNT(*) FROM hy2_users WHERE is_admin = 1) AS admin_users,
(SELECT COUNT(*) FROM hy2_nodes WHERE enabled = 1) AS enabled_nodes,
(SELECT COUNT(*) FROM node_user_presence WHERE connections > 0) AS active_presence_rows,
(SELECT COALESCE(SUM(connections), 0) FROM node_user_presence WHERE connections > 0) AS total_online_connections,
(SELECT COALESCE(SUM(used_tx_bytes + used_rx_bytes), 0) FROM hy2_users) AS total_traffic
`,
)
.get() as {
total_users: number;
enabled_users: number;
admin_users: number;
enabled_nodes: number;
active_presence_rows: number;
total_online_connections: number;
total_traffic: number;
};
const topUsers = db
.prepare(
`
SELECT
username,
auth_id,
used_tx_bytes,
used_rx_bytes,
(used_tx_bytes + used_rx_bytes) AS total_bytes
FROM hy2_users
ORDER BY total_bytes DESC
LIMIT 8
`,
)
.all() as Array<{
username: string;
auth_id: string;
used_tx_bytes: number;
used_rx_bytes: number;
total_bytes: number;
}>;
const nodeHealth = db
.prepare(
`
SELECT
id,
name,
slug,
enabled,
last_sync_ok_at,
last_error_at,
last_error_message,
last_online_users,
last_stream_count
FROM hy2_nodes
ORDER BY name ASC
`,
)
.all() as Array<{
id: number;
name: string;
slug: string;
enabled: number;
last_sync_ok_at: string | null;
last_error_at: string | null;
last_error_message: string | null;
last_online_users: number;
last_stream_count: number;
}>;
const recentFailures = db
.prepare(
`
SELECT
a.created_at,
a.reason,
a.username,
a.addr,
n.name AS node_name
FROM auth_audits a
LEFT JOIN hy2_nodes n ON n.id = a.node_id
WHERE a.ok = 0
ORDER BY a.created_at DESC
LIMIT 12
`,
)
.all() as Array<{
created_at: string;
reason: string;
username: string | null;
addr: string | null;
node_name: string | null;
}>;
const recentUsage = db
.prepare(
`
SELECT
l.created_at,
l.auth_id,
l.tx_bytes,
l.rx_bytes,
u.username,
n.name AS node_name
FROM traffic_ledgers l
LEFT JOIN hy2_users u ON u.id = l.user_id
LEFT JOIN hy2_nodes n ON n.id = l.node_id
ORDER BY l.created_at DESC
LIMIT 20
`,
)
.all() as Array<{
created_at: string;
auth_id: string;
tx_bytes: number;
rx_bytes: number;
username: string | null;
node_name: string | null;
}>;
const syncErrors = db
.prepare(
`
SELECT
r.created_at,
r.error_message,
n.name AS node_name
FROM node_sync_records r
INNER JOIN hy2_nodes n ON n.id = r.node_id
WHERE r.ok = 0
ORDER BY r.created_at DESC
LIMIT 12
`,
)
.all() as Array<{
created_at: string;
error_message: string | null;
node_name: string;
}>;
return {
totals,
topUsers,
nodeHealth,
recentFailures,
recentUsage,
syncErrors,
};
}
export async function createAdminSessionRecord(input: {
id: string;
adminId: number;
sessionHash: string;
expiresAt: string;
}) {
const db = await ensureDb();
const now = dbNow();
db.prepare(
`
INSERT INTO admin_sessions (id, admin_id, session_hash, expires_at, created_at, last_seen_at)
VALUES (?, ?, ?, ?, ?, ?)
`,
).run(input.id, input.adminId, input.sessionHash, input.expiresAt, now, now);
}
export async function getAdminSessionByHash(sessionHash: string) {
const db = await ensureDb();
return db
.prepare(
`
SELECT
s.id,
s.admin_id,
s.expires_at,
s.last_seen_at,
a.username
FROM admin_sessions s
INNER JOIN admins a ON a.id = s.admin_id
WHERE s.session_hash = ?
`,
)
.get(sessionHash) as
| {
id: string;
admin_id: number;
username: string;
expires_at: string;
last_seen_at: string;
}
| undefined;
}
export async function touchAdminSession(sessionId: string) {
const db = await ensureDb();
db.prepare("UPDATE admin_sessions SET last_seen_at = ? WHERE id = ?").run(
dbNow(),
sessionId,
);
}
export async function createUserSessionRecord(input: {
id: string;
userId: number;
sessionHash: string;
expiresAt: string;
}) {
const db = await ensureDb();
const now = dbNow();
db.prepare(
`
INSERT INTO user_sessions (id, user_id, session_hash, expires_at, created_at, last_seen_at)
VALUES (?, ?, ?, ?, ?, ?)
`,
).run(input.id, input.userId, input.sessionHash, input.expiresAt, now, now);
}
export async function getUserSessionByHash(sessionHash: string) {
const db = await ensureDb();
return db
.prepare(
`
SELECT
s.id,
s.user_id,
s.expires_at,
s.last_seen_at,
u.username,
u.is_admin
FROM user_sessions s
INNER JOIN hy2_users u ON u.id = s.user_id
WHERE s.session_hash = ?
`,
)
.get(sessionHash) as
| {
id: string;
user_id: number;
username: string;
is_admin: number;
expires_at: string;
last_seen_at: string;
}
| undefined;
}
export async function touchUserSession(sessionId: string) {
const db = await ensureDb();
db.prepare("UPDATE user_sessions SET last_seen_at = ? WHERE id = ?").run(
dbNow(),
sessionId,
);
}
export async function deleteUserSessionByHash(sessionHash: string) {
const db = await ensureDb();
db.prepare("DELETE FROM user_sessions WHERE session_hash = ?").run(sessionHash);
}
export async function deleteAdminSessionByHash(sessionHash: string) {
const db = await ensureDb();
db.prepare("DELETE FROM admin_sessions WHERE session_hash = ?").run(sessionHash);
}
export async function getSubscriptionSettings() {
const db = await ensureDb();
const rows = db
.prepare("SELECT key, value FROM app_settings")
.all() as Array<{ key: string; value: string }>;
const map = new Map(rows.map((row) => [row.key, row.value]));
return {
template: map.get("clash_template") ?? "",
extraProxyGroups: map.get("extra_proxy_groups") ?? "",
profileName: map.get("profile_name") ?? "FeieProxy",
proxyGroups: JSON.parse(map.get("proxy_groups_json") ?? "[]") as ProxyGroupConfig[],
};
}
export async function saveSubscriptionSettings(input: {
template: string;
extraProxyGroups: string;
profileName: string;
}) {
const db = await ensureDb();
const upsert = db.prepare(
`
INSERT INTO app_settings (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at
`,
);
const now = dbNow();
upsert.run("clash_template", input.template, now);
upsert.run("extra_proxy_groups", input.extraProxyGroups, now);
upsert.run("profile_name", input.profileName, now);
}
export async function saveProxyGroups(proxyGroups: ProxyGroupConfig[]) {
const db = await ensureDb();
const now = dbNow();
db.prepare(
`
INSERT INTO app_settings (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at
`,
).run("proxy_groups_json", JSON.stringify(proxyGroups), now);
}
export async function listSubscriptionNodes() {
const db = await ensureDb();
return db
.prepare(
`
SELECT *
FROM hy2_nodes
WHERE enabled = 1
ORDER BY created_at DESC
`,
)
.all() as Hy2NodeRow[];
}
export async function writeAuthAudit(input: AuditRecordInput) {
const db = await ensureDb();
db.prepare(
`
INSERT INTO auth_audits (
node_id, user_id, auth_id, username, addr, requested_tx, ok, reason, created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
).run(
input.nodeId ?? null,
input.userId ?? null,
input.authId ?? null,
input.username ?? null,
input.addr ?? null,
input.requestedTx ?? null,
input.ok ? 1 : 0,
input.reason,
dbNow(),
);
}
export async function listEnabledNodesForPolling() {
const db = await ensureDb();
return db
.prepare(
`
SELECT *
FROM hy2_nodes
WHERE enabled = 1
ORDER BY id ASC
`,
)
.all() as Hy2NodeRow[];
}
export async function shouldPollNode(nodeId: number, intervalSeconds: number) {
const db = await ensureDb();
const row = db
.prepare("SELECT last_polled_at FROM hy2_nodes WHERE id = ?")
.get(nodeId) as { last_polled_at: string | null } | undefined;
if (!row?.last_polled_at) {
return true;
}
return Date.now() - new Date(row.last_polled_at).getTime() >= intervalSeconds * 1000;
}
export function applyNodeSync(
db: Database.Database,
input: {
node: Hy2NodeRow;
startedAt: string;
finishedAt: string;
traffic: Record<string, { tx: number; rx: number }>;
online: Record<string, number>;
streamCount: number;
kickedAuthIds: string[];
},
) {
const authIds = new Set([
...Object.keys(input.traffic),
...Object.keys(input.online),
...input.kickedAuthIds,
]);
const lookupUsers = db.prepare(
`
SELECT id, auth_id
FROM hy2_users
WHERE auth_id IN (${Array.from(authIds)
.map(() => "?")
.join(",") || "''"})
`,
);
const users = authIds.size
? (lookupUsers.all(...Array.from(authIds)) as Array<{ id: number; auth_id: string }>)
: [];
const userMap = new Map(users.map((user) => [user.auth_id, user.id]));
const now = input.finishedAt;
const insertLedger = db.prepare(
`
INSERT INTO traffic_ledgers (
node_id, user_id, auth_id, tx_bytes, rx_bytes, window_started_at, window_ended_at, created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
);
const updateUserTraffic = db.prepare(
`
UPDATE hy2_users
SET
used_tx_bytes = used_tx_bytes + ?,
used_rx_bytes = used_rx_bytes + ?,
updated_at = ?
WHERE auth_id = ?
`,
);
for (const [authId, counters] of Object.entries(input.traffic)) {
insertLedger.run(
input.node.id,
userMap.get(authId) ?? null,
authId,
counters.tx,
counters.rx,
input.startedAt,
input.finishedAt,
now,
);
updateUserTraffic.run(counters.tx, counters.rx, now, authId);
}
db.prepare(
`
UPDATE node_user_presence
SET connections = 0, updated_at = ?
WHERE node_id = ?
`,
).run(now, input.node.id);
const upsertPresence = db.prepare(
`
INSERT INTO node_user_presence (node_id, user_id, auth_id, connections, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(node_id, auth_id) DO UPDATE SET
user_id = excluded.user_id,
connections = excluded.connections,
updated_at = excluded.updated_at
`,
);
for (const [authId, connections] of Object.entries(input.online)) {
upsertPresence.run(
input.node.id,
userMap.get(authId) ?? null,
authId,
connections,
now,
);
}
db.prepare(
`
UPDATE hy2_nodes
SET
last_polled_at = ?,
last_sync_ok_at = ?,
last_error_at = NULL,
last_error_message = NULL,
last_online_users = ?,
last_stream_count = ?,
updated_at = ?
WHERE id = ?
`,
).run(
now,
now,
Object.keys(input.online).length,
input.streamCount,
now,
input.node.id,
);
db.prepare(
`
INSERT INTO node_sync_records (
node_id, started_at, finished_at, ok, traffic_entries, online_users, stream_count,
kicked_auth_ids, error_message, created_at
)
VALUES (?, ?, ?, 1, ?, ?, ?, ?, NULL, ?)
`,
).run(
input.node.id,
input.startedAt,
input.finishedAt,
Object.keys(input.traffic).length,
Object.keys(input.online).length,
input.streamCount,
input.kickedAuthIds.join(","),
now,
);
}
export async function writeNodeSyncError(nodeId: number, startedAt: string, errorMessage: string) {
const db = await ensureDb();
const now = dbNow();
db.prepare(
`
UPDATE hy2_nodes
SET
last_polled_at = ?,
last_error_at = ?,
last_error_message = ?,
updated_at = ?
WHERE id = ?
`,
).run(now, now, errorMessage, now, nodeId);
db.prepare(
`
INSERT INTO node_sync_records (
node_id, started_at, finished_at, ok, traffic_entries, online_users, stream_count,
kicked_auth_ids, error_message, created_at
)
VALUES (?, ?, ?, 0, 0, 0, 0, '', ?, ?)
`,
).run(nodeId, startedAt, now, errorMessage, now);
}
export async function getKickCandidates(authIds: string[]) {
if (authIds.length === 0) {
return [];
}
const db = await ensureDb();
const placeholders = authIds.map(() => "?").join(",");
const rows = db
.prepare(
`
SELECT *
FROM hy2_users
WHERE auth_id IN (${placeholders})
`,
)
.all(...authIds) as Hy2UserRow[];
const map = new Map(rows.map((row) => [row.auth_id, row]));
return authIds.filter((authId) => {
const user = map.get(authId);
if (!user) return true;
return !getUserLifecycleState(user).ok;
});
}
export async function runInTransaction<T>(callback: (db: Database.Database) => T) {
const db = await ensureDb();
const transaction = db.transaction(callback);
return transaction(getDb());
}