mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
f06fb597e1
Avoid parallel better-sqlite3 connections that race N-API cleanup hooks after event-bus connect; defer build workers until listen settles.
169 lines
6.6 KiB
JavaScript
169 lines
6.6 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Persistent express-session store backed by the selected BetterDesk database.
|
|
* It keeps console sessions revocable across process restarts without creating
|
|
* a separate auth database.
|
|
*/
|
|
|
|
const session = require('express-session');
|
|
|
|
class DatabaseSessionStore extends session.Store {
|
|
constructor({ config, ttlMs }) {
|
|
super();
|
|
this.config = config;
|
|
this.ttlMs = Math.max(60_000, Number(ttlMs) || 24 * 60 * 60 * 1000);
|
|
this.type = /^(postgres|postgresql)$/i.test(process.env.DB_TYPE || '')
|
|
|| /^postgres(ql)?:\/\//i.test(process.env.DATABASE_URL || '')
|
|
? 'postgres'
|
|
: 'sqlite';
|
|
this.sqlite = null;
|
|
this.ownsSqlite = false;
|
|
this.pool = null;
|
|
this.ready = this.initialize();
|
|
}
|
|
|
|
async initialize() {
|
|
if (this.type === 'postgres') {
|
|
const { Pool } = require('pg');
|
|
this.pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
await this.pool.query(`
|
|
CREATE TABLE IF NOT EXISTS panel_sessions (
|
|
sid TEXT PRIMARY KEY,
|
|
session_json TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_panel_sessions_expires ON panel_sessions (expires_at);
|
|
`);
|
|
return;
|
|
}
|
|
// Prefer the process-wide SQLite handle (#353) when the adapter is
|
|
// already bound to the same db path; otherwise open a private handle
|
|
// (unit tests with a temp DB). Never create the adapter singleton here.
|
|
const { getSharedSqliteMainDbIfReady } = require('./dbAdapter');
|
|
const shared = getSharedSqliteMainDbIfReady(this.config.dbPath);
|
|
if (shared) {
|
|
this.sqlite = shared;
|
|
this.ownsSqlite = false;
|
|
} else {
|
|
const Database = require('better-sqlite3');
|
|
this.sqlite = new Database(this.config.dbPath, { readonly: false, fileMustExist: false });
|
|
this.sqlite.pragma('busy_timeout = 5000');
|
|
this.sqlite.pragma('journal_mode = WAL');
|
|
this.ownsSqlite = true;
|
|
}
|
|
this.sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS panel_sessions (
|
|
sid TEXT PRIMARY KEY,
|
|
session_json TEXT NOT NULL,
|
|
expires_at INTEGER NOT NULL,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_panel_sessions_expires ON panel_sessions (expires_at);
|
|
`);
|
|
}
|
|
|
|
expiresAt(sess) {
|
|
const raw = sess?.cookie?.expires;
|
|
const fromCookie = raw ? new Date(raw).getTime() : NaN;
|
|
return Number.isFinite(fromCookie) ? fromCookie : Date.now() + this.ttlMs;
|
|
}
|
|
|
|
get(sid, callback) {
|
|
this.ready.then(async () => {
|
|
let raw;
|
|
if (this.type === 'postgres') {
|
|
const result = await this.pool.query(
|
|
'SELECT session_json FROM panel_sessions WHERE sid = $1 AND expires_at > NOW()',
|
|
[sid]
|
|
);
|
|
raw = result.rows[0]?.session_json;
|
|
} else {
|
|
raw = this.sqlite.prepare(
|
|
'SELECT session_json FROM panel_sessions WHERE sid = ? AND expires_at > ?'
|
|
).get(sid, Date.now())?.session_json;
|
|
}
|
|
callback(null, raw ? JSON.parse(raw) : null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
set(sid, sess, callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
const serialized = JSON.stringify(sess);
|
|
const expiresAt = this.expiresAt(sess);
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(`
|
|
INSERT INTO panel_sessions (sid, session_json, expires_at, updated_at)
|
|
VALUES ($1, $2, to_timestamp($3 / 1000.0), NOW())
|
|
ON CONFLICT(sid) DO UPDATE SET
|
|
session_json = EXCLUDED.session_json,
|
|
expires_at = EXCLUDED.expires_at,
|
|
updated_at = NOW()
|
|
`, [sid, serialized, expiresAt]);
|
|
} else {
|
|
this.sqlite.prepare(`
|
|
INSERT INTO panel_sessions (sid, session_json, expires_at, updated_at)
|
|
VALUES (?, ?, ?, datetime('now'))
|
|
ON CONFLICT(sid) DO UPDATE SET
|
|
session_json = excluded.session_json,
|
|
expires_at = excluded.expires_at,
|
|
updated_at = datetime('now')
|
|
`).run(sid, serialized, expiresAt);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
touch(sid, sess, callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
const expiresAt = this.expiresAt(sess);
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(
|
|
'UPDATE panel_sessions SET expires_at = to_timestamp($2 / 1000.0), updated_at = NOW() WHERE sid = $1',
|
|
[sid, expiresAt]
|
|
);
|
|
} else {
|
|
this.sqlite.prepare(
|
|
"UPDATE panel_sessions SET expires_at = ?, updated_at = datetime('now') WHERE sid = ?"
|
|
).run(expiresAt, sid);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
destroy(sid, callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query('DELETE FROM panel_sessions WHERE sid = $1', [sid]);
|
|
} else {
|
|
this.sqlite.prepare('DELETE FROM panel_sessions WHERE sid = ?').run(sid);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
cleanup(callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query('DELETE FROM panel_sessions WHERE expires_at <= NOW()');
|
|
} else {
|
|
this.sqlite.prepare('DELETE FROM panel_sessions WHERE expires_at <= ?').run(Date.now());
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
async close() {
|
|
await this.ready;
|
|
if (this.pool) await this.pool.end();
|
|
// Never close a shared adapter handle — only private test handles.
|
|
if (this.ownsSqlite && this.sqlite) {
|
|
this.sqlite.close();
|
|
}
|
|
this.sqlite = null;
|
|
}
|
|
}
|
|
|
|
module.exports = { DatabaseSessionStore };
|