diff --git a/apps/server/package.json b/apps/server/package.json index f594dd515..62c7a9c30 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -104,7 +104,6 @@ "sanitize-html": "2.17.0", "sax": "1.4.1", "serve-favicon": "2.5.0", - "session-file-store": "1.5.0", "stream-throttle": "0.1.3", "strip-bom": "5.0.0", "striptags": "3.2.0", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 43170e0bd..9c35c6aed 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -4,7 +4,6 @@ import favicon from "serve-favicon"; import cookieParser from "cookie-parser"; import helmet from "helmet"; import compression from "compression"; -import sessionParser from "./routes/session_parser.js"; import config from "./services/config.js"; import utils, { getResourceDir } from "./services/utils.js"; import assets from "./routes/assets.js"; @@ -111,6 +110,8 @@ export default async function buildApp() { app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest"))); app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"))); app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png"))); + + const sessionParser = (await import("./routes/session_parser.js")).default; app.use(sessionParser); app.use(favicon(path.join(assetsDir, "icon.ico"))); diff --git a/apps/server/src/assets/db/migrations/0231__session_store.sql b/apps/server/src/assets/db/migrations/0231__session_store.sql new file mode 100644 index 000000000..de245d25c --- /dev/null +++ b/apps/server/src/assets/db/migrations/0231__session_store.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + data TEXT, + expires INTEGER +); \ No newline at end of file diff --git a/apps/server/src/assets/db/schema.sql b/apps/server/src/assets/db/schema.sql index 29b749d89..be21bcc3d 100644 --- a/apps/server/src/assets/db/schema.sql +++ b/apps/server/src/assets/db/schema.sql @@ -187,3 +187,9 @@ CREATE TABLE IF NOT EXISTS "embedding_providers" ( "dateModified" TEXT NOT NULL, "utcDateModified" TEXT NOT NULL ); + +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + data TEXT, + expires INTEGER +); diff --git a/apps/server/src/routes/session_parser.ts b/apps/server/src/routes/session_parser.ts index c674a4890..b036657a2 100644 --- a/apps/server/src/routes/session_parser.ts +++ b/apps/server/src/routes/session_parser.ts @@ -1,10 +1,55 @@ -import session from "express-session"; -import sessionFileStore from "session-file-store"; +import sql from "../services/sql.js"; +import session, { Store } from "express-session"; import sessionSecret from "../services/session_secret.js"; -import dataDir from "../services/data_dir.js"; import config from "../services/config.js"; +import log from "../services/log.js"; -const FileStore = sessionFileStore(session); +class SQLiteSessionStore extends Store { + + get(sid: string, callback: (err: any, session?: session.SessionData | null) => void): void { + try { + const data = sql.getValue(/*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() + 3600000; // fallback to 1 hour + 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); + } + } + +} const sessionParser = session({ secret: sessionSecret, @@ -16,10 +61,14 @@ const sessionParser = session({ maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds }, name: "trilium.sid", - store: new FileStore({ - ttl: config.Session.cookieMaxAge, - path: `${dataDir.TRILIUM_DATA_DIR}/sessions` - }) + store: new SQLiteSessionStore() }); +setInterval(() => { + // Clean up expired sesions. + const now = Date.now(); + const result = sql.execute(/*sql*/`DELETE FROM sessions WHERE expires < ?`, now); + console.log("Cleaning up expired sessions: ", result.changes); +}, 60 * 60 * 1000); + export default sessionParser; diff --git a/apps/server/src/services/app_info.ts b/apps/server/src/services/app_info.ts index 60b453f9d..7f70f8bcd 100644 --- a/apps/server/src/services/app_info.ts +++ b/apps/server/src/services/app_info.ts @@ -3,7 +3,7 @@ import build from "./build.js"; import packageJson from "../../package.json" with { type: "json" }; import dataDir from "./data_dir.js"; -const APP_DB_VERSION = 230; +const APP_DB_VERSION = 231; const SYNC_VERSION = 35; const CLIPPER_PROTOCOL_VERSION = "1.0"; diff --git a/apps/server/src/services/log.ts b/apps/server/src/services/log.ts index b84018fbf..f5598b7f6 100644 --- a/apps/server/src/services/log.ts +++ b/apps/server/src/services/log.ts @@ -69,7 +69,7 @@ function info(message: string | Error) { log(message); } -function error(message: string | Error) { +function error(message: string | Error | unknown) { log(`ERROR: ${message}`); } diff --git a/apps/server/src/www.ts b/apps/server/src/www.ts index 3261a80e3..4d2e0b412 100644 --- a/apps/server/src/www.ts +++ b/apps/server/src/www.ts @@ -1,6 +1,4 @@ #!/usr/bin/env node - -import sessionParser from "./routes/session_parser.js"; import fs from "fs"; import http from "http"; import https from "https"; @@ -79,6 +77,7 @@ async function startTrilium() { const httpServer = startHttpServer(app); + const sessionParser = (await import("./routes/session_parser.js")).default; ws.init(httpServer, sessionParser as any); // TODO: Not sure why session parser is incompatible. if (utils.isElectron) { diff --git a/docs/Release Notes/Release Notes/v0.94.0.md b/docs/Release Notes/Release Notes/v0.94.0.md index e24c6a4f4..424423eff 100644 --- a/docs/Release Notes/Release Notes/v0.94.0.md +++ b/docs/Release Notes/Release Notes/v0.94.0.md @@ -20,6 +20,7 @@ * [Inconsistent Find and Replace Behavior in Large Code Notes](https://github.com/TriliumNext/Notes/issues/1826) by @SiriusXT * [Incorrect import of multiple inline math](https://github.com/TriliumNext/Notes/pull/1906) by @SiriusXT +* [Random EPERM: operation not permitted on Windows](https://github.com/TriliumNext/Notes/issues/249) ## ✨ Improvements @@ -40,7 +41,8 @@ * [Added support for opening and activating a note in a new tab using Ctrl+Shift+click on notes in the launcher pane, note tree, or note images](https://github.com/TriliumNext/Notes/pull/1854) by @SiriusXT * [Style and footnote improvements](https://github.com/TriliumNext/Notes/pull/1913) by @SiriusXT * Backend log: disable some editor features in order to increase performance for large logs (syntax highlighting, folding, etc.). -* [Collapsible table of contents](https://github.com/TriliumNext/Notes/pull/1954) by @SriiusXT +* [Collapsible table of contents](https://github.com/TriliumNext/Notes/pull/1954) by @SiriusXT +* Sessions (logins) are no longer stored as files in the data directory, but as entries in the database. This improves the session reliability on Windows platforms. ## 📖 Documentation diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bedd91a4..ee31587ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -755,9 +755,6 @@ importers: serve-favicon: specifier: 2.5.0 version: 2.5.0 - session-file-store: - specifier: 1.5.0 - version: 1.5.0 stream-throttle: specifier: 0.1.3 version: 0.1.3 @@ -6089,9 +6086,6 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - asn1.js@5.4.1: - resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -6221,9 +6215,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - bagpipe@0.3.5: - resolution: {integrity: sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -6320,9 +6311,6 @@ packages: bmp-ts@1.0.9: resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} - bn.js@4.12.1: - resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} - body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -9297,9 +9285,6 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -9708,10 +9693,6 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - kruptein@2.2.3: - resolution: {integrity: sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==} - engines: {node: '>6'} - langium@3.3.1: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} engines: {node: '>=16.0.0'} @@ -12432,10 +12413,6 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} - session-file-store@1.5.0: - resolution: {integrity: sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==} - engines: {node: '>= 6'} - set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -13375,9 +13352,6 @@ packages: typed-assert@1.0.9: resolution: {integrity: sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==} - typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -14073,9 +14047,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -20726,13 +20697,6 @@ snapshots: asap@2.0.6: {} - asn1.js@5.4.1: - dependencies: - bn.js: 4.12.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - safer-buffer: 2.1.2 - assertion-error@2.0.1: {} ast-types@0.13.4: @@ -20900,8 +20864,6 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.10) - bagpipe@0.3.5: {} - balanced-match@1.0.2: {} balanced-match@2.0.0: {} @@ -20988,8 +20950,6 @@ snapshots: bmp-ts@1.0.9: {} - bn.js@4.12.1: {} - body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -24569,8 +24529,6 @@ snapshots: dependencies: which-typed-array: 1.1.19 - is-typedarray@1.0.0: {} - is-unicode-supported@0.1.0: {} is-url@1.2.4: {} @@ -25207,10 +25165,6 @@ snapshots: kolorist@1.8.0: {} - kruptein@2.2.3: - dependencies: - asn1.js: 5.4.1 - langium@3.3.1: dependencies: chevrotain: 11.0.3 @@ -28268,15 +28222,6 @@ snapshots: transitivePeerDependencies: - supports-color - session-file-store@1.5.0: - dependencies: - bagpipe: 0.3.5 - fs-extra: 8.1.0 - kruptein: 2.2.3 - object-assign: 4.1.1 - retry: 0.12.0 - write-file-atomic: 3.0.3 - set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -29481,10 +29426,6 @@ snapshots: typed-assert@1.0.9: {} - typedarray-to-buffer@3.1.5: - dependencies: - is-typedarray: 1.0.0 - typedarray@0.0.6: {} typescript-eslint@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): @@ -30336,13 +30277,6 @@ snapshots: wrappy@1.0.2: {} - write-file-atomic@3.0.3: - dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.7 - typedarray-to-buffer: 3.1.5 - write-file-atomic@4.0.2: dependencies: imurmurhash: 0.1.4