Files
Trilium/apps/server/src/routes/session_parser.ts

130 lines
4.5 KiB
TypeScript

import sql from "../services/sql.js";
import session, { Store } from "express-session";
import sessionSecret from "../services/session_secret.js";
import config from "../services/config.js";
import log from "../services/log.js";
import type express from "express";
/**
* The amount of time in milliseconds after which expired sessions are cleaned up.
*/
export const CLEAN_UP_INTERVAL = 60 * 60 * 1000; // 1 hour
/**
* The amount of time in milliseconds after which a session cookie expires if "Remember me" is not checked.
*
* Note that the session is renewed on each request, so the session will last up to this time from the last request.
*/
export const SESSION_COOKIE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
export class SQLiteSessionStore extends Store {
get(sid: string, callback: (err: any, session?: session.SessionData | null) => void): void {
try {
const data = sql.getValue<string>(/*sql*/`SELECT data FROM sessions WHERE id = ?`, sid);
let session = null;
if (data) {
session = JSON.parse(data);
}
return callback(null, session);
} catch (e: unknown) {
log.error(e);
return callback(e);
}
}
set(id: string, session: session.SessionData, callback?: (err?: any) => void): void {
try {
const expires = session.cookie?.expires
? new Date(session.cookie.expires).getTime()
: Date.now() + SESSION_COOKIE_EXPIRY;
const data = JSON.stringify(session);
sql.upsert("sessions", "id", {
id,
expires,
data
});
callback?.();
} catch (e) {
log.error(e);
return callback?.(e);
}
}
destroy(sid: string, callback?: (err?: any) => void): void {
try {
sql.execute(/*sql*/`DELETE FROM sessions WHERE id = ?`, sid);
callback?.();
} catch (e) {
log.error(e);
callback?.(e);
}
}
touch(sid: string, session: session.SessionData, callback?: (err?: any) => void): void {
// For now it's only for session cookies ("Remember me" unchecked).
if (session.cookie?.expires) {
callback?.();
return;
}
try {
const expires = Date.now() + SESSION_COOKIE_EXPIRY;
sql.execute(/*sql*/`UPDATE sessions SET expires = ? WHERE id = ?`, [expires, sid]);
callback?.();
} catch (e) {
log.error(e);
callback?.(e);
}
}
/**
* Given a session ID, returns the expiry date of the session.
*
* @param sid the session ID to check.
* @returns the expiry date of the session or null if the session does not exist.
*/
getSessionExpiry(sid: string): Date | null {
try {
const expires = sql.getValue<number>(/*sql*/`SELECT expires FROM sessions WHERE id = ?`, sid);
return expires !== undefined ? new Date(expires) : null;
} catch (e) {
log.error(e);
return null;
}
}
}
export const sessionStore = new SQLiteSessionStore();
const sessionParser: express.RequestHandler = session({
secret: sessionSecret,
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.
saveUninitialized: true, // Ensures new sessions are always persisted and the session cookie is sent on the first response.
// This is required for csrf-csrf v4, which binds CSRF tokens to the session ID via HMAC.
// With saveUninitialized: false, a brand-new session would never be saved (and its cookie
// never sent) when noAuthentication=true, causing every request to get a different ephemeral
// session ID and making CSRF validation fail for all API calls.
rolling: true, // forces the session to be saved back to the session store, resetting the expiration date.
cookie: {
path: "/",
httpOnly: true,
maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds
},
name: "trilium.sid",
store: sessionStore
});
export function startSessionCleanup() {
setInterval(() => {
// Clean up expired sessions.
const now = Date.now();
const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now);
console.log("Cleaning up expired sessions: ", result.changes);
}, CLEAN_UP_INTERVAL);
}
export default sessionParser;