source: Klonkt/src/services/SqliteSessionStore.js@ b05eb97

main
Last change on this file since b05eb97 was 7bc636b, checked in by Robin <robin@…>, 4 months ago

Initial commit — PrutFolio v1 source (pulled from Hetzner /srv/prutfolio)

  • Property mode set to 100644
File size: 3.6 KB
Line 
1/**
2 * Persistent session store backed by the existing `sessions` table in
3 * the SQLite DB. Survives `node --watch` restarts (no more "had to log in
4 * again after every save").
5 *
6 * Schema (already in migrations/001-init.sql):
7 * sessions(sid TEXT PRIMARY KEY, data TEXT, expiresAt DATETIME)
8 *
9 * Implements the minimal express-session Store contract: get / set /
10 * destroy / touch / length / clear / all. Periodic GC clears expired rows.
11 */
12
13import session from 'express-session';
14import db from '../config/database.js';
15
16const Store = session.Store;
17
18export class SqliteSessionStore extends Store {
19 constructor(options = {}) {
20 super(options);
21 // Periodic cleanup of expired rows. Default every 15 min.
22 const intervalMs = options.cleanupIntervalMs ?? 15 * 60 * 1000;
23 if (intervalMs > 0) {
24 this._gcTimer = setInterval(() => this._gc(), intervalMs);
25 // Don't keep the event loop alive just for GC.
26 if (this._gcTimer.unref) this._gcTimer.unref();
27 }
28
29 // Prepared statements (faster than re-preparing per call)
30 this._stmtGet = db.prepare('SELECT data, expiresAt FROM sessions WHERE sid = ?');
31 this._stmtSet = db.prepare(
32 'INSERT INTO sessions (sid, data, expiresAt) VALUES (?, ?, ?) ' +
33 'ON CONFLICT(sid) DO UPDATE SET data = excluded.data, expiresAt = excluded.expiresAt'
34 );
35 this._stmtDestroy = db.prepare('DELETE FROM sessions WHERE sid = ?');
36 this._stmtTouch = db.prepare('UPDATE sessions SET expiresAt = ? WHERE sid = ?');
37 this._stmtCount = db.prepare("SELECT COUNT(*) AS c FROM sessions WHERE expiresAt > datetime('now')");
38 this._stmtClear = db.prepare('DELETE FROM sessions');
39 this._stmtAll = db.prepare("SELECT sid, data FROM sessions WHERE expiresAt > datetime('now')");
40 this._stmtGc = db.prepare("DELETE FROM sessions WHERE expiresAt <= datetime('now')");
41 }
42
43 _gc() {
44 try { this._stmtGc.run(); } catch (e) { /* swallow */ }
45 }
46
47 _expiryFor(sess) {
48 // express-session sets sess.cookie.expires (Date) when maxAge is set.
49 // Fallback to "now + 30 days" if missing (matches our cookie maxAge default).
50 const exp = sess?.cookie?.expires
51 ? new Date(sess.cookie.expires)
52 : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
53 return exp.toISOString();
54 }
55
56 get(sid, cb) {
57 try {
58 const row = this._stmtGet.get(sid);
59 if (!row) return cb(null, null);
60 // Lazy expiry check (the periodic GC may not have run yet)
61 if (row.expiresAt && new Date(row.expiresAt).getTime() <= Date.now()) {
62 this._stmtDestroy.run(sid);
63 return cb(null, null);
64 }
65 cb(null, JSON.parse(row.data));
66 } catch (e) {
67 cb(e);
68 }
69 }
70
71 set(sid, sess, cb) {
72 try {
73 this._stmtSet.run(sid, JSON.stringify(sess), this._expiryFor(sess));
74 cb && cb(null);
75 } catch (e) {
76 cb && cb(e);
77 }
78 }
79
80 destroy(sid, cb) {
81 try {
82 this._stmtDestroy.run(sid);
83 cb && cb(null);
84 } catch (e) {
85 cb && cb(e);
86 }
87 }
88
89 touch(sid, sess, cb) {
90 try {
91 this._stmtTouch.run(this._expiryFor(sess), sid);
92 cb && cb(null);
93 } catch (e) {
94 cb && cb(e);
95 }
96 }
97
98 length(cb) {
99 try { cb(null, this._stmtCount.get().c); } catch (e) { cb(e); }
100 }
101
102 clear(cb) {
103 try { this._stmtClear.run(); cb && cb(null); } catch (e) { cb && cb(e); }
104 }
105
106 all(cb) {
107 try {
108 const rows = this._stmtAll.all();
109 const out = {};
110 for (const r of rows) {
111 try { out[r.sid] = JSON.parse(r.data); } catch (e) { /* skip */ }
112 }
113 cb(null, out);
114 } catch (e) { cb(e); }
115 }
116}
117
118export default SqliteSessionStore;
Note: See TracBrowser for help on using the repository browser.