"use strict"; const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); function hashToken(token) { return crypto.createHash("sha256").update(String(token)).digest("hex"); } class SessionStore { /** * @param {Object} options * @param {string} options.file - Путь к sessions.json * @param {number} options.ttlMs - Время жизни сессии в мс * @param {boolean} [options.persist=true] - Сохранять ли на диск * @param {import('../../logger').Logger} [options.logger] */ constructor({ file, ttlMs = 7 * 24 * 60 * 60 * 1000, persist = true, logger = null }) { this.file = file; this.ttlMs = ttlMs; this.persist = persist; this.logger = logger; /** @type {Map} */ this.sessions = new Map(); // key: hash(token) this._persistTimer = null; this._sweepInterval = null; if (this.persist && this.file) { this._load(); } this._sweepInterval = setInterval(() => this.sweep(), 60 * 60 * 1000); if (this._sweepInterval.unref) { this._sweepInterval.unref(); } } _load() { try { if (!fs.existsSync(this.file)) return; const raw = fs.readFileSync(this.file, "utf8"); if (!raw.trim()) return; const data = JSON.parse(raw); const now = Date.now(); let loaded = 0; for (const [tokenHash, session] of Object.entries(data)) { if (session && session.expiresAt && session.expiresAt > now && session.user) { this.sessions.set(tokenHash, { user: session.user, expiresAt: session.expiresAt, createdAt: session.createdAt || now, }); loaded++; } } if (this.logger) { this.logger.debug("Сессии загружены с диска", { count: loaded }); } } catch (err) { if (this.logger) { this.logger.warn("Ошибка при загрузке sessions.json, инициализация пустого хранилища", { error: err.message, }); } } } create(user) { const rawToken = crypto.randomBytes(32).toString("hex"); const tokenHash = hashToken(rawToken); const now = Date.now(); const session = { user: String(user || "admin"), expiresAt: now + this.ttlMs, createdAt: now, }; this.sessions.set(tokenHash, session); this._schedulePersist(); return rawToken; } get(token) { if (!token) return null; const tokenHash = hashToken(token); const session = this.sessions.get(tokenHash); if (!session) return null; const now = Date.now(); if (session.expiresAt <= now) { this.sessions.delete(tokenHash); this._schedulePersist(); return null; } // Sliding TTL session.expiresAt = now + this.ttlMs; this._schedulePersist(); return { user: session.user, expiresAt: session.expiresAt, createdAt: session.createdAt, }; } delete(token) { if (!token) return; const tokenHash = hashToken(token); if (this.sessions.delete(tokenHash)) { this._schedulePersist(); } } sweep() { const now = Date.now(); let removed = 0; for (const [tokenHash, session] of this.sessions) { if (session.expiresAt <= now) { this.sessions.delete(tokenHash); removed++; } } if (removed > 0) { this._schedulePersist(); if (this.logger) { this.logger.debug("Очищены устаревшие сессии", { count: removed }); } } } _schedulePersist() { if (!this.persist || !this.file) return; if (this._persistTimer) return; this._persistTimer = setTimeout(() => { this._persistTimer = null; this._flushPersist(); }, 500); if (this._persistTimer.unref) { this._persistTimer.unref(); } } _flushPersist() { if (!this.persist || !this.file) return; try { const dir = path.dirname(this.file); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const obj = {}; for (const [tokenHash, session] of this.sessions) { obj[tokenHash] = session; } const tmpFile = `${this.file}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`; const fd = fs.openSync(tmpFile, "w"); try { fs.writeFileSync(fd, JSON.stringify(obj, null, 2), "utf8"); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } fs.renameSync(tmpFile, this.file); } catch (err) { if (this.logger) { this.logger.error("Ошибка при сохранении sessions.json", { error: err.message }); } } } close() { if (this._sweepInterval) { clearInterval(this._sweepInterval); this._sweepInterval = null; } if (this._persistTimer) { clearTimeout(this._persistTimer); this._persistTimer = null; } this._flushPersist(); } } module.exports = { SessionStore, hashToken, };