mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-10-26 06:19:53 +00:00
db07c5c5af
Server-side session policy replaces long-lived idle console logins: sliding idle timeout, absolute max age, atomic replacement of prior sessions, IP/UA audit metadata, and consistent expired-login redirects. Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow); muopc (@muopc)
538 lines
24 KiB
JavaScript
538 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Persistent express-session store backed by the selected BetterDesk database.
|
|
* Enforces idle/absolute timeouts and one active panel session per account.
|
|
*/
|
|
|
|
const session = require('express-session');
|
|
const {
|
|
END_REASONS,
|
|
resolveSessionPolicy,
|
|
sanitizeIp,
|
|
sanitizeUserAgent,
|
|
sanitizeFingerprint
|
|
} = require('../lib/sessionPolicy');
|
|
|
|
class DatabaseSessionStore extends session.Store {
|
|
constructor({ config, ttlMs, idleTimeoutMs, maxAgeMs }) {
|
|
super();
|
|
this.config = config;
|
|
const policy = resolveSessionPolicy(process.env);
|
|
this.idleTimeoutMs = Math.max(60_000, Number(idleTimeoutMs) || policy.idleTimeoutMs);
|
|
this.maxAgeMs = Math.max(this.idleTimeoutMs, Number(maxAgeMs) || Number(ttlMs) || policy.maxAgeMs);
|
|
this.ttlMs = this.maxAgeMs;
|
|
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);
|
|
`);
|
|
await this.ensurePostgresColumns();
|
|
return;
|
|
}
|
|
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);
|
|
`);
|
|
this.ensureSqliteColumns();
|
|
}
|
|
|
|
ensureSqliteColumns() {
|
|
const cols = this.sqlite.prepare('PRAGMA table_info(panel_sessions)').all().map((c) => c.name);
|
|
const add = (name, ddl) => {
|
|
if (!cols.includes(name)) this.sqlite.exec(`ALTER TABLE panel_sessions ADD COLUMN ${ddl}`);
|
|
};
|
|
add('user_id', 'user_id INTEGER');
|
|
add('username', 'username TEXT');
|
|
add('ip_address', 'ip_address TEXT');
|
|
add('user_agent', 'user_agent TEXT');
|
|
add('client_fingerprint', 'client_fingerprint TEXT');
|
|
add('created_at', 'created_at INTEGER');
|
|
add('last_activity_at', 'last_activity_at INTEGER');
|
|
add('ended_at', 'ended_at INTEGER');
|
|
add('end_reason', 'end_reason TEXT');
|
|
add('is_active', 'is_active INTEGER NOT NULL DEFAULT 1');
|
|
this.sqlite.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_panel_sessions_user_active
|
|
ON panel_sessions (user_id, is_active);
|
|
CREATE INDEX IF NOT EXISTS idx_panel_sessions_active
|
|
ON panel_sessions (is_active, expires_at);
|
|
`);
|
|
}
|
|
|
|
async ensurePostgresColumns() {
|
|
const desired = [
|
|
['user_id', 'INTEGER'],
|
|
['username', 'TEXT'],
|
|
['ip_address', 'TEXT'],
|
|
['user_agent', 'TEXT'],
|
|
['client_fingerprint', 'TEXT'],
|
|
['created_at', 'BIGINT'],
|
|
['last_activity_at', 'BIGINT'],
|
|
['ended_at', 'BIGINT'],
|
|
['end_reason', 'TEXT'],
|
|
['is_active', 'BOOLEAN NOT NULL DEFAULT TRUE']
|
|
];
|
|
for (const [name, type] of desired) {
|
|
await this.pool.query(
|
|
`ALTER TABLE panel_sessions ADD COLUMN IF NOT EXISTS ${name} ${type}`
|
|
);
|
|
}
|
|
await this.pool.query(`
|
|
CREATE INDEX IF NOT EXISTS idx_panel_sessions_user_active
|
|
ON panel_sessions (user_id, is_active);
|
|
CREATE INDEX IF NOT EXISTS idx_panel_sessions_active
|
|
ON panel_sessions (is_active, expires_at);
|
|
`);
|
|
}
|
|
|
|
expiresAt(sess) {
|
|
const raw = sess?.cookie?.expires;
|
|
const fromCookie = raw ? new Date(raw).getTime() : NaN;
|
|
if (Number.isFinite(fromCookie)) return fromCookie;
|
|
const created = Number(sess?.createdAt) || Date.now();
|
|
const absolute = created + this.maxAgeMs;
|
|
const idle = Date.now() + this.idleTimeoutMs;
|
|
return Math.min(absolute, idle);
|
|
}
|
|
|
|
rowExpired(row, now = Date.now()) {
|
|
if (!row) return { expired: true, reason: END_REASONS.DESTROYED };
|
|
const active = this.type === 'postgres' ? row.is_active !== false : Number(row.is_active) === 1;
|
|
if (!active) {
|
|
return { expired: true, reason: row.end_reason || END_REASONS.REVOKED };
|
|
}
|
|
const created = Number(row.created_at) || 0;
|
|
const last = Number(row.last_activity_at) || created || now;
|
|
if (created && now - created > this.maxAgeMs) {
|
|
return { expired: true, reason: END_REASONS.MAX_AGE };
|
|
}
|
|
if (last && now - last > this.idleTimeoutMs) {
|
|
return { expired: true, reason: END_REASONS.IDLE };
|
|
}
|
|
const expiresAt = this.type === 'postgres'
|
|
? new Date(row.expires_at).getTime()
|
|
: Number(row.expires_at);
|
|
if (Number.isFinite(expiresAt) && expiresAt <= now) {
|
|
// Prefer idle/max classification when metadata is present.
|
|
if (created && now - created > this.maxAgeMs) {
|
|
return { expired: true, reason: END_REASONS.MAX_AGE };
|
|
}
|
|
return { expired: true, reason: END_REASONS.IDLE };
|
|
}
|
|
return { expired: false };
|
|
}
|
|
|
|
async markEnded(sid, reason) {
|
|
const endedAt = Date.now();
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(
|
|
`UPDATE panel_sessions
|
|
SET is_active = FALSE, ended_at = $2, end_reason = COALESCE(end_reason, $3), updated_at = NOW()
|
|
WHERE sid = $1 AND is_active = TRUE`,
|
|
[sid, endedAt, reason]
|
|
);
|
|
return;
|
|
}
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions
|
|
SET is_active = 0, ended_at = ?, end_reason = COALESCE(end_reason, ?), updated_at = datetime('now')
|
|
WHERE sid = ? AND is_active = 1`
|
|
).run(endedAt, reason, sid);
|
|
}
|
|
|
|
get(sid, callback) {
|
|
this.ready.then(async () => {
|
|
let row;
|
|
if (this.type === 'postgres') {
|
|
const result = await this.pool.query(
|
|
'SELECT * FROM panel_sessions WHERE sid = $1',
|
|
[sid]
|
|
);
|
|
row = result.rows[0] || null;
|
|
} else {
|
|
row = this.sqlite.prepare('SELECT * FROM panel_sessions WHERE sid = ?').get(sid) || null;
|
|
}
|
|
if (!row) {
|
|
callback(null, null);
|
|
return;
|
|
}
|
|
const check = this.rowExpired(row);
|
|
if (check.expired) {
|
|
if (this.type === 'postgres' ? row.is_active !== false : Number(row.is_active) === 1) {
|
|
await this.markEnded(sid, check.reason);
|
|
}
|
|
callback(null, null);
|
|
return;
|
|
}
|
|
callback(null, JSON.parse(row.session_json));
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
set(sid, sess, callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
const serialized = JSON.stringify(sess);
|
|
const expiresAt = this.expiresAt(sess);
|
|
const now = Date.now();
|
|
const createdAt = Number(sess?.createdAt) || now;
|
|
const lastActivity = Number(sess?.lastActivityAt) || now;
|
|
const userId = sess?.userId ?? null;
|
|
const username = sess?.user?.username || sess?.username || null;
|
|
const ip = sanitizeIp(sess?.loginIp || '');
|
|
const ua = sanitizeUserAgent(sess?.loginUserAgent || '');
|
|
const fp = sanitizeFingerprint(sess?.clientFingerprint || '');
|
|
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(`
|
|
INSERT INTO panel_sessions (
|
|
sid, session_json, expires_at, updated_at,
|
|
user_id, username, ip_address, user_agent, client_fingerprint,
|
|
created_at, last_activity_at, ended_at, end_reason, is_active
|
|
) VALUES (
|
|
$1, $2, to_timestamp($3 / 1000.0), NOW(),
|
|
$4, $5, NULLIF($6, ''), NULLIF($7, ''), NULLIF($8, ''),
|
|
$9, $10, NULL, NULL, TRUE
|
|
)
|
|
ON CONFLICT(sid) DO UPDATE SET
|
|
session_json = EXCLUDED.session_json,
|
|
expires_at = EXCLUDED.expires_at,
|
|
updated_at = NOW(),
|
|
user_id = COALESCE(EXCLUDED.user_id, panel_sessions.user_id),
|
|
username = COALESCE(EXCLUDED.username, panel_sessions.username),
|
|
ip_address = COALESCE(NULLIF(EXCLUDED.ip_address, ''), panel_sessions.ip_address),
|
|
user_agent = COALESCE(NULLIF(EXCLUDED.user_agent, ''), panel_sessions.user_agent),
|
|
client_fingerprint = COALESCE(NULLIF(EXCLUDED.client_fingerprint, ''), panel_sessions.client_fingerprint),
|
|
created_at = COALESCE(panel_sessions.created_at, EXCLUDED.created_at),
|
|
last_activity_at = EXCLUDED.last_activity_at,
|
|
is_active = CASE
|
|
WHEN panel_sessions.is_active = FALSE THEN FALSE
|
|
ELSE TRUE
|
|
END
|
|
`, [sid, serialized, expiresAt, userId, username, ip, ua, fp, createdAt, lastActivity]);
|
|
} else {
|
|
this.sqlite.prepare(`
|
|
INSERT INTO panel_sessions (
|
|
sid, session_json, expires_at, updated_at,
|
|
user_id, username, ip_address, user_agent, client_fingerprint,
|
|
created_at, last_activity_at, ended_at, end_reason, is_active
|
|
) VALUES (?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, NULL, NULL, 1)
|
|
ON CONFLICT(sid) DO UPDATE SET
|
|
session_json = excluded.session_json,
|
|
expires_at = excluded.expires_at,
|
|
updated_at = datetime('now'),
|
|
user_id = COALESCE(excluded.user_id, panel_sessions.user_id),
|
|
username = COALESCE(excluded.username, panel_sessions.username),
|
|
ip_address = COALESCE(NULLIF(excluded.ip_address, ''), panel_sessions.ip_address),
|
|
user_agent = COALESCE(NULLIF(excluded.user_agent, ''), panel_sessions.user_agent),
|
|
client_fingerprint = COALESCE(NULLIF(excluded.client_fingerprint, ''), panel_sessions.client_fingerprint),
|
|
created_at = COALESCE(panel_sessions.created_at, excluded.created_at),
|
|
last_activity_at = excluded.last_activity_at,
|
|
is_active = CASE
|
|
WHEN panel_sessions.is_active = 0 THEN 0
|
|
ELSE 1
|
|
END
|
|
`).run(sid, serialized, expiresAt, userId, username, ip || null, ua || null, fp || null, createdAt, lastActivity);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
touch(sid, sess, callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
const expiresAt = this.expiresAt(sess);
|
|
const lastActivity = Number(sess?.lastActivityAt) || Date.now();
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(
|
|
`UPDATE panel_sessions
|
|
SET expires_at = to_timestamp($2 / 1000.0),
|
|
last_activity_at = $3,
|
|
updated_at = NOW()
|
|
WHERE sid = $1 AND is_active = TRUE`,
|
|
[sid, expiresAt, lastActivity]
|
|
);
|
|
} else {
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions
|
|
SET expires_at = ?, last_activity_at = ?, updated_at = datetime('now')
|
|
WHERE sid = ? AND is_active = 1`
|
|
).run(expiresAt, lastActivity, sid);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
destroy(sid, callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
// Soft-end so history remains available for audit UIs.
|
|
await this.markEnded(sid, END_REASONS.DESTROYED);
|
|
// Keep row for history; express-session only needs it gone for get().
|
|
// Soft-end already makes get() return null. Optionally clear cookie blob.
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(
|
|
`UPDATE panel_sessions
|
|
SET session_json = '{}', end_reason = COALESCE(end_reason, $2), updated_at = NOW()
|
|
WHERE sid = $1`,
|
|
[sid, END_REASONS.DESTROYED]
|
|
);
|
|
} else {
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions
|
|
SET session_json = '{}', end_reason = COALESCE(end_reason, ?), updated_at = datetime('now')
|
|
WHERE sid = ?`
|
|
).run(END_REASONS.DESTROYED, sid);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
/**
|
|
* Atomically mark the session as the sole active session for a user.
|
|
* Previous active sessions are ended with reason "replaced".
|
|
* @returns {Promise<{ replaced: Array<{sid, ip_address, user_agent}> }>}
|
|
*/
|
|
async activateExclusiveSession(sid, meta = {}) {
|
|
await this.ready;
|
|
const now = Date.now();
|
|
const userId = meta.userId;
|
|
if (userId == null) {
|
|
throw new Error('activateExclusiveSession requires userId');
|
|
}
|
|
const username = meta.username || null;
|
|
const ip = sanitizeIp(meta.ip || '');
|
|
const ua = sanitizeUserAgent(meta.userAgent || '');
|
|
const fp = sanitizeFingerprint(meta.clientFingerprint || '');
|
|
const createdAt = Number(meta.createdAt) || now;
|
|
const lastActivity = Number(meta.lastActivityAt) || now;
|
|
const expiresAt = Math.min(createdAt + this.maxAgeMs, now + this.idleTimeoutMs);
|
|
|
|
if (this.type === 'postgres') {
|
|
const client = await this.pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
const prev = await client.query(
|
|
`UPDATE panel_sessions
|
|
SET is_active = FALSE,
|
|
ended_at = $2,
|
|
end_reason = $3,
|
|
updated_at = NOW()
|
|
WHERE user_id = $1 AND is_active = TRUE AND sid <> $4
|
|
RETURNING sid, ip_address, user_agent, username`,
|
|
[userId, now, END_REASONS.REPLACED, sid]
|
|
);
|
|
await client.query(
|
|
`UPDATE panel_sessions
|
|
SET user_id = $2,
|
|
username = $3,
|
|
ip_address = NULLIF($4, ''),
|
|
user_agent = NULLIF($5, ''),
|
|
client_fingerprint = NULLIF($6, ''),
|
|
created_at = $7,
|
|
last_activity_at = $8,
|
|
expires_at = to_timestamp($9 / 1000.0),
|
|
ended_at = NULL,
|
|
end_reason = NULL,
|
|
is_active = TRUE,
|
|
updated_at = NOW()
|
|
WHERE sid = $1`,
|
|
[sid, userId, username, ip, ua, fp, createdAt, lastActivity, expiresAt]
|
|
);
|
|
await client.query('COMMIT');
|
|
return { replaced: prev.rows || [] };
|
|
} catch (err) {
|
|
await client.query('ROLLBACK');
|
|
throw err;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
const replaceTxn = this.sqlite.transaction(() => {
|
|
const replaced = this.sqlite.prepare(
|
|
`SELECT sid, ip_address, user_agent, username
|
|
FROM panel_sessions
|
|
WHERE user_id = ? AND is_active = 1 AND sid <> ?`
|
|
).all(userId, sid);
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions
|
|
SET is_active = 0, ended_at = ?, end_reason = ?, updated_at = datetime('now')
|
|
WHERE user_id = ? AND is_active = 1 AND sid <> ?`
|
|
).run(now, END_REASONS.REPLACED, userId, sid);
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions
|
|
SET user_id = ?,
|
|
username = ?,
|
|
ip_address = ?,
|
|
user_agent = ?,
|
|
client_fingerprint = ?,
|
|
created_at = ?,
|
|
last_activity_at = ?,
|
|
expires_at = ?,
|
|
ended_at = NULL,
|
|
end_reason = NULL,
|
|
is_active = 1,
|
|
updated_at = datetime('now')
|
|
WHERE sid = ?`
|
|
).run(userId, username, ip || null, ua || null, fp || null, createdAt, lastActivity, expiresAt, sid);
|
|
return replaced;
|
|
});
|
|
return { replaced: replaceTxn() || [] };
|
|
}
|
|
|
|
async endSession(sid, reason = END_REASONS.LOGOUT) {
|
|
await this.ready;
|
|
await this.markEnded(sid, reason);
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(
|
|
`UPDATE panel_sessions SET session_json = '{}', updated_at = NOW() WHERE sid = $1`,
|
|
[sid]
|
|
);
|
|
} else {
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions SET session_json = '{}', updated_at = datetime('now') WHERE sid = ?`
|
|
).run(sid);
|
|
}
|
|
}
|
|
|
|
async getSessionRecord(sid) {
|
|
await this.ready;
|
|
if (this.type === 'postgres') {
|
|
const result = await this.pool.query('SELECT * FROM panel_sessions WHERE sid = $1', [sid]);
|
|
return result.rows[0] || null;
|
|
}
|
|
return this.sqlite.prepare('SELECT * FROM panel_sessions WHERE sid = ?').get(sid) || null;
|
|
}
|
|
|
|
async listUserSessions(userId, { includeEnded = true, limit = 50 } = {}) {
|
|
await this.ready;
|
|
const lim = Math.min(200, Math.max(1, Number(limit) || 50));
|
|
if (this.type === 'postgres') {
|
|
const result = await this.pool.query(
|
|
includeEnded
|
|
? `SELECT sid, user_id, username, ip_address, user_agent, client_fingerprint,
|
|
created_at, last_activity_at, ended_at, end_reason, is_active, expires_at
|
|
FROM panel_sessions
|
|
WHERE user_id = $1
|
|
ORDER BY COALESCE(last_activity_at, created_at) DESC NULLS LAST
|
|
LIMIT $2`
|
|
: `SELECT sid, user_id, username, ip_address, user_agent, client_fingerprint,
|
|
created_at, last_activity_at, ended_at, end_reason, is_active, expires_at
|
|
FROM panel_sessions
|
|
WHERE user_id = $1 AND is_active = TRUE
|
|
ORDER BY COALESCE(last_activity_at, created_at) DESC NULLS LAST
|
|
LIMIT $2`,
|
|
[userId, lim]
|
|
);
|
|
return result.rows;
|
|
}
|
|
return this.sqlite.prepare(
|
|
includeEnded
|
|
? `SELECT sid, user_id, username, ip_address, user_agent, client_fingerprint,
|
|
created_at, last_activity_at, ended_at, end_reason, is_active, expires_at
|
|
FROM panel_sessions
|
|
WHERE user_id = ?
|
|
ORDER BY COALESCE(last_activity_at, created_at) DESC
|
|
LIMIT ?`
|
|
: `SELECT sid, user_id, username, ip_address, user_agent, client_fingerprint,
|
|
created_at, last_activity_at, ended_at, end_reason, is_active, expires_at
|
|
FROM panel_sessions
|
|
WHERE user_id = ? AND is_active = 1
|
|
ORDER BY COALESCE(last_activity_at, created_at) DESC
|
|
LIMIT ?`
|
|
).all(userId, lim);
|
|
}
|
|
|
|
cleanup(callback = () => {}) {
|
|
this.ready.then(async () => {
|
|
const now = Date.now();
|
|
// Soft-expire stale active sessions first (keep history).
|
|
if (this.type === 'postgres') {
|
|
await this.pool.query(
|
|
`UPDATE panel_sessions
|
|
SET is_active = FALSE,
|
|
ended_at = COALESCE(ended_at, $1),
|
|
end_reason = COALESCE(end_reason, $2),
|
|
updated_at = NOW()
|
|
WHERE is_active = TRUE
|
|
AND (
|
|
expires_at <= NOW()
|
|
OR (created_at IS NOT NULL AND $1 - created_at > $3)
|
|
OR (last_activity_at IS NOT NULL AND $1 - last_activity_at > $4)
|
|
)`,
|
|
[now, END_REASONS.IDLE, this.maxAgeMs, this.idleTimeoutMs]
|
|
);
|
|
// Hard-delete only anonymous / never-authenticated expired blobs older than maxAge.
|
|
await this.pool.query(
|
|
`DELETE FROM panel_sessions
|
|
WHERE user_id IS NULL
|
|
AND expires_at <= NOW() - INTERVAL '1 day'`
|
|
);
|
|
} else {
|
|
this.sqlite.prepare(
|
|
`UPDATE panel_sessions
|
|
SET is_active = 0,
|
|
ended_at = COALESCE(ended_at, ?),
|
|
end_reason = COALESCE(end_reason, ?),
|
|
updated_at = datetime('now')
|
|
WHERE is_active = 1
|
|
AND (
|
|
expires_at <= ?
|
|
OR (created_at IS NOT NULL AND ? - created_at > ?)
|
|
OR (last_activity_at IS NOT NULL AND ? - last_activity_at > ?)
|
|
)`
|
|
).run(now, END_REASONS.IDLE, now, now, this.maxAgeMs, now, this.idleTimeoutMs);
|
|
this.sqlite.prepare(
|
|
`DELETE FROM panel_sessions
|
|
WHERE user_id IS NULL AND expires_at <= ?`
|
|
).run(now - 24 * 60 * 60 * 1000);
|
|
}
|
|
callback(null);
|
|
}).catch((err) => callback(err));
|
|
}
|
|
|
|
async close() {
|
|
await this.ready;
|
|
if (this.pool) await this.pool.end();
|
|
if (this.ownsSqlite && this.sqlite) {
|
|
this.sqlite.close();
|
|
}
|
|
this.sqlite = null;
|
|
}
|
|
}
|
|
|
|
module.exports = { DatabaseSessionStore, END_REASONS };
|