From 010783102a3945796f45407453935ec86bdc43fc Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Wed, 12 Feb 2025 23:09:20 +0100 Subject: [PATCH 01/10] build: fix missing copying of etapi.openapi.yaml into dist folder fixes build via electron-forge --- bin/copy-dist.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/copy-dist.ts b/bin/copy-dist.ts index 6ffc2cf98..85013e992 100644 --- a/bin/copy-dist.ts +++ b/bin/copy-dist.ts @@ -29,7 +29,7 @@ const copy = async () => { fs.copySync(path.join("build", srcFile), destFile, { recursive: true }); } - const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"]; + const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json", "./src/etapi/etapi.openapi.yaml"]; for (const file of filesToCopy) { log(`Copying ${file}`); await fs.copy(file, path.join(DEST_DIR, file)); From 53576f5578295f8a81c4406e2732033d5b0d37a6 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 10 Feb 2025 19:50:30 +0100 Subject: [PATCH 02/10] feat(config): add Session.cookieMaxAge allows users to control how long their session will be live, before it expires and they are forced to login again defaults to 1 day ("24 * 60 * 60 * 1000") as previously set in sessionParser --- config-sample.ini | 1 + src/services/config.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config-sample.ini b/config-sample.ini index 939eaa7a5..49222b1fa 100644 --- a/config-sample.ini +++ b/config-sample.ini @@ -36,6 +36,7 @@ trustedReverseProxy=false # e.g. if you have https://your-domain.com/triliumNext/instanceA and https://your-domain.com/triliumNext/instanceB # you would want to set the cookiePath value to "/triliumNext/instanceA" for your first and "/triliumNext/instanceB" for your second instance cookiePath=/ +cookieMaxAge= [Sync] #syncServerHost= diff --git a/src/services/config.ts b/src/services/config.ts index b529d4792..28b598b53 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -34,6 +34,7 @@ export interface TriliumConfig { }; Session: { cookiePath: string; + cookieMaxAge: number; } Sync: { syncServerHost: string; @@ -81,7 +82,10 @@ const config: TriliumConfig = { Session: { cookiePath: - process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/" + process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/", + + cookieMaxAge: + process.env.TRILIUM_SESSION_COOKIEMAXAGE || iniConfig?.Session?.cookieMaxAge || "24 * 60 * 60 * 1000" // 24 hours in Milliseconds }, Sync: { From 4e23b5193d271c415bfade19e9eeed73c6cb8e12 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 10 Feb 2025 19:51:05 +0100 Subject: [PATCH 03/10] feat(session_parser): use Session.cookieMaxAge from config --- src/routes/session_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/session_parser.ts b/src/routes/session_parser.ts index eaaf0ebe9..d82e8be3d 100644 --- a/src/routes/session_parser.ts +++ b/src/routes/session_parser.ts @@ -12,7 +12,7 @@ const sessionParser = session({ cookie: { path: config.Session.cookiePath, httpOnly: true, - maxAge: 24 * 60 * 60 * 1000 // in milliseconds + maxAge: config.Session.cookieMaxAge }, name: "trilium.sid", store: new FileStore({ From 04827c0ce19105cdb98b764651b161b62b26db04 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 10 Feb 2025 19:59:40 +0100 Subject: [PATCH 04/10] fix(session_parser): FileStore ttl should be ideally the same as session cookies maxAge this avoids having "unused" dead session on the filesystem --- src/routes/session_parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/session_parser.ts b/src/routes/session_parser.ts index d82e8be3d..22323ce58 100644 --- a/src/routes/session_parser.ts +++ b/src/routes/session_parser.ts @@ -16,7 +16,7 @@ const sessionParser = session({ }, name: "trilium.sid", store: new FileStore({ - ttl: 30 * 24 * 3600, + ttl: config.Session.cookieMaxAge / 1000, // needs value in seconds path: `${dataDir.TRILIUM_DATA_DIR}/sessions` }) }); From 2a740781cb42f9c087a3151a9fbf7e7791ea2b8b Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Thu, 13 Feb 2025 08:39:02 +0100 Subject: [PATCH 05/10] feat(session_parser): use seconds for setting maxAge and update default value to 21 days 21 days was used in the login route previously, when "remember me" was set --- src/routes/session_parser.ts | 4 ++-- src/services/config.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/session_parser.ts b/src/routes/session_parser.ts index 22323ce58..503cfff75 100644 --- a/src/routes/session_parser.ts +++ b/src/routes/session_parser.ts @@ -12,11 +12,11 @@ const sessionParser = session({ cookie: { path: config.Session.cookiePath, httpOnly: true, - maxAge: config.Session.cookieMaxAge + maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds }, name: "trilium.sid", store: new FileStore({ - ttl: config.Session.cookieMaxAge / 1000, // needs value in seconds + ttl: config.Session.cookieMaxAge, path: `${dataDir.TRILIUM_DATA_DIR}/sessions` }) }); diff --git a/src/services/config.ts b/src/services/config.ts index 28b598b53..b17ed954b 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -85,7 +85,7 @@ const config: TriliumConfig = { process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/", cookieMaxAge: - process.env.TRILIUM_SESSION_COOKIEMAXAGE || iniConfig?.Session?.cookieMaxAge || "24 * 60 * 60 * 1000" // 24 hours in Milliseconds + process.env.TRILIUM_SESSION_COOKIEMAXAGE || iniConfig?.Session?.cookieMaxAge || 21 * 24 * 60 * 60 // 21 Days in Seconds }, Sync: { From 38215c46aec5ac3561b1efa05a2b242a6b39f464 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Thu, 13 Feb 2025 09:04:34 +0100 Subject: [PATCH 06/10] feat(login): make use of default maxAge by sessionParser cookie will use the default value set in sessionParser middleware, which is controlled by config.Session.cookieMaxAge if rememberMe is not set -> the value is unset and the cookie becomes a non-persistent cookie, which the browser delete after the current session (e.g. when you close the browser) --- src/routes/login.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/login.ts b/src/routes/login.ts index 21ebaf280..9bac3db74 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -70,9 +70,12 @@ function login(req: Request, res: Response) { } req.session.regenerate(() => { - const sessionMaxAge = 21 * 24 * 3600000 // 3 weeks in Milliseconds + if (!rememberMe) { + // unset default maxAge set by sessionParser + // Cookie becomes non-persistent and expires after current browser session (e.g. when browser is closed) + req.session.cookie.maxAge = undefined; + } - req.session.cookie.maxAge = (rememberMe) ? sessionMaxAge : undefined; req.session.loggedIn = true; res.redirect("."); From 201663d9ecbde7587c68f6d6661ca2315ebe0ec6 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Thu, 13 Feb 2025 09:07:25 +0100 Subject: [PATCH 07/10] chore(prettier): fix prettier issues --- src/routes/login.ts | 1 - src/routes/session_parser.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/login.ts b/src/routes/login.ts index 9bac3db74..68b98e893 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -80,7 +80,6 @@ function login(req: Request, res: Response) { res.redirect("."); }); - } function verifyPassword(guessedPassword: string) { diff --git a/src/routes/session_parser.ts b/src/routes/session_parser.ts index 503cfff75..89df0e037 100644 --- a/src/routes/session_parser.ts +++ b/src/routes/session_parser.ts @@ -12,7 +12,7 @@ const sessionParser = session({ cookie: { path: config.Session.cookiePath, httpOnly: true, - maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds + maxAge: config.Session.cookieMaxAge * 1000 // needs value in milliseconds }, name: "trilium.sid", store: new FileStore({ From cab0a5e41f3bad5a864092d3a056f2346466437d Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Thu, 13 Feb 2025 09:25:24 +0100 Subject: [PATCH 08/10] feat(config): improve Session descriptions --- config-sample.ini | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/config-sample.ini b/config-sample.ini index 49222b1fa..baa026730 100644 --- a/config-sample.ini +++ b/config-sample.ini @@ -30,13 +30,18 @@ trustedReverseProxy=false [Session] -# Use this setting to constrain the current instance's "Path" value for the set cookies +# Use this setting to set a custom value for the "Path" Attribute value of the session cookie. # This can be useful, when you have several instances running on the same domain, under different paths (e.g. by using a reverse proxy). -# It prevents your instances from overwriting each others' cookies. -# e.g. if you have https://your-domain.com/triliumNext/instanceA and https://your-domain.com/triliumNext/instanceB +# It prevents your instances from overwriting each others' cookies, allowing you to stay logged in multiple instances simultanteously. +# E.g. if you have instances running under https://your-domain.com/triliumNext/instanceA and https://your-domain.com/triliumNext/instanceB # you would want to set the cookiePath value to "/triliumNext/instanceA" for your first and "/triliumNext/instanceB" for your second instance cookiePath=/ -cookieMaxAge= + +# Use this setting to set a custom value for the "Max-Age" Attribute of the session cookie. +# This controls how long your session will be valid, before it expires and you need to log in again, when you use the "Remember Me" option. +# Value needs to be entered in Seconds. +# Default value is 1814400 Seconds, which is 21 Days. +cookieMaxAge=1814400 [Sync] #syncServerHost= From b692c00b8de0aef5380858570e8366c6773b9697 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Thu, 13 Feb 2025 09:46:49 +0100 Subject: [PATCH 09/10] feat(config): improve typesafety by definitely returning a number previously it was either a number like string (in case env or config.ini was used) or a number (the fallback value) we now parseInt the value -> if any value is NaN (e.g. because it was incorrectly set) it will try with the next, before it uses the fallback value the strange looking `parseInt(String(process.env.TRILIUM_SESSION_COOKIEMAXAGE))` is required to make TypeScript happy, other variants of trying to get the value into a string were not good enough for typescript :-) The `String(process.env.TRILIUM_SESSION_COOKIEMAXAGE)` will now either return a number like value or 'undefined' (as string), which parseInt parses into NaN, which is falsy. --- src/services/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/config.ts b/src/services/config.ts index b17ed954b..b10015aa7 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -85,7 +85,7 @@ const config: TriliumConfig = { process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/", cookieMaxAge: - process.env.TRILIUM_SESSION_COOKIEMAXAGE || iniConfig?.Session?.cookieMaxAge || 21 * 24 * 60 * 60 // 21 Days in Seconds + parseInt(String(process.env.TRILIUM_SESSION_COOKIEMAXAGE)) || parseInt(iniConfig?.Session?.cookieMaxAge) || 21 * 24 * 60 * 60 // 21 Days in Seconds }, Sync: { From be4b74e791727c1e70bd244406e263a714d29119 Mon Sep 17 00:00:00 2001 From: FliegendeWurst Date: Thu, 13 Feb 2025 17:02:03 +0100 Subject: [PATCH 10/10] Automated OpenAPI spec generation --- bin/generate-openapi.js | 51 +++++++++ package-lock.json | 227 +++++++++++++++++++++++++++++++++++++ package.json | 2 + src/routes/api/app_info.ts | 45 ++++++++ src/routes/api/login.ts | 62 ++++++++++ src/routes/api/setup.ts | 28 +++++ 6 files changed, 415 insertions(+) create mode 100644 bin/generate-openapi.js diff --git a/bin/generate-openapi.js b/bin/generate-openapi.js new file mode 100644 index 000000000..ae6818482 --- /dev/null +++ b/bin/generate-openapi.js @@ -0,0 +1,51 @@ +import swaggerJsdoc from 'swagger-jsdoc'; + +/* + * Usage: npm run generate-openapi | tail -n1 > x.json + * + * Inspect generated file by opening it in https://editor-next.swagger.io/ + * + */ + +const options = { + definition: { + openapi: '3.1.1', + info: { + title: 'Trilium Notes - Sync server API', + version: '0.96.6', + description: "This is the internal sync server API used by Trilium Notes / TriliumNext Notes.\n\n_If you're looking for the officially supported External Trilium API, see [here](https://triliumnext.github.io/Docs/Wiki/etapi.html)._\n\nThis page does not yet list all routes. For a full list, see the [route controller](https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/routes.ts).", + contact: { + name: "TriliumNext issue tracker", + url: "https://github.com/TriliumNext/Notes/issues", + }, + license: { + name: "GNU Free Documentation License 1.3 (or later)", + url: "https://www.gnu.org/licenses/fdl-1.3", + }, + }, + }, + apis: ['./src/routes/api/*.ts', './bin/generate-openapi.js'], +}; + +const openapiSpecification = swaggerJsdoc(options); + +console.log(JSON.stringify(openapiSpecification)); + +/** + * @swagger + * components: + * schemas: + * UtcDateTime: + * type: string + * example: "2025-02-13T07:42:47.698Z" + * securitySchemes: + * user-password: + * type: apiKey + * name: trilium-cred + * in: header + * description: "Username and password, formatted as `user:password`" + * session: + * type: apiKey + * in: cookie + * name: trilium.sid + */ diff --git a/package-lock.json b/package-lock.json index d633ccfe2..5fdf64248 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,6 +165,7 @@ "prettier": "3.5.0", "rcedit": "4.0.1", "rimraf": "6.0.1", + "swagger-jsdoc": "6.2.8", "tslib": "2.8.1", "tsx": "4.19.2", "typedoc": "0.27.7", @@ -222,6 +223,54 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.2.tgz", @@ -2715,6 +2764,13 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jsdoc/salty": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz", @@ -6021,6 +6077,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/caniuse-lite": { "version": "1.0.30001689", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", @@ -7799,6 +7862,19 @@ "p-limit": "^3.1.0 " } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -8992,6 +9068,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -11962,6 +12048,21 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -13101,6 +13202,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -16088,6 +16197,82 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/swagger-ui-dist": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz", @@ -17187,6 +17372,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -18629,6 +18824,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 2555c4ed3..9829a639f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts", "integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts", "generate-document": "cross-env nodemon ./bin/generate_document.ts 1000", + "generate-openapi": "node bin/generate-openapi.js", "ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts", "prettier-check": "prettier . --check", "prettier-fix": "prettier . --write" @@ -211,6 +212,7 @@ "prettier": "3.5.0", "rcedit": "4.0.1", "rimraf": "6.0.1", + "swagger-jsdoc": "6.2.8", "tslib": "2.8.1", "tsx": "4.19.2", "typedoc": "0.27.7", diff --git a/src/routes/api/app_info.ts b/src/routes/api/app_info.ts index 87501e804..560a52d1f 100644 --- a/src/routes/api/app_info.ts +++ b/src/routes/api/app_info.ts @@ -2,6 +2,51 @@ import appInfo from "../../services/app_info.js"; +/** + * @swagger + * /api/app-info: + * get: + * summary: Get installation info + * operationId: app-info + * externalDocs: + * description: Server implementation + * url: https://github.com/TriliumNext/Notes/blob/v0.91.6/src/services/app_info.ts + * responses: + * '200': + * content: + * application/json: + * schema: + * type: object + * properties: + * appVersion: + * type: string + * example: "0.91.6" + * dbVersion: + * type: integer + * example: 228 + * nodeVersion: + * type: string + * description: "value of process.version" + * syncVersion: + * type: integer + * example: 34 + * buildDate: + * type: string + * example: "2024-09-07T18:36:34Z" + * buildRevision: + * type: string + * example: "7c0d6930fa8f20d269dcfbcbc8f636a25f6bb9a7" + * dataDirectory: + * type: string + * example: "/var/lib/trilium" + * clipperProtocolVersion: + * type: string + * example: "1.0" + * utcDateTime: + * $ref: '#/components/schemas/UtcDateTime' + * security: + * - session: [] + */ function getAppInfo() { return appInfo; } diff --git a/src/routes/api/login.ts b/src/routes/api/login.ts index 3a6d43e99..1f190aec2 100644 --- a/src/routes/api/login.ts +++ b/src/routes/api/login.ts @@ -14,6 +14,68 @@ import ws from "../../services/ws.js"; import etapiTokenService from "../../services/etapi_tokens.js"; import type { Request } from "express"; +/** + * @swagger + * /api/login/sync: + * post: + * tags: + * - auth + * summary: Log in using documentSecret + * description: The `hash` parameter is computed using a HMAC of the `documentSecret` and `timestamp`. + * operationId: login-sync + * externalDocs: + * description: HMAC calculation + * url: https://github.com/TriliumNext/Notes/blob/v0.91.6/src/services/utils.ts#L62-L66 + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * timestamp: + * $ref: '#/components/schemas/UtcDateTime' + * hash: + * type: string + * syncVersion: + * type: integer + * example: 34 + * responses: + * '200': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * syncVersion: + * type: integer + * example: 34 + * options: + * type: object + * properties: + * documentSecret: + * type: string + * '400': + * description: Sync version / document secret mismatch + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Non-matching sync versions, local is version ${server syncVersion}, remote is ${requested syncVersion}. It is recommended to run same version of Trilium on both sides of sync" + * '401': + * description: Timestamp mismatch + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: "Auth request time is out of sync, please check that both client and server have correct time. The difference between clocks has to be smaller than 5 minutes" + */ function loginSync(req: Request) { if (!sqlInit.schemaExists()) { return [500, { message: "DB schema does not exist, can't sync." }]; diff --git a/src/routes/api/setup.ts b/src/routes/api/setup.ts index e4e5a6729..718bc37a7 100644 --- a/src/routes/api/setup.ts +++ b/src/routes/api/setup.ts @@ -45,6 +45,34 @@ function saveSyncSeed(req: Request) { sqlInit.createDatabaseForSync(options); } +/** + * @swagger + * /api/setup/sync-seed: + * get: + * tags: + * - auth + * summary: Sync documentSecret value + * description: First step to logging in. + * operationId: setup-sync-seed + * responses: + * '200': + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * syncVersion: + * type: integer + * example: 34 + * options: + * type: object + * properties: + * documentSecret: + * type: string + * security: + * - user-password: [] + */ function getSyncSeed() { log.info("Serving sync seed.");