/** * Persistent token store backed by SQLite (better-sqlite3). * * Survives process restarts — Emby tokens remain valid across deploys * and container restarts, so users do not need to re-login after an * update. Tokens are stored in DATA_DIR/tokens.db (default: /app/data * inside the container, or ./data locally). * * Schema: tokens(userId TEXT PK, accessToken TEXT, createdAt INTEGER) * * Expired rows (older than TOKEN_TTL_DAYS) are pruned on startup and * once per hour, preventing unbounded growth. */ const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); const TOKEN_TTL_DAYS = 31; // slightly longer than max cookie lifetime const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '../../data'); // Ensure data directory exists (non-root writable in container) if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } const DB_PATH = path.join(DATA_DIR, 'tokens.db'); const db = new Database(DB_PATH); // WAL mode for better concurrent read performance db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS tokens ( userId TEXT PRIMARY KEY, accessToken TEXT NOT NULL, createdAt INTEGER NOT NULL DEFAULT (strftime('%s','now')) ) `); const storeToken = db.prepare( `INSERT OR REPLACE INTO tokens (userId, accessToken, createdAt) VALUES (?, ?, strftime('%s','now'))` ); const getTokenStmt = db.prepare( `SELECT accessToken FROM tokens WHERE userId = ?` ); const clearTokenStmt = db.prepare( `DELETE FROM tokens WHERE userId = ?` ); const pruneStmt = db.prepare( `DELETE FROM tokens WHERE createdAt < strftime('%s','now') - ?` ); const TTL_SECONDS = TOKEN_TTL_DAYS * 24 * 60 * 60; function prune() { const result = pruneStmt.run(TTL_SECONDS); if (result.changes > 0) { console.log(`[TokenStore] Pruned ${result.changes} expired token(s)`); } } // Prune on startup and every hour prune(); setInterval(prune, 60 * 60 * 1000).unref(); module.exports = { storeToken(userId, accessToken) { storeToken.run(userId, accessToken); }, getToken(userId) { const row = getTokenStmt.get(userId); return row ? { accessToken: row.accessToken } : null; }, clearToken(userId) { clearTokenStmt.run(userId); } };