From 3ca7fccb306d0b23626befc3791ffa360b3db1e7 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Thu, 20 Jun 2024 17:47:28 -0700 Subject: [PATCH] update dependencies, fixes to support new authentication features --- CHANGELOG.md | 10 + config/index.js | 5 + documentation/media/postgres-er.svg | 20 +- documentation/media/sqlite-er.svg | 20 +- package-lock.json | 582 +++++++++--------- package.json | 10 +- src/common.js | 3 + src/db/base.js | 28 +- src/db/postgres/index.js | 47 +- .../sql/authentication-update-credential.sql | 3 + .../sql/authentication-update-otp-key.sql | 3 + src/db/postgres/sql/authentication-upsert.sql | 7 +- src/db/postgres/sql/schema/1.1.0/apply.sql | 7 + src/db/postgres/sql/schema/1.1.0/er.dot | 140 +++++ src/db/postgres/sql/schema/1.1.0/revert.sql | 5 + src/db/sqlite/index.js | 51 +- .../sql/authentication-update-credential.sql | 3 + .../sql/authentication-update-otp-key.sql | 3 + src/db/sqlite/sql/authentication-upsert.sql | 7 +- src/db/sqlite/sql/schema/1.1.0/apply.sql | 7 + src/db/sqlite/sql/schema/1.1.0/er.dot | 140 +++++ src/db/sqlite/sql/schema/1.1.0/revert.sql | 5 + src/service.js | 97 ++- src/template/admin-overview-html.js | 7 +- src/template/admin-topic-details-html.js | 7 +- src/template/root-html.js | 13 +- src/template/template-helper.js | 25 +- static/theme.css | 4 +- test/src/db/factory.js | 4 +- test/src/db/integration.js | 30 +- test/src/db/postgres.js | 77 ++- test/src/db/sqlite.js | 77 ++- test/src/service.js | 28 + test/stub-db.js | 2 + 34 files changed, 1101 insertions(+), 376 deletions(-) create mode 100644 src/db/postgres/sql/authentication-update-credential.sql create mode 100644 src/db/postgres/sql/authentication-update-otp-key.sql create mode 100644 src/db/postgres/sql/schema/1.1.0/apply.sql create mode 100644 src/db/postgres/sql/schema/1.1.0/er.dot create mode 100644 src/db/postgres/sql/schema/1.1.0/revert.sql create mode 100644 src/db/sqlite/sql/authentication-update-credential.sql create mode 100644 src/db/sqlite/sql/authentication-update-otp-key.sql create mode 100644 src/db/sqlite/sql/schema/1.1.0/apply.sql create mode 100644 src/db/sqlite/sql/schema/1.1.0/er.dot create mode 100644 src/db/sqlite/sql/schema/1.1.0/revert.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index fa40422..375376f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ Releases and notable changes to this project are documented here. +## [unreleased] - TBD + +### Added + +- Account credential management interface. + +### Fixed + +- Dependency updates. + ## [v1.3.11] - 2023-03-22 ### Fixed diff --git a/config/index.js b/config/index.js index d06162e..8f31211 100644 --- a/config/index.js +++ b/config/index.js @@ -5,6 +5,11 @@ const common = require('../src/common'); const defaultEnvironment = 'development'; const testEnvironment = 'test'; +/** + * Merge environment-specific config objects on top of defaults. + * @param {string=} environment from NODE_ENV + * @returns {object} config + */ function Config(environment) { environment = environment || defaultEnvironment; const defaultConfig = require('./default'); diff --git a/documentation/media/postgres-er.svg b/documentation/media/postgres-er.svg index 8610593..26a00ff 100644 --- a/documentation/media/postgres-er.svg +++ b/documentation/media/postgres-er.svg @@ -1,7 +1,7 @@ - Websub Hub Entity-Relations Postgres -Schema 1.0.4 +Schema 1.1.0 topic @@ -238,17 +238,19 @@ authentication - - -AUTHENTICATION + + +AUTHENTICATION + +created -created +last_authenticated -last_authenticated +identifier -identifier +credential -credential +otp_key diff --git a/documentation/media/sqlite-er.svg b/documentation/media/sqlite-er.svg index 91752cb..0d151f4 100644 --- a/documentation/media/sqlite-er.svg +++ b/documentation/media/sqlite-er.svg @@ -1,7 +1,7 @@ - Websub Hub Entity-Relations SQLite -Schema 1.0.4 +Schema 1.1.0 topic @@ -238,17 +238,19 @@ authentication - - -AUTHENTICATION + + +AUTHENTICATION + +created -created +last_authenticated -last_authenticated +identifier -identifier +credential -credential +otp_key diff --git a/package-lock.json b/package-lock.json index a8b6d27..b09d6a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "1.3.11", "license": "ISC", "dependencies": { - "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.9", - "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.3.1", - "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0", + "@squeep/api-dingus": "^2", + "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.5.0", + "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1", "@squeep/logger-json-console": "^3", "@squeep/roman": "^1", "@squeep/web-linking": "^1", @@ -757,20 +757,20 @@ } }, "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", "optional": true, "dependencies": { - "detect-libc": "^2.0.0", + "detect-libc": "^1.0.3", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.6.1", "nopt": "^5.0.0", - "npmlog": "^5.0.1", + "npmlog": "^4.1.2", "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" + "semver": "^7.3.4", + "tar": "^6.1.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" @@ -881,30 +881,34 @@ "dev": true }, "node_modules/@squeep/api-dingus": { - "version": "1.2.9", - "resolved": "git+https://git.squeep.com/squeep-api-dingus/#3b15b5ff792fc5d61be8337989058c297460cd99", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@squeep/api-dingus/-/api-dingus-2.1.2.tgz", + "integrity": "sha512-d4jj0TL0qDz/ADWMqwPkmJ/eFy2fhzFTP2iBTcAzIG/ZH0y0Xtic8XdZct5kiihAVQC6ut3SCptkxXKeA7K/mA==", "dependencies": { + "@squeep/log-helper": "^1", "mime-db": "^1.52.0", - "uuid": "^9.0.0" + "uuid": "^9.0.1" }, "engines": { - "node": ">=14" + "node": ">=14.13.1" } }, "node_modules/@squeep/authentication-module": { - "version": "1.3.1", - "resolved": "git+https://git.squeep.com/squeep-authentication-module/#a821a0f79c42f8ea55aec503c992650113d82028", + "version": "1.5.0", + "resolved": "git+https://git.squeep.com/squeep-authentication-module/#5ea2ffe571a74618eef073c58c5fef06e1cf06a7", "dependencies": { - "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.9", - "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0", - "@squeep/indieauth-helper": "^1.3.0", - "@squeep/mystery-box": "^2.0.1" + "@squeep/api-dingus": "^2", + "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1", + "@squeep/indieauth-helper": "^1", + "@squeep/mystery-box": "^2", + "@squeep/totp": "^1", + "uuid": "^9" }, "engines": { - "node": "^14 >=14.18 || >=15.7" + "node": ">=18" }, "optionalDependencies": { - "argon2": "^0.30.3", + "argon2": "^0.40.1", "node-linux-pam": "^0.2.1" } }, @@ -929,13 +933,13 @@ } }, "node_modules/@squeep/html-template-helper": { - "version": "1.4.0", - "resolved": "git+https://git.squeep.com/squeep-html-template-helper#100046316a87631fb8814f80b35647709e6c7319", + "version": "1.6.1", + "resolved": "git+https://git.squeep.com/squeep-html-template-helper#93d1b030d6b3c6ea93c36a46f4940181a1acaca0", "dependencies": { - "@squeep/lazy-property": "^1.1.2" + "@squeep/lazy-property": "^1" }, "engines": { - "node": ">=14" + "node": ">=14.13.1" } }, "node_modules/@squeep/indieauth-helper": { @@ -991,6 +995,18 @@ "resolved": "https://registry.npmjs.org/@squeep/roman/-/roman-1.0.2.tgz", "integrity": "sha512-VFNjvSTR96r1Ii7zTxweUimiyoxs9cRo6TXXwjR/dX+riOL+YERvhf2w5M+mYYC3QhKP+j9aUVFrTWgjJV3MIw==" }, + "node_modules/@squeep/totp": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@squeep/totp/-/totp-1.1.5.tgz", + "integrity": "sha512-keqggH2NrHs8hqzyov31zIA4XTLUxwXBn+VfUFlCdzZY2omoWbgm4742Ht8j3W48FLtIX1q4Zrm1ncObi9RfMA==", + "dependencies": { + "base32.js": "^0.1.0", + "qrcode-svg": "^1.1.0" + }, + "engines": { + "node": ">14" + } + }, "node_modules/@squeep/web-linking": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@squeep/web-linking/-/web-linking-1.0.9.tgz", @@ -1038,9 +1054,9 @@ "dev": true }, "node_modules/@typescript-eslint/types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz", - "integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==", + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz", + "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1182,9 +1198,9 @@ } }, "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "node_modules/archy": { @@ -1203,46 +1219,29 @@ } }, "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "readable-stream": "^2.0.6" } }, "node_modules/argon2": { - "version": "0.30.3", - "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.30.3.tgz", - "integrity": "sha512-DoH/kv8c9127ueJSBxAVJXinW9+EuPA3EMUxoV2sAY1qDE5H9BjTyVF/aD2XyHqbqUWabgBkIfcP3ZZuGhbJdg==", + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.3.tgz", + "integrity": "sha512-FrSmz4VeM91jwFvvjsQv9GYp6o/kARWoYKjbjDB2U5io1H3e5X67PYGclFDeQff6UXIhUd4aHR3mxCdBbMMuQw==", "hasInstallScript": true, "optional": true, "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.10", "@phc/format": "^1.0.0", - "node-addon-api": "^5.0.0" + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.17.0" } }, "node_modules/argparse": { @@ -1271,6 +1270,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "devOptional": true }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1533,9 +1540,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001634", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001634.tgz", - "integrity": "sha512-fbBYXQ9q3+yp1q1gBk86tOFs4pyn/yxFm5ZNP18OXJDfA3txImOY9PhfxVggZ4vRHDqoU8NrKU81eN0OtzOgRA==", + "version": "1.0.30001636", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", + "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", "dev": true, "funding": [ { @@ -1648,6 +1655,29 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -1675,15 +1705,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", @@ -1853,12 +1874,15 @@ "optional": true }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, "engines": { - "node": ">=8" + "node": ">=0.10" } }, "node_modules/diff": { @@ -1922,9 +1946,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.803", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz", - "integrity": "sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==", + "version": "1.4.805", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.805.tgz", + "integrity": "sha512-8W4UJwX/w9T0QSzINJckTKG6CYpAUTqsaWcWIsdud3I1FYJcMgW9QqT1/4CBff/pP/TihWh13OmiyY8neto6vw==", "dev": true }, "node_modules/emoji-regex": { @@ -2207,12 +2231,12 @@ } }, "node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -2525,24 +2549,41 @@ } }, "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", + "aproba": "^1.0.3", "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, "node_modules/gensync": { @@ -2655,9 +2696,9 @@ } }, "node_modules/globals": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.4.0.tgz", - "integrity": "sha512-unnwvMZpv0eDUyjNyh9DH/yxUaRYrEjW/qK4QcdrHg3oO11igUQrCSgODHEqxlKg8v2CD2Sd7UkqqEBoz5U7TQ==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.6.0.tgz", + "integrity": "sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==", "dev": true, "engines": { "node": ">=18" @@ -2964,12 +3005,15 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "devOptional": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "optional": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/is-glob": { @@ -3669,10 +3713,13 @@ } }, "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "optional": true + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", + "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } }, "node_modules/node-fetch": { "version": "2.7.0", @@ -3694,6 +3741,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-linux-pam": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-linux-pam/-/node-linux-pam-0.2.1.tgz", @@ -3714,52 +3772,6 @@ "node": ">=8.6.0" } }, - "node_modules/node-linux-pam/node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", - "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.1", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "rimraf": "^3.0.2", - "semver": "^7.3.4", - "tar": "^6.1.0" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/node-linux-pam/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/node-linux-pam/node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true - }, - "node_modules/node-linux-pam/node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "deprecated": "This package is no longer supported.", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/node-linux-pam/node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -3771,39 +3783,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/node-linux-pam/node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-linux-pam/node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-linux-pam/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/node-linux-pam/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -3817,47 +3796,13 @@ "node": ">=8" } }, - "node_modules/node-linux-pam/node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "deprecated": "This package is no longer supported.", - "optional": true, - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/node-linux-pam/node_modules/gauge/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "optional": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/node-linux-pam/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "optional": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/node-linux-pam/node_modules/locate-path": { @@ -3878,19 +3823,6 @@ "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==", "optional": true }, - "node_modules/node-linux-pam/node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "deprecated": "This package is no longer supported.", - "optional": true, - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/node-linux-pam/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3918,16 +3850,18 @@ "node": ">=8" } }, - "node_modules/node-linux-pam/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/node-linux-pam/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "optional": true, "dependencies": { - "ansi-regex": "^2.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/node-linux-pam/node_modules/wrap-ansi": { @@ -3944,27 +3878,6 @@ "node": ">=8" } }, - "node_modules/node-linux-pam/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-linux-pam/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/node-linux-pam/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -4060,16 +3973,16 @@ } }, "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "deprecated": "This package is no longer supported.", "optional": true, "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, "node_modules/number-is-nan": { @@ -4167,6 +4080,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/nyc/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/nyc/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4215,6 +4137,20 @@ "node": ">=8" } }, + "node_modules/nyc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nyc/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -4784,6 +4720,15 @@ "node": ">=10" } }, + "node_modules/prebuild-install/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4835,6 +4780,14 @@ "node": ">=6" } }, + "node_modules/qrcode-svg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz", + "integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==", + "bin": { + "qrcode-svg": "bin/qrcode-svg.js" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5319,17 +5272,38 @@ "optional": true }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "optional": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/strip-ansi": { @@ -5559,9 +5533,9 @@ } }, "node_modules/type-fest": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", - "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==", + "version": "4.20.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.1.tgz", + "integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==", "engines": { "node": ">=16" }, @@ -5746,6 +5720,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5854,6 +5851,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d457c09..8da0034 100644 --- a/package.json +++ b/package.json @@ -29,15 +29,15 @@ "author": "Justin Wind ", "license": "ISC", "pre-commit": [ - "audit", "eslint", "coverage", - "coverage-check" + "coverage-check", + "audit" ], "dependencies": { - "@squeep/api-dingus": "git+https://git.squeep.com/squeep-api-dingus/#v1.2.9", - "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.3.1", - "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.4.0", + "@squeep/api-dingus": "^2", + "@squeep/authentication-module": "git+https://git.squeep.com/squeep-authentication-module/#v1.5.0", + "@squeep/html-template-helper": "git+https://git.squeep.com/squeep-html-template-helper#v1.6.1", "@squeep/logger-json-console": "^3", "@squeep/roman": "^1", "@squeep/web-linking": "^1", diff --git a/src/common.js b/src/common.js index 7d0d318..0d6500c 100644 --- a/src/common.js +++ b/src/common.js @@ -164,6 +164,8 @@ const logTruncate = (str, len) => { return str.toString().slice(0, len) + `... (${str.toString().length} bytes)`; }; +const nop = () => undefined; + module.exports = { ...common, arrayChunk, @@ -172,6 +174,7 @@ module.exports = { ensureArray, freezeDeep, logTruncate, + nop, randomBytesAsync, stackSafePush, topicLeaseDefaults, diff --git a/src/db/base.js b/src/db/base.js index 2be2db0..8331fb2 100644 --- a/src/db/base.js +++ b/src/db/base.js @@ -12,9 +12,8 @@ const svh = require('./schema-version-helper'); const _fileScope = common.fileScope(__filename); class Database { - constructor(logger = common.nullLogger, options = {}) { + constructor(logger, options = {}) { this.logger = logger; - common.ensureLoggerLevels(this.logger); // Store the merged config and default values for lease values. // N.B. breaking hierarchy of config options here @@ -327,12 +326,35 @@ class Database { * @param {*} dbCtx db context * @param {string} identifier authentication identifier * @param {string} credential authentication credential + * @param {string=} otpKey authentication otp key */ - async authenticationUpsert(dbCtx, identifier, credential) { + async authenticationUpsert(dbCtx, identifier, credential, otpKey) { this._notImplemented('authenticationUpsert', arguments); } + /** + * Update an authentication entity's otp key. + * @param {*} dbCtx db context + * @param {string} identifier authentication identifier + * @param {string=} otpKey authentication otp key + */ + async authenticationUpdateOTPKey(dbCtx, identifier, otpKey) { + this._notImplemented('authenticationUpdateKey', arguments); + } + + + /** + * Update an authentication entity's credential. + * @param {*} dbCtx db context + * @param {string} identifier authentication identifier + * @param {string} credential authentication credential + */ + async authenticationUpdateCredential(dbCtx, identifier, credential) { + this._notImplemented('authenticationUpdateKey', arguments); + } + + /** * All subscriptions to a topic. * @param {*} dbCtx db context diff --git a/src/db/postgres/index.js b/src/db/postgres/index.js index 9cacaec..78a4dab 100644 --- a/src/db/postgres/index.js +++ b/src/db/postgres/index.js @@ -29,8 +29,8 @@ const schemaVersionsSupported = { }, max: { major: 1, - minor: 0, - patch: 4, + minor: 1, + patch: 0, }, }; @@ -369,17 +369,36 @@ class DatabasePostgres extends Database { } - async authenticationUpsert(dbCtx, identifier, credential) { + async authenticationUpsert(dbCtx, identifier, credential, otpKey) { const _scope = _fileScope('authenticationUpsert'); const scrubbedCredential = '*'.repeat((credential || '').length); - this.logger.debug(_scope, 'called', { identifier, scrubbedCredential }); + const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null; + this.logger.debug(_scope, 'called', { identifier, scrubbedCredential, scrubbedOTPKey }); let result; try { - result = await dbCtx.result(this.statement.authenticationUpsert, { identifier, credential }); + result = await dbCtx.result(this.statement.authenticationUpsert, { identifier, credential, otpKey }); if (result.rowCount != 1) { throw new DBErrors.UnexpectedResult('did not upsert authentication'); } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential, scrubbedOTPKey }); + throw e; + } + } + + + async authenticationUpdateCredential(dbCtx, identifier, credential) { + const _scope = _fileScope('authenticationUpdateCredential'); + const scrubbedCredential = '*'.repeat((credential || '').length); + this.logger.debug(_scope, 'called', { identifier, scrubbedCredential }); + + let result; + try { + result = await dbCtx.result(this.statement.authenticationUpdateCredential, { identifier, credential }); + if (result.rowCount != 1) { + throw new DBErrors.UnexpectedResult('did not update authentication credential'); + } } catch (e) { this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential }); throw e; @@ -387,6 +406,24 @@ class DatabasePostgres extends Database { } + async authenticationUpdateOTPKey(dbCtx, identifier, otpKey) { + const _scope = _fileScope('authenticationUpdateOTPKey'); + const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null; + this.logger.debug(_scope, 'called', { identifier, scrubbedOTPKey }); + + let result; + try { + result = await dbCtx.result(this.statement.authenticationUpdateOtpKey, { identifier, otpKey }); + if (result.rowCount != 1) { + throw new DBErrors.UnexpectedResult('did not update authentication otp key'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedOTPKey }); + throw e; + } + } + + async subscriptionsByTopicId(dbCtx, topicId) { const _scope = _fileScope('subscriptionsByTopicId'); this.logger.debug(_scope, 'called', { topicId }); diff --git a/src/db/postgres/sql/authentication-update-credential.sql b/src/db/postgres/sql/authentication-update-credential.sql new file mode 100644 index 0000000..8d48498 --- /dev/null +++ b/src/db/postgres/sql/authentication-update-credential.sql @@ -0,0 +1,3 @@ +UPDATE authentication + SET credential = $(credential) + WHERE identifier = $(identifier) diff --git a/src/db/postgres/sql/authentication-update-otp-key.sql b/src/db/postgres/sql/authentication-update-otp-key.sql new file mode 100644 index 0000000..b639c69 --- /dev/null +++ b/src/db/postgres/sql/authentication-update-otp-key.sql @@ -0,0 +1,3 @@ +UPDATE authentication + SET otp_key = $(otpKey) + WHERE identifier = $(identifier) diff --git a/src/db/postgres/sql/authentication-upsert.sql b/src/db/postgres/sql/authentication-upsert.sql index e86d4fb..f37892f 100644 --- a/src/db/postgres/sql/authentication-upsert.sql +++ b/src/db/postgres/sql/authentication-upsert.sql @@ -1,9 +1,10 @@ -- INSERT INTO authentication - (identifier, credential) + (identifier, credential, otp_key) VALUES - ($(identifier), $(credential)) + ($(identifier), $(credential), $(otpKey)) ON CONFLICT (identifier) DO UPDATE SET identifier = $(identifier), - credential = $(credential) + credential = $(credential), + otp_key = $(otpKey) diff --git a/src/db/postgres/sql/schema/1.1.0/apply.sql b/src/db/postgres/sql/schema/1.1.0/apply.sql new file mode 100644 index 0000000..64be4d4 --- /dev/null +++ b/src/db/postgres/sql/schema/1.1.0/apply.sql @@ -0,0 +1,7 @@ +BEGIN; + + ALTER TABLE authentication ADD COLUMN otp_key TEXT; + + INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 1, 0); + +COMMIT; \ No newline at end of file diff --git a/src/db/postgres/sql/schema/1.1.0/er.dot b/src/db/postgres/sql/schema/1.1.0/er.dot new file mode 100644 index 0000000..8dcd3b0 --- /dev/null +++ b/src/db/postgres/sql/schema/1.1.0/er.dot @@ -0,0 +1,140 @@ +digraph WebsubHubERD { + graph[ + rankdir=LR, + overlap=false, + splines=true, + label="Websub Hub Entity-Relations\nPostgres\nSchema 1.1.0", + labelloc="t", + fontsize=26, + ]; + // layout=neato; + node[shape=plain]; + edge[arrowhead=crow]; + + topic [label=< + + + + + + + + + + + + + + + + + + + + + +
TOPIC
id
created
url
lease_seconds_preferred
lease_seconds_min
lease_seconds_max
publisher_validation_url
content_hash_algorithm
is_active
is_deleted
last_publish
content_fetch_next_attempt
content_fetch_attempts_since_success
content_updated
content
content_hash
content_type
http_etag
http_last_modified
+ >]; + + topic_fetch_in_progress [label=< + + + + + + +
TOPIC_FETCH_IN_PROGRESS
id
claimant
claimed
claim_expires
+ >]; + topic:pk_id -> topic_fetch_in_progress:fk_id; + + topic_content_history [label=< + + + + + + +
TOPIC_CONTENT_HISTORY
topic_id
content_updated
content_size
content_hash
+ >]; + topic:pk_id -> topic_content_history:fk_topic_id; + + subscription [label=< + + + + + + + + + + + + + + + + +
SUBSCRIPTION
id
created
topic_id
callback
verified
expires
secret
signature_algorithm
http_remote_addr
http_from
content_delivered
latest_content_delivered
delivery_attempts_since_success
delivery_next_attempt
+ >]; + topic:pk_id -> subscription:fk_topic_id; + + subscription_delivery_in_progress [label=< + + + + + + +
SUBSCRIPTION_DELIVERY_IN_PROGRESS
id
claimant
claimed
claim_expires
+ >]; + subscription:pk_id -> subscription_delivery_in_progress:fk_id; + + verification [label=< + + + + + + + + + + + + + + + + + +
VERIFICATION
id
created
topic_id
callback
secret
signature_algorithm
http_remote_addr
http_from
mode
reason
lease_seconds
is_publisher_validated
request_id
attempts
next_attempt
+ >]; + topic:pk_id -> verification:fk_topic_id; + + verification_in_progress [label=< + + + + + + + + +
VERIFICATION_IN_PROGRESS
id
topic_id
callback
claimant
claimed
claim_expires
+ >]; + verification:pk_id -> verification_in_progress:fk_id; + topic:pk_id -> verification_in_progress:fk_topic_id; + + authentication [label=< + + + + + + + +
AUTHENTICATION
created
last_authenticated
identifier
credential
otp_key
+ >]; + +} diff --git a/src/db/postgres/sql/schema/1.1.0/revert.sql b/src/db/postgres/sql/schema/1.1.0/revert.sql new file mode 100644 index 0000000..fbcc115 --- /dev/null +++ b/src/db/postgres/sql/schema/1.1.0/revert.sql @@ -0,0 +1,5 @@ +BEGIN; + ALTER TABLE authentication DROP COLUMN otp_key; + + DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0; +COMMIT; diff --git a/src/db/sqlite/index.js b/src/db/sqlite/index.js index 0325211..6100d49 100644 --- a/src/db/sqlite/index.js +++ b/src/db/sqlite/index.js @@ -19,8 +19,8 @@ const schemaVersionsSupported = { }, max: { major: 1, - minor: 0, - patch: 4, + minor: 1, + patch: 0, }, }; @@ -48,7 +48,7 @@ class DatabaseSQLite extends Database { this.db = new SQLite(dbFilename, sqliteOptions); this.schemaVersionsSupported = schemaVersionsSupported; this.changesSinceLastOptimize = BigInt(0); - this.optimizeAfterChanges = options.db.connectionString.optimizeAfterChanges; + this.optimizeAfterChanges = options.db.optimizeAfterChanges; this.db.pragma('foreign_keys = on'); // Enforce consistency. this.db.pragma('journal_mode = WAL'); // Be faster, expect local filesystem. this.db.defaultSafeIntegers(true); // This probably isn't necessary, but by using these BigInts we keep weird floats out of the query logs. @@ -290,17 +290,54 @@ class DatabaseSQLite extends Database { } - authenticationUpsert(dbCtx, identifier, credential) { + authenticationUpsert(dbCtx, identifier, credential, otpKey) { const _scope = _fileScope('authenticationUpsert'); const scrubbedCredential = '*'.repeat((credential || '').length); - this.logger.debug(_scope, 'called', { identifier, scrubbedCredential }); + const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null; + this.logger.debug(_scope, 'called', { identifier, scrubbedCredential, scrubbedOTPKey }); let result; try { - result = this.statement.authenticationUpsert.run({ identifier, credential }); + result = this.statement.authenticationUpsert.run({ identifier, credential, otpKey }); if (result.changes != 1) { throw new DBErrors.UnexpectedResult('did not upsert authentication'); } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential, scrubbedOTPKey }); + throw e; + } + } + + + authenticationUpdateOTPKey(dbCtx, identifier, otpKey) { + const _scope = _fileScope('authenticationUpdateOTPKey'); + const scrubbedOTPKey = '*'.repeat((otpKey || '').length) || null; + this.logger.debug(_scope, 'called', { identifier, scrubbedOTPKey }); + + let result; + try { + result = this.statement.authenticationUpdateOtpKey.run({ identifier, otpKey }); + if (result.changes != 1) { + throw new DBErrors.UnexpectedResult('did not update authentication otp key'); + } + } catch (e) { + this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedOTPKey }); + throw e; + } + } + + + authenticationUpdateCredential(dbCtx, identifier, credential) { + const _scope = _fileScope('authenticationUpdateCredential'); + const scrubbedCredential = '*'.repeat((credential || '').length); + this.logger.debug(_scope, 'called', { identifier, scrubbedCredential }); + + let result; + try { + result = this.statement.authenticationUpdateCredential.run({ identifier, credential }); + if (result.changes != 1) { + throw new DBErrors.UnexpectedResult('did not update authentication credential'); + } } catch (e) { this.logger.error(_scope, 'failed', { error: e, identifier, scrubbedCredential }); throw e; @@ -1149,4 +1186,4 @@ class DatabaseSQLite extends Database { } -module.exports = DatabaseSQLite; \ No newline at end of file +module.exports = DatabaseSQLite; diff --git a/src/db/sqlite/sql/authentication-update-credential.sql b/src/db/sqlite/sql/authentication-update-credential.sql new file mode 100644 index 0000000..021b9ec --- /dev/null +++ b/src/db/sqlite/sql/authentication-update-credential.sql @@ -0,0 +1,3 @@ +UPDATE authentication + SET credential = :credential + WHERE identifier = :identifier diff --git a/src/db/sqlite/sql/authentication-update-otp-key.sql b/src/db/sqlite/sql/authentication-update-otp-key.sql new file mode 100644 index 0000000..d52506b --- /dev/null +++ b/src/db/sqlite/sql/authentication-update-otp-key.sql @@ -0,0 +1,3 @@ +UPDATE authentication + SET otp_key = :otpKey + WHERE identifier = :identifier diff --git a/src/db/sqlite/sql/authentication-upsert.sql b/src/db/sqlite/sql/authentication-upsert.sql index 8a141ee..b830719 100644 --- a/src/db/sqlite/sql/authentication-upsert.sql +++ b/src/db/sqlite/sql/authentication-upsert.sql @@ -1,9 +1,10 @@ -- INSERT INTO authentication - (identifier, credential) + (identifier, credential, otp_key) VALUES - (:identifier, :credential) + (:identifier, :credential, :otpKey) ON CONFLICT (identifier) DO UPDATE SET identifier = :identifier, - credential = :credential + credential = :credential, + otp_key = :otpKey diff --git a/src/db/sqlite/sql/schema/1.1.0/apply.sql b/src/db/sqlite/sql/schema/1.1.0/apply.sql new file mode 100644 index 0000000..fdfd499 --- /dev/null +++ b/src/db/sqlite/sql/schema/1.1.0/apply.sql @@ -0,0 +1,7 @@ +BEGIN; + + ALTER TABLE authentication ADD COLUMN otp_key TEXT CHECK (typeof(otp_key) IN ('text', 'null')); + + INSERT INTO _meta_schema_version (major, minor, patch) VALUES (1, 1, 0); + +COMMIT; diff --git a/src/db/sqlite/sql/schema/1.1.0/er.dot b/src/db/sqlite/sql/schema/1.1.0/er.dot new file mode 100644 index 0000000..0436d73 --- /dev/null +++ b/src/db/sqlite/sql/schema/1.1.0/er.dot @@ -0,0 +1,140 @@ +digraph WebsubHubERD { + graph[ + rankdir=LR, + overlap=false, + splines=true, + label="Websub Hub Entity-Relations\nSQLite\nSchema 1.1.0", + labelloc="t", + fontsize=26, + ]; + // layout=neato; + node[shape=plain]; + edge[arrowhead=crow]; + + topic [label=< + + + + + + + + + + + + + + + + + + + + + +
TOPIC
id
created
url
lease_seconds_preferred
lease_seconds_min
lease_seconds_max
publisher_validation_url
content_hash_algorithm
is_active
is_deleted
last_publish
content_fetch_next_attempt
content_fetch_attempts_since_success
content_updated
content
content_hash
content_type
http_etag
http_last_modified
+ >]; + + topic_fetch_in_progress [label=< + + + + + + +
TOPIC_FETCH_IN_PROGRESS
id
claimant
claimed
claim_expires
+ >]; + topic:pk_id -> topic_fetch_in_progress:fk_id; + + topic_content_history [label=< + + + + + + +
TOPIC_CONTENT_HISTORY
topic_id
content_updated
content_size
content_hash
+ >]; + topic:pk_id -> topic_content_history:fk_topic_id; + + subscription [label=< + + + + + + + + + + + + + + + + +
SUBSCRIPTION
id
created
topic_id
callback
verified
expires
secret
signature_algorithm
http_remote_addr
http_from
content_delivered
latest_content_delivered
delivery_attempts_since_success
delivery_next_attempt
+ >]; + topic:pk_id -> subscription:fk_topic_id; + + subscription_delivery_in_progress [label=< + + + + + + +
SUBSCRIPTION_DELIVERY_IN_PROGRESS
id
claimant
claimed
claim_expires
+ >]; + subscription:pk_id -> subscription_delivery_in_progress:fk_id; + + verification [label=< + + + + + + + + + + + + + + + + + +
VERIFICATION
id
created
topic_id
callback
secret
signature_algorithm
http_remote_addr
http_from
mode
reason
lease_seconds
is_publisher_validated
request_id
attempts
next_attempt
+ >]; + topic:pk_id -> verification:fk_topic_id; + + verification_in_progress [label=< + + + + + + + + +
VERIFICATION_IN_PROGRESS
id
topic_id
callback
claimant
claimed
claim_expires
+ >]; + verification:pk_id -> verification_in_progress:fk_id; + topic:pk_id -> verification_in_progress:fk_topic_id; + + authentication [label=< + + + + + + + +
AUTHENTICATION
created
last_authenticated
identifier
credential
otp_key
+ >]; + +} diff --git a/src/db/sqlite/sql/schema/1.1.0/revert.sql b/src/db/sqlite/sql/schema/1.1.0/revert.sql new file mode 100644 index 0000000..fbcc115 --- /dev/null +++ b/src/db/sqlite/sql/schema/1.1.0/revert.sql @@ -0,0 +1,5 @@ +BEGIN; + ALTER TABLE authentication DROP COLUMN otp_key; + + DELETE FROM _meta_schema_version WHERE major = 1 AND minor = 1 AND patch = 0; +COMMIT; diff --git a/src/service.js b/src/service.js index 742bcda..fbbb1c4 100644 --- a/src/service.js +++ b/src/service.js @@ -10,6 +10,7 @@ const common = require('./common'); const Enum = require('./enum'); const Manager = require('./manager'); const { Authenticator, SessionManager } = require('@squeep/authentication-module'); +const { initContext, navLinks } = require('./template/template-helper'); const path = require('path'); const _fileScope = common.fileScope(__filename); @@ -31,10 +32,10 @@ class Service extends Dingus { this.on('POST', '/', this.handlerPostRoot.bind(this)); // Information page about service - this.on(['GET', 'HEAD'], '/', this.handlerGetRoot.bind(this)); + this.on(['GET'], '/', this.handlerGetRoot.bind(this)); // Give load-balancers something to check - this.on(['GET', 'HEAD'], '/healthcheck', this.handlerGetHealthcheck.bind(this)); + this.on(['GET'], '/healthcheck', this.handlerGetHealthcheck.bind(this)); // Public information about topics this.on('GET', '/info', this.handlerGetInfo.bind(this)); @@ -42,17 +43,17 @@ class Service extends Dingus { // These routes are intended for accessing static content during development. // In production, a proxy server would likely handle these first. - this.on(['GET', 'HEAD'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`); - this.on(['GET', 'HEAD'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html'); - this.on(['GET', 'HEAD'], '/static/:file', this.handlerGetStaticFile.bind(this)); - this.on(['GET', 'HEAD'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico'); - this.on(['GET', 'HEAD'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt'); + this.on(['GET'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`); + this.on(['GET'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html'); + this.on(['GET'], '/static/:file', this.handlerGetStaticFile.bind(this)); + this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico'); + this.on(['GET'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt'); // Private informational endpoints - this.on(['GET', 'HEAD'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`); - this.on(['GET', 'HEAD'], '/admin/', this.handlerGetAdminOverview.bind(this)); - this.on(['GET', 'HEAD'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this)); - this.on(['GET', 'HEAD'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this)); + this.on(['GET'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`); + this.on(['GET'], '/admin/', this.handlerGetAdminOverview.bind(this)); + this.on(['GET'], '/admin/topic/:topicId', this.handlerGetAdminTopicDetails.bind(this)); + this.on(['GET'], '/admin/topic/:topicId/history.svg', this.handlerGetHistorySVG.bind(this)); // Private data-editing endpoints this.on(['PATCH', 'DELETE'], '/admin/topic/:topicId', this.handlerUpdateTopic.bind(this)); @@ -62,10 +63,12 @@ class Service extends Dingus { this.on('POST', '/admin/process', this.handlerPostAdminProcess.bind(this)); // Admin login - this.on(['GET', 'HEAD'], '/admin/login', this.handlerGetAdminLogin.bind(this)); + this.on(['GET'], '/admin/login', this.handlerGetAdminLogin.bind(this)); this.on(['POST'], '/admin/login', this.handlerPostAdminLogin.bind(this)); this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout.bind(this)); this.on(['GET'], '/admin/_ia', this.handlerGetAdminIA.bind(this)); + this.on(['GET'], '/admin/settings', this.handlerGetAdminSettings.bind(this)); + this.on(['POST'], '/admin/settings', this.handlerPostAdminSettings.bind(this)); } @@ -81,6 +84,8 @@ class Service extends Dingus { */ async preHandler(req, res, ctx) { await super.preHandler(req, res, ctx); + ctx.url = req.url; // Persisted for logout redirect + const logObject = this.asyncLocalStorage.getStore(); // FIXME: for some reason, returning from the super.preHandler sometimes loses async context? // Workaround until cause and solution are found. @@ -102,6 +107,8 @@ class Service extends Dingus { const _scope = _fileScope('handlerPostRoot'); this.logger.debug(_scope, 'called', { req, ctx }); + initContext(ctx); + this.setResponseType(this.responseTypes, req, res, ctx); await this.ingestBody(req, res, ctx); @@ -121,7 +128,7 @@ class Service extends Dingus { ]; this.logger.debug(_scope, 'called', { req, ctx }); - Dingus.setHeadHandler(req, res, ctx); + initContext(ctx); this.setResponseType(responseTypes, req, res, ctx); @@ -139,8 +146,6 @@ class Service extends Dingus { async handlerGetHealthcheck(req, res, ctx) { const _scope = _fileScope('handlerGetHealthcheck'); this.logger.debug(_scope, 'called', { req, ctx }); - - Dingus.setHeadHandler(req, res, ctx); this.setResponseType(this.responseTypes, req, res, ctx); @@ -159,8 +164,6 @@ class Service extends Dingus { const responseTypes = [...this.responseTypes, Enum.ContentType.ImageSVG]; - Dingus.setHeadHandler(req, res, ctx); - this.setResponseType(responseTypes, req, res, ctx); await this.manager.getInfo(res, ctx); @@ -173,8 +176,6 @@ class Service extends Dingus { const responseTypes = [Enum.ContentType.ImageSVG]; - Dingus.setHeadHandler(req, res, ctx); - this.setResponseType(responseTypes, req, res, ctx); await this.manager.getHistorySVG(res, ctx); @@ -190,7 +191,7 @@ class Service extends Dingus { const _scope = _fileScope('handlerGetAdminOverview'); this.logger.debug(_scope, 'called', { req, ctx }); - Dingus.setHeadHandler(req, res, ctx); + initContext(ctx); this.setResponseType(this.responseTypes, req, res, ctx); @@ -209,7 +210,7 @@ class Service extends Dingus { const _scope = _fileScope('handlerGetAdminTopicDetails'); this.logger.debug(_scope, 'called', { req, ctx }); - Dingus.setHeadHandler(req, res, ctx); + initContext(ctx); this.setResponseType(this.responseTypes, req, res, ctx); @@ -269,7 +270,7 @@ class Service extends Dingus { ctx.method = req.method; await this.manager.updateSubscription(res, ctx); } - + /** * @param {http.ClientRequest} req request @@ -298,7 +299,7 @@ class Service extends Dingus { const _scope = _fileScope('handlerGetAdminLogin'); this.logger.debug(_scope, 'called', { req, ctx }); - Dingus.setHeadHandler(req, res, ctx); + initContext(ctx); this.setResponseType(this.responseTypes, req, res, ctx); @@ -316,6 +317,8 @@ class Service extends Dingus { const _scope = _fileScope('handlerPostAdminLogin'); this.logger.debug(_scope, 'called', { req, ctx }); + initContext(ctx); + this.setResponseType(this.responseTypes, req, res, ctx); await this.authenticator.sessionOptionalLocal(req, res, ctx); @@ -326,6 +329,47 @@ class Service extends Dingus { } + /** + * Delegate account settings to authentication module. + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + */ + async handlerGetAdminSettings(req, res, ctx) { + const _scope = _fileScope('handlerGetAdminSettings'); + this.logger.debug(_scope, 'called', { req, ctx }); + + initContext(ctx); + + this.setResponseType(this.responseTypes, req, res, ctx); + + if (await this.authenticator.sessionRequiredLocal(req, res, ctx)) { + await this.sessionManager.getAdminSettings(res, ctx, navLinks); + } + } + + + /** + * Delegate account settings to authentication module. + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + */ + async handlerPostAdminSettings(req, res, ctx) { + const _scope = _fileScope('handlerPostAdminSettings'); + this.logger.debug(_scope, 'called', { req, ctx }); + + initContext(ctx); + + this.setResponseType(this.responseTypes, req, res, ctx); + + if (await this.authenticator.sessionRequiredLocal(req, res, ctx)) { + await this.maybeIngestBody(req, res, ctx); + await this.sessionManager.postAdminSettings(res, ctx, navLinks); + } + } + + /** * Delegate login to authentication module. * @param {http.ClientRequest} req request @@ -336,6 +380,8 @@ class Service extends Dingus { const _scope = _fileScope('handlerGetAdminLogout'); this.logger.debug(_scope, 'called', { req, ctx }); + initContext(ctx); + this.setResponseType(this.responseTypes, req, res, ctx); await this.authenticator.sessionOptionalLocal(req, res, ctx); @@ -354,10 +400,9 @@ class Service extends Dingus { const _scope = _fileScope('handlerGetAdminIA'); this.logger.debug(_scope, 'called', { req, ctx }); - this.setResponseType(this.responseTypes, req, res, ctx); + initContext(ctx); - // Special case here, to see cookie before session established - ctx.cookie = req.getHeader(Enum.Header.Cookie); + this.setResponseType(this.responseTypes, req, res, ctx); await this.sessionManager.getAdminIA(res, ctx); } diff --git a/src/template/admin-overview-html.js b/src/template/admin-overview-html.js index fd0261f..948bdbb 100644 --- a/src/template/admin-overview-html.js +++ b/src/template/admin-overview-html.js @@ -1,6 +1,7 @@ 'use strict'; const th = require('./template-helper'); +const { sessionNavLinks } = require('@squeep/authentication-module'); /** * Show a summary of all topics. @@ -12,6 +13,7 @@ const th = require('./template-helper'); * @returns {string} html */ module.exports = (ctx, options) => { + const pagePathLevel = 1; const pageTitle = `${options.manager.pageTitle} - Topics`; const logoUrl = options.manager.logoUrl; const footerEntries = options.manager.footerEntries; @@ -20,10 +22,13 @@ module.exports = (ctx, options) => { } const htmlOptions = { + pageIdentifier: 'admin', pageTitle, logoUrl, footerEntries, }; + th.navLinks(pagePathLevel, ctx, htmlOptions); + sessionNavLinks(pagePathLevel, ctx, htmlOptions); const content = [ `
@@ -39,5 +44,5 @@ module.exports = (ctx, options) => {
`, ]; - return th.htmlPage(1, ctx, htmlOptions, content); + return th.htmlPage(pagePathLevel, ctx, htmlOptions, content); }; \ No newline at end of file diff --git a/src/template/admin-topic-details-html.js b/src/template/admin-topic-details-html.js index a70c510..b598313 100644 --- a/src/template/admin-topic-details-html.js +++ b/src/template/admin-topic-details-html.js @@ -1,6 +1,7 @@ 'use strict'; const th = require('./template-helper'); +const { sessionNavLinks } = require('@squeep/authentication-module'); /** * Show a topic with all of its subscribers. @@ -13,6 +14,7 @@ const th = require('./template-helper'); * @returns {string} html */ module.exports = (ctx, options) => { + const pagePathLevel = 2; const pageTitle = `${options.manager.pageTitle} - Topic Details`; const logoUrl = options.manager.logoUrl; const navLinks = [ @@ -27,11 +29,14 @@ module.exports = (ctx, options) => { } const htmlOptions = { + pageIdentifier: 'admin', pageTitle, logoUrl, navLinks, footerEntries, }; + th.navLinks(pagePathLevel, ctx, htmlOptions); + sessionNavLinks(pagePathLevel, ctx, htmlOptions); const content = [ `
@@ -68,5 +73,5 @@ module.exports = (ctx, options) => {
`, ]; - return th.htmlPage(2, ctx, htmlOptions, content); + return th.htmlPage(pagePathLevel, ctx, htmlOptions, content); }; \ No newline at end of file diff --git a/src/template/root-html.js b/src/template/root-html.js index d035bb8..575d4bc 100644 --- a/src/template/root-html.js +++ b/src/template/root-html.js @@ -1,6 +1,7 @@ 'use strict'; const th = require('./template-helper'); +const { sessionNavLinks } = require('@squeep/authentication-module'); /** * @@ -164,26 +165,26 @@ ${contactHTML} * @returns {string} html */ module.exports = (ctx, options) => { + const pagePathLevel = 0; const pageTitle = options.manager.pageTitle; const isPublicHub = options.manager.publicHub; const contactHTML = options.adminContactHTML; const footerEntries = options.manager.footerEntries; const hubURL = options.dingus.selfBaseUrl || 'https://hub.example.com/'; - const navLinks = [{ - href: 'admin/', - text: 'Admin', - }]; const htmlOptions = { + pageIdentifier: 'root', pageTitle, logoUrl: options.manager.logoUrl, footerEntries, - navLinks, + navLinks: [], }; + th.navLinks(pagePathLevel, ctx, htmlOptions); + sessionNavLinks(pagePathLevel, ctx, htmlOptions); const content = [ aboutSection(), usageSection(isPublicHub, hubURL), contactSection(contactHTML), hAppSection(pageTitle, options.manager.logoUrl), ]; - return th.htmlPage(0, ctx, htmlOptions, content); + return th.htmlPage(pagePathLevel, ctx, htmlOptions, content); }; \ No newline at end of file diff --git a/src/template/template-helper.js b/src/template/template-helper.js index f891e4f..5850fc1 100644 --- a/src/template/template-helper.js +++ b/src/template/template-helper.js @@ -134,10 +134,33 @@ function xmlEscape(string) { }[c])); } + +/** + * Add common site links to navigation header. + * @param {number} pagePathLevel depth from root + * @param {object} ctx context + * @param {object} options options + */ +function navLinks(pagePathLevel, ctx, options) { + if (!options.navLinks) { + options.navLinks = []; + } + const rootPath = '../'.repeat(pagePathLevel); + + if (options.pageIdentifier !== 'admin') { + options.navLinks.push({ + text: 'Admin', + href: `${rootPath}admin/`, + }); + } +} + + module.exports = Object.assign(Object.create(TemplateHelper), { + navLinks, xmlEscape, renderTopicRowHeader, renderTopicRow, renderSubscriptionRowHeader, renderSubscriptionRow, -}); \ No newline at end of file +}); diff --git a/static/theme.css b/static/theme.css index f6099e2..3939c55 100644 --- a/static/theme.css +++ b/static/theme.css @@ -1,4 +1,3 @@ -* {} html { height: 100vh; } @@ -87,6 +86,9 @@ section + section { .errors ul { border: 1px solid red; } +.otp-key-qr svg { + height: 30vh; +} table { border: 0; width: 100%; diff --git a/test/src/db/factory.js b/test/src/db/factory.js index ebb89ff..d26d51a 100644 --- a/test/src/db/factory.js +++ b/test/src/db/factory.js @@ -2,16 +2,16 @@ const assert = require('node:assert'); const sinon = require('sinon'); -const common = require('../../../src/common'); const DB = require('../../../src/db'); const DBErrors = require('../../../src/db/errors'); const DatabasePostgres = require('../../../src/db/postgres'); const DatabaseSQLite = require('../../../src/db/sqlite'); +const stubLogger = require('../../stub-logger'); describe('DatabaseFactory', function () { let logger, options; beforeEach(function () { - logger = common.nullLogger; + logger = stubLogger, options = { db: { connectionString: '', diff --git a/test/src/db/integration.js b/test/src/db/integration.js index 7eb7d91..eed37e2 100644 --- a/test/src/db/integration.js +++ b/test/src/db/integration.js @@ -83,33 +83,51 @@ describe('Database Integration', function () { }); describe('Authentication', function () { - let identifier, credential; + let identifier, credential, otpKey; beforeEach(function () { identifier = 'username'; credential = 'myEncryptedPassword'; + otpKey = '1234567890123456789012'; }); - step('create auth entry', async function() { + step('create auth entry', async function () { await db.context(async (dbCtx) => { await db.authenticationUpsert(dbCtx, identifier, credential); }); }); - step('get auth entry', async function() { + step('get auth entry', async function () { await db.context(async (dbCtx) => { const authInfo = await db.authenticationGet(dbCtx, identifier); assert.strictEqual(authInfo.credential, credential); }); }); - step('valid auth event', async function() { + step('valid auth event', async function () { await db.context(async (dbCtx) => { await db.authenticationSuccess(dbCtx, identifier); const authInfo = await db.authenticationGet(dbCtx, identifier); assert.notStrictEqual(authInfo.lastAuthentication, undefined); }); }); - step('update auth entry', async function() { + step('update auth entry', async function () { await db.context(async (dbCtx) => { credential = 'myNewPassword'; - await db.authenticationUpsert(dbCtx, identifier, credential); + await db.authenticationUpsert(dbCtx, identifier, credential, otpKey); + const authInfo = await db.authenticationGet(dbCtx, identifier); + assert.strictEqual(authInfo.credential, credential); + assert.strictEqual(authInfo.otpKey, otpKey); + }); + }); + step('update auth otp key', async function () { + await db.context(async (dbCtx) => { + const removedOTPKey = null; + await db.authenticationUpdateOTPKey(dbCtx, identifier, removedOTPKey); + const authInfo = await db.authenticationGet(dbCtx, identifier); + assert.strictEqual(authInfo.otpKey, removedOTPKey); + }); + }); + step('update credential', async function () { + await db.context(async (dbCtx) => { + credential = '$plain$anotherCredential'; + await db.authenticationUpdateCredential(dbCtx, identifier, credential); const authInfo = await db.authenticationGet(dbCtx, identifier); assert.strictEqual(authInfo.credential, credential); }); diff --git a/test/src/db/postgres.js b/test/src/db/postgres.js index 1cdcfda..825a295 100644 --- a/test/src/db/postgres.js +++ b/test/src/db/postgres.js @@ -486,10 +486,11 @@ describe('DatabasePostgres', function () { }); // authenticationGet describe('authenticationUpsert', function () { - let identifier, credential; + let identifier, credential, otpKey; beforeEach(function () { identifier = 'username'; credential = '$z$foo'; + otpKey = '12345678901234567890123456789012'; }); it('success', async function () { const dbResult = { @@ -498,7 +499,7 @@ describe('DatabasePostgres', function () { duration: 22, }; sinon.stub(db.db, 'result').resolves(dbResult); - await db.authenticationUpsert(dbCtx, identifier, credential); + await db.authenticationUpsert(dbCtx, identifier, credential, otpKey); }); it('failure', async function() { credential = undefined; @@ -509,7 +510,7 @@ describe('DatabasePostgres', function () { }; sinon.stub(db.db, 'result').resolves(dbResult); try { - await db.authenticationUpsert(dbCtx, identifier, credential); + await db.authenticationUpsert(dbCtx, identifier, credential, otpKey); assert.fail(noExpectedException); } catch (e) { assert(e instanceof DBErrors.UnexpectedResult); @@ -517,6 +518,68 @@ describe('DatabasePostgres', function () { }); }); // authenticationUpsert + describe('authenticationUpdateCredential', function () { + let identifier, credential; + beforeEach(function () { + identifier = 'username'; + }); + it('success', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await db.authenticationUpdateCredential(dbCtx, identifier, credential); + }); + it('failure', async function() { + credential = undefined; + const dbResult = { + rowCount: 0, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + try { + await db.authenticationUpdateCredential(dbCtx, identifier, credential); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DBErrors.UnexpectedResult); + } + }); + }); // authenticationUpdateCredential + + describe('authenticationUpdateOTPKey', function () { + let identifier, otpKey; + beforeEach(function () { + identifier = 'username'; + otpKey = '12345678901234567890123456789012'; + }); + it('success', async function () { + const dbResult = { + rowCount: 1, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey); + }); + it('failure', async function() { + const dbResult = { + rowCount: 0, + rows: undefined, + duration: 22, + }; + sinon.stub(db.db, 'result').resolves(dbResult); + try { + await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DBErrors.UnexpectedResult); + } + }); + }); // authenticationUpdateOTPKey + describe('subscriptionsByTopicId', function () { it('success', async function () { const expected = []; @@ -1189,11 +1252,17 @@ describe('DatabasePostgres', function () { describe('topicGetByUrl', function () { it('success', async function() { - const expected = []; + const expected = { id: topicId }; sinon.stub(db.db, 'oneOrNone').resolves(expected); const result = await db.topicGetByUrl(dbCtx, topicUrl); assert.deepStrictEqual(result, expected); }); + it('success, no default', async function() { + const expected = { id: topicId }; + sinon.stub(db.db, 'oneOrNone').resolves(expected); + const result = await db.topicGetByUrl(dbCtx, topicUrl, false); + assert.deepStrictEqual(result, expected); + }); it('failure', async function () { const expected = new Error(); sinon.stub(db.db, 'oneOrNone').throws(expected); diff --git a/test/src/db/sqlite.js b/test/src/db/sqlite.js index 8d25fc8..33f0028 100644 --- a/test/src/db/sqlite.js +++ b/test/src/db/sqlite.js @@ -43,6 +43,12 @@ describe('DatabaseSQLite', function () { sinon.restore(); }); + it('covers options', function () { + const xoptions = new Config('test'); + delete xoptions.db.connectionString; + db = new DB(stubLogger, xoptions); + }); + // Ensure all interface methods are implemented describe('Implementation', function () { it('implements interface', async function () { @@ -337,10 +343,11 @@ describe('DatabaseSQLite', function () { }); // authenticationGet describe('authenticationUpsert', function () { - let identifier, credential; + let identifier, credential, otpKey; beforeEach(function () { identifier = 'username'; credential = '$z$foo'; + otpKey = '12345678901234567890123456789012'; }); it('success', async function() { const dbResult = { @@ -348,7 +355,7 @@ describe('DatabaseSQLite', function () { lastInsertRowid: undefined, }; sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult); - await db.authenticationUpsert(dbCtx, identifier, credential); + await db.authenticationUpsert(dbCtx, identifier, credential, otpKey); }); it('failure', async function () { const dbResult = { @@ -357,7 +364,65 @@ describe('DatabaseSQLite', function () { }; sinon.stub(db.statement.authenticationUpsert, 'run').returns(dbResult); try { - await db.authenticationUpsert(dbCtx, identifier, credential); + await db.authenticationUpsert(dbCtx, identifier, credential, otpKey); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DBErrors.UnexpectedResult); + } + }); + }); // authenticationUpsert + + describe('authenticationUpdateCredential', function () { + let identifier, credential; + beforeEach(function () { + identifier = 'username'; + credential = '$z$foo'; + }); + it('success', async function() { + const dbResult = { + changes: 1, + lastInsertRowid: undefined, + }; + sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult); + await db.authenticationUpdateCredential(dbCtx, identifier, credential); + }); + it('failure', async function () { + const dbResult = { + changes: 0, + lastInsertRowid: undefined, + }; + sinon.stub(db.statement.authenticationUpdateCredential, 'run').returns(dbResult); + try { + await db.authenticationUpdateCredential(dbCtx, identifier, credential); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof DBErrors.UnexpectedResult); + } + }); + }); // authenticationUpdateCredential + + describe('authenticationUpdateOTPKey', function () { + let identifier, otpKey; + beforeEach(function () { + identifier = 'username'; + otpKey = '12345678901234567890123456789012'; + }); + it('success', async function() { + const dbResult = { + changes: 1, + lastInsertRowid: undefined, + }; + sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult); + await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey); + }); + it('failure', async function () { + const dbResult = { + changes: 0, + lastInsertRowid: undefined, + }; + sinon.stub(db.statement.authenticationUpdateOtpKey, 'run').returns(dbResult); + try { + await db.authenticationUpdateOTPKey(dbCtx, identifier, otpKey); assert.fail(noExpectedException); } catch (e) { assert(e instanceof DBErrors.UnexpectedResult); @@ -1042,6 +1107,12 @@ describe('DatabaseSQLite', function () { const result = await db.topicGetByUrl(dbCtx, topicUrl); assert.deepStrictEqual(result, expected); }); + it('success, no defaults', async function() { + const expected = []; + sinon.stub(db.statement.topicGetByUrl, 'get').returns(expected); + const result = await db.topicGetByUrl(dbCtx, topicUrl, false); + assert.deepStrictEqual(result, expected); + }); it('failure', async function () { const expected = new Error(); sinon.stub(db.statement.topicGetByUrl, 'get').throws(expected); diff --git a/test/src/service.js b/test/src/service.js index 8be37e2..2cfe38d 100644 --- a/test/src/service.js +++ b/test/src/service.js @@ -188,6 +188,34 @@ describe('Service', function () { }); }); // handlerGetAdminLogin + describe('handlerGetAdminSettings', function () { + it('covers logged in', async function () { + service.authenticator.sessionRequiredLocal.resolves(true); + await service.handlerGetAdminSettings(req, res, ctx); + assert(service.sessionManager.getAdminSettings.called); + }); + it('covers not logged in', async function () { + service.authenticator.sessionRequiredLocal.resolves(false); + await service.handlerGetAdminSettings(req, res, ctx); + assert(service.sessionManager.getAdminSettings.notCalled); + }); + }); // handlerGetAdminSettings + + describe('handlerPostAdminSettings', function () { + it('covers logged in', async function () { + service.authenticator.sessionRequiredLocal.resolves(true); + sinon.stub(service, 'bodyData').resolves(); + await service.handlerPostAdminSettings(req, res, ctx); + assert(service.sessionManager.postAdminSettings.called); + }); + it('covers logged outo', async function () { + service.authenticator.sessionRequiredLocal.resolves(false); + sinon.stub(service, 'bodyData').resolves(); + await service.handlerPostAdminSettings(req, res, ctx); + assert(service.sessionManager.postAdminSettings.notCalled); + }); + }); // handlerPostAdminSettings + describe('handlerPostAdminLogin', function () { it('covers', async function () { sinon.stub(service, 'bodyData').resolves(); diff --git a/test/stub-db.js b/test/stub-db.js index e0c7ad0..129c907 100644 --- a/test/stub-db.js +++ b/test/stub-db.js @@ -12,6 +12,8 @@ const stubFns = [ 'authenticationSuccess', 'authenticationGet', 'authenticationUpsert', + 'authenticationUpdateCredential', + 'authenticationUpdateOTPKey', 'healthCheck', 'initialize', 'subscriptionsByTopicId', -- 2.44.2