From 2426f79cffcbaf0bb63a163490fff64aec525805 Mon Sep 17 00:00:00 2001 From: apilat Date: Thu, 22 Jun 2023 00:05:18 +0100 Subject: [PATCH 001/139] OpenID implementation --- package.json | 1 + src/account-db.js | 211 ++- src/app-account.js | 37 +- src/load-config.js | 2 +- src/scripts/reset-password.js | 2 +- src/sql/account.sql | 10 +- yarn.lock | 2471 ++++++++++++++++----------------- 7 files changed, 1457 insertions(+), 1277 deletions(-) diff --git a/package.json b/package.json index a0c4ab6a3..db6c2b43f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "express-response-size": "^0.0.3", "jws": "^4.0.0", "nordigen-node": "^1.2.3", + "openid-client": "^5.4.2", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/src/account-db.js b/src/account-db.js index 3c1e5845c..393d94eba 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -5,6 +5,7 @@ import config, { sqlDir } from './load-config.js'; import createDebug from 'debug'; import * as uuid from 'uuid'; import * as bcrypt from 'bcrypt'; +import { generators, Issuer } from 'openid-client'; const debug = createDebug('actual:account-db'); @@ -40,36 +41,87 @@ function hashPassword(password) { export function needsBootstrap() { let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT * FROM auth'); - return rows.length === 0; + let row = accountDb.first('SELECT count(*) FROM auth'); + return row['count(*)'] === 0; } -export function bootstrap(password) { +// Supported login settings: +// "password": "secret_password", +// "openid": { +// "issuer": "https://example.org", +// "client_id": "your_client_id", +// "client_secret": "your_client_secret", +// "server_hostname": "https://actual.your_website.com" +// } +export function bootstrap(loginSettings) { let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT * FROM auth'); - if (rows.length !== 0) { + if (!needsBootstrap()) { return { error: 'already-bootstrapped' }; } - if (password == null || password === '') { - return { error: 'invalid-password' }; + if ( + !loginSettings.hasOwnProperty('password') && + !loginSettings.hasOwnProperty('openid') + ) { + return { error: 'no-auth-method-selected' }; } - // Hash the password. There's really not a strong need for this - // since this is a self-hosted instance owned by the user. - // However, just in case we do it. - let hashed = hashPassword(password); - accountDb.mutate('INSERT INTO auth (password) VALUES (?)', [hashed]); + if (loginSettings.hasOwnProperty('password')) { + const password = loginSettings.password; + if (password === null || password === '') { + return { error: 'invalid-password' }; + } + } - let token = uuid.v4(); - accountDb.mutate('INSERT INTO sessions (token) VALUES (?)', [token]); - return { token }; + if (loginSettings.hasOwnProperty('openid')) { + const config = loginSettings.openid; + if (!config.hasOwnProperty('issuer')) { + return { error: 'missing-issuer' }; + } + if (!config.hasOwnProperty('client_id')) { + return { error: 'missing-client-id' }; + } + if (!config.hasOwnProperty('client_secret')) { + return { error: 'missing-client-secret' }; + } + if (!config.hasOwnProperty('server_hostname')) { + return { error: 'missing-server-hostname' }; + } + // Beyond verifying that the configuration exists, we do not attempt + // to check if the configuration is actually correct. + // If the user improperly configures this during bootstrap, there is + // no way to recover without manually editing the database. However, + // this might not be a real issue since an analogous situation happens + // if they forget their password. + } + + if (loginSettings.hasOwnProperty('password')) { + // Hash the password. There's really not a strong need for this + // since this is a self-hosted instance owned by the user. + // However, just in case we do it. + let hashed = hashPassword(loginSettings.password); + accountDb.mutate( + "INSERT INTO auth (method, extra_data) VALUES ('password', ?)", + [hashed], + ); + } + + if (loginSettings.hasOwnProperty('openid')) { + accountDb.mutate( + "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", + [JSON.stringify(loginSettings.openid)], + ); + } + + return {}; } -export function login(password) { +export function loginWithPassword(password) { let accountDb = getAccountDb(); - let row = accountDb.first('SELECT * FROM auth'); + let row = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'password'", + ); let confirmed = row && bcrypt.compareSync(password, row.password); if (confirmed) { @@ -84,6 +136,116 @@ export function login(password) { } } +async function setupOpenIdClient(config) { + const issuer = await Issuer.discover(config.issuer); + + const client = new issuer.Client({ + client_id: config.client_id, + client_secret: config.client_secret, + redirect_uri: config.server_hostname + '/account/login-openid/cb', + }); + + return client; +} + +export async function loginWithOpenIdSetup(body) { + if (!body.return_url) { + return { error: 'return-url-missing' }; + } + + let accountDb = getAccountDb(); + let config = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'openid'", + ); + if (!config) { + return { error: 'openid-not-configured' }; + } + config = JSON.parse(config['extra_data']); + + let client; + try { + client = await setupOpenIdClient(config); + } catch (err) { + return { error: 'openid-setup-failed: ' + err }; + } + + const state = generators.state(); + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + + const now_time = Date.now(); + const expiry_time = now_time + 300 * 1000; + + accountDb.mutate( + 'DELETE FROM pending_openid_requests WHERE expiry_time < ?', + [now_time], + ); + accountDb.mutate( + 'INSERT INTO pending_openid_requests (state, code_verifier, return_url, expiry_time) VALUES (?, ?, ?, ?)', + [state, code_verifier, body.return_url, expiry_time], + ); + + const url = client.authorizationUrl({ + response_type: 'code', + scope: 'openid', + state, + code_challenge, + code_challenge_method: 'S256', + }); + + return { url }; +} + +export async function loginWithOpenIdFinalize(body) { + if (!body.code) { + return { error: 'missing-authorization-code' }; + } + if (!body.state) { + return { error: 'missing-state' }; + } + + let accountDb = getAccountDb(); + let config = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'openid'", + ); + if (!config) { + return { error: 'openid-not-configured' }; + } + config = JSON.parse(config['extra_data']); + + let client; + try { + client = await setupOpenIdClient(config); + } catch (err) { + return { error: 'openid-setup-failed: ' + err }; + } + + let { code_verifier, return_url } = accountDb.first( + 'SELECT code_verifier, return_url FROM pending_openid_requests WHERE state = ? AND expiry_time > ?', + [body.state, Date.now()], + ); + + try { + let grant = await client.grant({ + grant_type: 'authorization_code', + code: body.code, + code_verifier, + redirect_uri: client.redirect_uris[0], + }); + await client.userinfo(grant); + // The server requests have succeeded, so the user has been authenticated. + // Ideally, we would create a session token here tied to the returned access token + // and verify it with the server whenever the user connects. + // However, the rest of this server code uses only a single permanent token, + // so that is what we do here as well. + } catch (err) { + return { error: 'openid-grant-failed: ' + err }; + } + + let { token } = accountDb.first('SELECT token FROM sessions'); + return { url: `${return_url}/login/openid-cb?token=${token}` }; +} + export function changePassword(newPassword) { let accountDb = getAccountDb(); @@ -93,9 +255,12 @@ export function changePassword(newPassword) { let hashed = hashPassword(newPassword); let token = uuid.v4(); - // Note that this doesn't have a WHERE. This table only ever has 1 - // row (maybe that will change in the future? if so this will not work) - accountDb.mutate('UPDATE auth SET password = ?', [hashed]); + // This query does nothing if password authentication was disabled during + // bootstrap (then no row with method=password exists). Maybe we should + // return an error here if that is the case? + accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [ + hashed, + ]); accountDb.mutate('UPDATE sessions SET token = ?', [token]); return {}; } @@ -104,3 +269,9 @@ export function getSession(token) { let accountDb = getAccountDb(); return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); } + +export function listLoginMethods() { + let accountDb = getAccountDb(); + let rows = accountDb.all('SELECT method FROM auth'); + return rows.map((r) => r['method']); +} diff --git a/src/app-account.js b/src/app-account.js index a5cd0b8a2..594317dd8 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -3,7 +3,10 @@ import errorMiddleware from './util/error-middleware.js'; import validateUser from './util/validate-user.js'; import { bootstrap, - login, + listLoginMethods, + loginWithPassword, + loginWithOpenIdSetup, + loginWithOpenIdFinalize, changePassword, needsBootstrap, } from './account-db.js'; @@ -31,21 +34,47 @@ app.get('/needs-bootstrap', (req, res) => { }); app.post('/bootstrap', (req, res) => { - let { error, token } = bootstrap(req.body.password); + let { error } = bootstrap(req.body); if (error) { res.status(400).send({ status: 'error', reason: error }); return; } else { - res.send({ status: 'ok', data: { token } }); + res.send({ status: 'ok' }); } }); +app.get('/login-methods', (req, res) => { + let methods = listLoginMethods(); + res.send({ status: 'ok', methods }); +}); + app.post('/login', (req, res) => { - let token = login(req.body.password); + let token = loginWithPassword(req.body.password); res.send({ status: 'ok', data: { token } }); }); +app.post('/login-openid', async (req, res) => { + // req.body needs to contain + // - return_url: address of the actual frontend which we should return to after the openid flow + let { error, url } = await loginWithOpenIdSetup(req.body); + if (error) { + res.send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok', data: { redirect_url: url } }); +}); + +app.get('/login-openid/cb', async (req, res) => { + let { error, url } = await loginWithOpenIdFinalize(req.query); + if (error) { + res.send({ error }); + return; + } + + res.redirect(url); +}); + app.post('/change-password', (req, res) => { let user = validateUser(req, res); if (!user) return; diff --git a/src/load-config.js b/src/load-config.js index 45fecc262..ead48d462 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -10,7 +10,7 @@ const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); debug(`project root: '${projectRoot}'`); export const sqlDir = path.join(projectRoot, 'src', 'sql'); -let defaultDataDir = fs.existsSync('/data') ? '/data' : projectRoot; +let defaultDataDir = projectRoot; //fs.existsSync('/data') ? '/data' : projectRoot; debug(`default data directory: '${defaultDataDir}'`); function parseJSON(path, allowMissing = false) { diff --git a/src/scripts/reset-password.js b/src/scripts/reset-password.js index 26d5b1638..c1aff8943 100644 --- a/src/scripts/reset-password.js +++ b/src/scripts/reset-password.js @@ -7,7 +7,7 @@ if (needsBootstrap()) { ); promptPassword().then((password) => { - let { error } = bootstrap(password); + let { error } = bootstrap({ password }); if (error) { console.log('Error setting password:', error); console.log( diff --git a/src/sql/account.sql b/src/sql/account.sql index 8fe266200..70aafec4e 100644 --- a/src/sql/account.sql +++ b/src/sql/account.sql @@ -1,10 +1,16 @@ - CREATE TABLE auth - (password TEXT PRIMARY KEY); + (method TEXT PRIMARY KEY, + extra_data TEXT); CREATE TABLE sessions (token TEXT PRIMARY KEY); +CREATE TABLE pending_openid_requests + (state TEXT PRIMARY KEY, + code_verifier TEXT, + return_url TEXT, + expiry_time INTEGER); + CREATE TABLE files (id TEXT PRIMARY KEY, group_id TEXT, diff --git a/yarn.lock b/yarn.lock index 3bde01479..3ff014067 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,337 +23,276 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.1.0": - version: 2.2.0 - resolution: "@ampproject/remapping@npm:2.2.0" +"@ampproject/remapping@npm:^2.2.0": + version: 2.2.1 + resolution: "@ampproject/remapping@npm:2.2.1" dependencies: - "@jridgewell/gen-mapping": ^0.1.0 + "@jridgewell/gen-mapping": ^0.3.0 "@jridgewell/trace-mapping": ^0.3.9 - checksum: d74d170d06468913921d72430259424b7e4c826b5a7d39ff839a29d547efb97dc577caa8ba3fb5cf023624e9af9d09651afc3d4112a45e2050328abc9b3a2292 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/code-frame@npm:7.18.6" - dependencies: - "@babel/highlight": ^7.18.6 - checksum: 195e2be3172d7684bf95cff69ae3b7a15a9841ea9d27d3c843662d50cdd7d6470fd9c8e64be84d031117e4a4083486effba39f9aef6bbb2c89f7f21bcfba33ba + checksum: 03c04fd526acc64a1f4df22651186f3e5ef0a9d6d6530ce4482ec9841269cf7a11dbb8af79237c282d721c5312024ff17529cd72cc4768c11e999b58e2302079 languageName: node linkType: hard -"@babel/code-frame@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/code-frame@npm:7.21.4" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/code-frame@npm:7.22.5" dependencies: - "@babel/highlight": ^7.18.6 - checksum: e5390e6ec1ac58dcef01d4f18eaf1fd2f1325528661ff6d4a5de8979588b9f5a8e852a54a91b923846f7a5c681b217f0a45c2524eb9560553160cd963b7d592c + "@babel/highlight": ^7.22.5 + checksum: cfe804f518f53faaf9a1d3e0f9f74127ab9a004912c3a16fda07fb6a633393ecb9918a053cb71804204c1b7ec3d49e1699604715e2cfb0c9f7bc4933d324ebb6 languageName: node linkType: hard -"@babel/compat-data@npm:^7.20.5": - version: 7.20.14 - resolution: "@babel/compat-data@npm:7.20.14" - checksum: 6c9efe36232094e4ad0b70d165587f21ca718e5d011f7a52a77a18502a7524e90e2855aa5a2e086395bcfd21bd2c7c99128dcd8d9fdffe94316b72acf5c66f2c +"@babel/compat-data@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/compat-data@npm:7.22.5" + checksum: eb1a47ebf79ae268b4a16903e977be52629339806e248455eb9973897c503a04b701f36a9de64e19750d6e081d0561e77a514c8dc470babbeba59ae94298ed18 languageName: node linkType: hard "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": - version: 7.20.12 - resolution: "@babel/core@npm:7.20.12" - dependencies: - "@ampproject/remapping": ^2.1.0 - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.20.7 - "@babel/helper-compilation-targets": ^7.20.7 - "@babel/helper-module-transforms": ^7.20.11 - "@babel/helpers": ^7.20.7 - "@babel/parser": ^7.20.7 - "@babel/template": ^7.20.7 - "@babel/traverse": ^7.20.12 - "@babel/types": ^7.20.7 + version: 7.22.5 + resolution: "@babel/core@npm:7.22.5" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.22.5 + "@babel/generator": ^7.22.5 + "@babel/helper-compilation-targets": ^7.22.5 + "@babel/helper-module-transforms": ^7.22.5 + "@babel/helpers": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 convert-source-map: ^1.7.0 debug: ^4.1.0 gensync: ^1.0.0-beta.2 json5: ^2.2.2 semver: ^6.3.0 - checksum: 62e6c3e2149a70b5c9729ef5f0d3e2e97e9dcde89fc039c8d8e3463d5d7ba9b29ee84d10faf79b61532ac1645aa62f2bd42338320617e6e3a8a4d8e2a27076e7 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.20.7, @babel/generator@npm:^7.7.2": - version: 7.20.14 - resolution: "@babel/generator@npm:7.20.14" - dependencies: - "@babel/types": ^7.20.7 - "@jridgewell/gen-mapping": ^0.3.2 - jsesc: ^2.5.1 - checksum: 5f6aa2d86af26e76d276923a5c34191124a119b16ee9ccc34aef654a7dec84fbd7d2daed2e6458a6a06bf87f3661deb77c9fea59b8f67faff5c90793c96d76d6 + checksum: 173ae426958c90c7bbd7de622c6f13fcab8aef0fac3f138e2d47bc466d1cd1f86f71ca82ae0acb9032fd8794abed8efb56fea55c031396337eaec0d673b69d56 languageName: node linkType: hard -"@babel/generator@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/generator@npm:7.21.4" +"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/generator@npm:7.22.5" dependencies: - "@babel/types": ^7.21.4 + "@babel/types": ^7.22.5 "@jridgewell/gen-mapping": ^0.3.2 "@jridgewell/trace-mapping": ^0.3.17 jsesc: ^2.5.1 - checksum: 9ffbb526a53bb8469b5402f7b5feac93809b09b2a9f82fcbfcdc5916268a65dae746a1f2479e03ba4fb0776facd7c892191f63baa61ab69b2cfdb24f7b92424d + checksum: efa64da70ca88fe69f05520cf5feed6eba6d30a85d32237671488cc355fdc379fe2c3246382a861d49574c4c2f82a317584f8811e95eb024e365faff3232b49d languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-annotate-as-pure@npm:7.18.6" +"@babel/helper-annotate-as-pure@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" dependencies: - "@babel/types": ^7.18.6 - checksum: 88ccd15ced475ef2243fdd3b2916a29ea54c5db3cd0cfabf9d1d29ff6e63b7f7cd1c27264137d7a40ac2e978b9b9a542c332e78f40eb72abe737a7400788fc1b + "@babel/types": ^7.22.5 + checksum: 53da330f1835c46f26b7bf4da31f7a496dee9fd8696cca12366b94ba19d97421ce519a74a837f687749318f94d1a37f8d1abcbf35e8ed22c32d16373b2f6198d languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.20.7": - version: 7.20.7 - resolution: "@babel/helper-compilation-targets@npm:7.20.7" +"@babel/helper-compilation-targets@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-compilation-targets@npm:7.22.5" dependencies: - "@babel/compat-data": ^7.20.5 - "@babel/helper-validator-option": ^7.18.6 + "@babel/compat-data": ^7.22.5 + "@babel/helper-validator-option": ^7.22.5 browserslist: ^4.21.3 lru-cache: ^5.1.1 semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: 8c32c873ba86e2e1805b30e0807abd07188acbe00ebb97576f0b09061cc65007f1312b589eccb4349c5a8c7f8bb9f2ab199d41da7030bf103d9f347dcd3a3cf4 + checksum: a479460615acffa0f4fd0a29b740eafb53a93694265207d23a6038ccd18d183a382cacca515e77b7c9b042c3ba80b0aca0da5f1f62215140e81660d2cf721b68 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.21.0": - version: 7.21.4 - resolution: "@babel/helper-create-class-features-plugin@npm:7.21.4" +"@babel/helper-create-class-features-plugin@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-create-class-features-plugin@npm:7.22.5" dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.21.0 - "@babel/helper-member-expression-to-functions": ^7.21.0 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/helper-replace-supers": ^7.20.7 - "@babel/helper-skip-transparent-expression-wrappers": ^7.20.0 - "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-function-name": ^7.22.5 + "@babel/helper-member-expression-to-functions": ^7.22.5 + "@babel/helper-optimise-call-expression": ^7.22.5 + "@babel/helper-replace-supers": ^7.22.5 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + semver: ^6.3.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: 9123ca80a4894aafdb1f0bc08e44f6be7b12ed1fbbe99c501b484f9b1a17ff296b6c90c18c222047d53c276f07f17b4de857946fa9d0aa207023b03e4cc716f2 - languageName: node - linkType: hard - -"@babel/helper-environment-visitor@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/helper-environment-visitor@npm:7.18.9" - checksum: b25101f6162ddca2d12da73942c08ad203d7668e06663df685634a8fde54a98bc015f6f62938e8554457a592a024108d45b8f3e651fd6dcdb877275b73cc4420 - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.19.0": - version: 7.19.0 - resolution: "@babel/helper-function-name@npm:7.19.0" - dependencies: - "@babel/template": ^7.18.10 - "@babel/types": ^7.19.0 - checksum: eac1f5db428ba546270c2b8d750c24eb528b8fcfe50c81de2e0bdebf0e20f24bec688d4331533b782e4a907fad435244621ca2193cfcf80a86731299840e0f6e + checksum: f1e91deae06dbee6dd956c0346bca600adfbc7955427795d9d8825f0439a3c3290c789ba2b4a02a1cdf6c1a1bd163dfa16d3d5e96b02a8efb639d2a774e88ed9 languageName: node linkType: hard -"@babel/helper-function-name@npm:^7.21.0": - version: 7.21.0 - resolution: "@babel/helper-function-name@npm:7.21.0" - dependencies: - "@babel/template": ^7.20.7 - "@babel/types": ^7.21.0 - checksum: d63e63c3e0e3e8b3138fa47b0cd321148a300ef12b8ee951196994dcd2a492cc708aeda94c2c53759a5c9177fffaac0fd8778791286746f72a000976968daf4e +"@babel/helper-environment-visitor@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-environment-visitor@npm:7.22.5" + checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 languageName: node linkType: hard -"@babel/helper-hoist-variables@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-hoist-variables@npm:7.18.6" +"@babel/helper-function-name@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-function-name@npm:7.22.5" dependencies: - "@babel/types": ^7.18.6 - checksum: fd9c35bb435fda802bf9ff7b6f2df06308a21277c6dec2120a35b09f9de68f68a33972e2c15505c1a1a04b36ec64c9ace97d4a9e26d6097b76b4396b7c5fa20f + "@babel/template": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.20.7, @babel/helper-member-expression-to-functions@npm:^7.21.0": - version: 7.21.0 - resolution: "@babel/helper-member-expression-to-functions@npm:7.21.0" +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" dependencies: - "@babel/types": ^7.21.0 - checksum: 49cbb865098195fe82ba22da3a8fe630cde30dcd8ebf8ad5f9a24a2b685150c6711419879cf9d99b94dad24cff9244d8c2a890d3d7ec75502cd01fe58cff5b5d + "@babel/types": ^7.22.5 + checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-module-imports@npm:7.18.6" +"@babel/helper-member-expression-to-functions@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.22.5" dependencies: - "@babel/types": ^7.18.6 - checksum: f393f8a3b3304b1b7a288a38c10989de754f01d29caf62ce7c4e5835daf0a27b81f3ac687d9d2780d39685aae7b55267324b512150e7b2be967b0c493b6a1def + "@babel/types": ^7.22.5 + checksum: 4bd5791529c280c00743e8bdc669ef0d4cd1620d6e3d35e0d42b862f8262bc2364973e5968007f960780344c539a4b9cf92ab41f5b4f94560a9620f536de2a39 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.20.11": - version: 7.20.11 - resolution: "@babel/helper-module-transforms@npm:7.20.11" +"@babel/helper-module-imports@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-imports@npm:7.22.5" dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.20.2 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/helper-validator-identifier": ^7.19.1 - "@babel/template": ^7.20.7 - "@babel/traverse": ^7.20.10 - "@babel/types": ^7.20.7 - checksum: 29319ebafa693d48756c6ba0d871677bb0037e0da084fbe221a17c38d57093fc8aa38543c07d76e788266a937976e37ab4901971ca7f237c5ab45f524b9ecca0 + "@babel/types": ^7.22.5 + checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.21.2": - version: 7.21.2 - resolution: "@babel/helper-module-transforms@npm:7.21.2" +"@babel/helper-module-transforms@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-transforms@npm:7.22.5" dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-module-imports": ^7.18.6 - "@babel/helper-simple-access": ^7.20.2 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/helper-validator-identifier": ^7.19.1 - "@babel/template": ^7.20.7 - "@babel/traverse": ^7.21.2 - "@babel/types": ^7.21.2 - checksum: 8a1c129a4f90bdf97d8b6e7861732c9580f48f877aaaafbc376ce2482febebcb8daaa1de8bc91676d12886487603f8c62a44f9e90ee76d6cac7f9225b26a49e1 + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-module-imports": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 8985dc0d971fd17c467e8b84fe0f50f3dd8610e33b6c86e5b3ca8e8859f9448bcc5c84e08a2a14285ef388351c0484797081c8f05a03770bf44fc27bf4900e68 languageName: node linkType: hard -"@babel/helper-optimise-call-expression@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-optimise-call-expression@npm:7.18.6" +"@babel/helper-optimise-call-expression@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-optimise-call-expression@npm:7.22.5" dependencies: - "@babel/types": ^7.18.6 - checksum: e518fe8418571405e21644cfb39cf694f30b6c47b10b006609a92469ae8b8775cbff56f0b19732343e2ea910641091c5a2dc73b56ceba04e116a33b0f8bd2fbd + "@babel/types": ^7.22.5 + checksum: c70ef6cc6b6ed32eeeec4482127e8be5451d0e5282d5495d5d569d39eb04d7f1d66ec99b327f45d1d5842a9ad8c22d48567e93fc502003a47de78d122e355f7c languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.19.0, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.20.2 - resolution: "@babel/helper-plugin-utils@npm:7.20.2" - checksum: f6cae53b7fdb1bf3abd50fa61b10b4470985b400cc794d92635da1e7077bb19729f626adc0741b69403d9b6e411cddddb9c0157a709cc7c4eeb41e663be5d74b +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.20.7": - version: 7.20.7 - resolution: "@babel/helper-replace-supers@npm:7.20.7" +"@babel/helper-replace-supers@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-replace-supers@npm:7.22.5" dependencies: - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-member-expression-to-functions": ^7.20.7 - "@babel/helper-optimise-call-expression": ^7.18.6 - "@babel/template": ^7.20.7 - "@babel/traverse": ^7.20.7 - "@babel/types": ^7.20.7 - checksum: b8e0087c9b0c1446e3c6f3f72b73b7e03559c6b570e2cfbe62c738676d9ebd8c369a708cf1a564ef88113b4330750a50232ee1131d303d478b7a5e65e46fbc7c + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-member-expression-to-functions": ^7.22.5 + "@babel/helper-optimise-call-expression": ^7.22.5 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: af29deff6c6dc3fa2d1a517390716aa3f4d329855e8689f1d5c3cb07c1b898e614a5e175f1826bb58e9ff1480e6552885a71a9a0ba5161787aaafa2c79b216cc languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.20.2": - version: 7.20.2 - resolution: "@babel/helper-simple-access@npm:7.20.2" +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" dependencies: - "@babel/types": ^7.20.2 - checksum: ad1e96ee2e5f654ffee2369a586e5e8d2722bf2d8b028a121b4c33ebae47253f64d420157b9f0a8927aea3a9e0f18c0103e74fdd531815cf3650a0a4adca11a1 + "@babel/types": ^7.22.5 + checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2 languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0": - version: 7.20.0 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.20.0" +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.22.5" dependencies: - "@babel/types": ^7.20.0 - checksum: 34da8c832d1c8a546e45d5c1d59755459ffe43629436707079989599b91e8c19e50e73af7a4bd09c95402d389266731b0d9c5f69e372d8ebd3a709c05c80d7dd + "@babel/types": ^7.22.5 + checksum: 1012ef2295eb12dc073f2b9edf3425661e9b8432a3387e62a8bc27c42963f1f216ab3124228015c748770b2257b4f1fda882ca8fa34c0bf485e929ae5bc45244 languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-split-export-declaration@npm:7.18.6" +"@babel/helper-split-export-declaration@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-split-export-declaration@npm:7.22.5" dependencies: - "@babel/types": ^7.18.6 - checksum: c6d3dede53878f6be1d869e03e9ffbbb36f4897c7cc1527dc96c56d127d834ffe4520a6f7e467f5b6f3c2843ea0e81a7819d66ae02f707f6ac057f3d57943a2b - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.19.4": - version: 7.19.4 - resolution: "@babel/helper-string-parser@npm:7.19.4" - checksum: b2f8a3920b30dfac81ec282ac4ad9598ea170648f8254b10f475abe6d944808fb006aab325d3eb5a8ad3bea8dfa888cfa6ef471050dae5748497c110ec060943 + "@babel/types": ^7.22.5 + checksum: d10e05a02f49c1f7c578cea63d2ac55356501bbf58856d97ac9bfde4957faee21ae97c7f566aa309e38a256eef58b58e5b670a7f568b362c00e93dfffe072650 languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": - version: 7.19.1 - resolution: "@babel/helper-validator-identifier@npm:7.19.1" - checksum: 0eca5e86a729162af569b46c6c41a63e18b43dbe09fda1d2a3c8924f7d617116af39cac5e4cd5d431bb760b4dca3c0970e0c444789b1db42bcf1fa41fbad0a3a +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/helper-validator-option@npm:7.18.6" - checksum: f9cc6eb7cc5d759c5abf006402180f8d5e4251e9198197428a97e05d65eb2f8ae5a0ce73b1dfd2d35af41d0eb780627a64edf98a4e71f064eeeacef8de58f2cf +"@babel/helper-validator-identifier@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-identifier@npm:7.22.5" + checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.21.0": - version: 7.21.0 - resolution: "@babel/helper-validator-option@npm:7.21.0" - checksum: 8ece4c78ffa5461fd8ab6b6e57cc51afad59df08192ed5d84b475af4a7193fc1cb794b59e3e7be64f3cdc4df7ac78bf3dbb20c129d7757ae078e6279ff8c2f07 +"@babel/helper-validator-option@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-option@npm:7.22.5" + checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 languageName: node linkType: hard -"@babel/helpers@npm:^7.20.7": - version: 7.20.13 - resolution: "@babel/helpers@npm:7.20.13" +"@babel/helpers@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helpers@npm:7.22.5" dependencies: - "@babel/template": ^7.20.7 - "@babel/traverse": ^7.20.13 - "@babel/types": ^7.20.7 - checksum: d62076fa834f342798f8c3fd7aec0870cc1725d273d99e540cbaa8d6c3ed10258228dd14601c8e66bfeabbb9424c3b31090ecc467fe855f7bd72c4734df7fb09 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: a96e785029dff72f171190943df895ab0f76e17bf3881efd630bc5fae91215042d1c2e9ed730e8e4adf4da6f28b24bd1f54ed93b90ffbca34c197351872a084e languageName: node linkType: hard -"@babel/highlight@npm:^7.18.6": - version: 7.18.6 - resolution: "@babel/highlight@npm:7.18.6" +"@babel/highlight@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/highlight@npm:7.22.5" dependencies: - "@babel/helper-validator-identifier": ^7.18.6 + "@babel/helper-validator-identifier": ^7.22.5 chalk: ^2.0.0 js-tokens: ^4.0.0 - checksum: 92d8ee61549de5ff5120e945e774728e5ccd57fd3b2ed6eace020ec744823d4a98e242be1453d21764a30a14769ecd62170fba28539b211799bbaf232bbb2789 + checksum: f61ae6de6ee0ea8d9b5bcf2a532faec5ab0a1dc0f7c640e5047fc61630a0edb88b18d8c92eb06566d30da7a27db841aca11820ecd3ebe9ce514c9350fbed39c4 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.13, @babel/parser@npm:^7.20.7": - version: 7.20.13 - resolution: "@babel/parser@npm:7.20.13" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/parser@npm:7.22.5" bin: parser: ./bin/babel-parser.js - checksum: 7eb2e3d9d9ad5e24b087c88d137f5701d94f049e28b9dce9f3f5c6d4d9b06a0d7c43b9106f1c02df8a204226200e0517de4bc81a339768a4ebd4c59107ea93a4 - languageName: node - linkType: hard - -"@babel/parser@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/parser@npm:7.21.4" - bin: - parser: ./bin/babel-parser.js - checksum: de610ecd1bff331766d0c058023ca11a4f242bfafefc42caf926becccfb6756637d167c001987ca830dd4b34b93c629a4cef63f8c8c864a8564cdfde1989ac77 + checksum: 470ebba516417ce8683b36e2eddd56dcfecb32c54b9bb507e28eb76b30d1c3e618fd0cfeee1f64d8357c2254514e1a19e32885cfb4e73149f4ae875436a6d59c languageName: node linkType: hard @@ -412,25 +351,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/plugin-syntax-jsx@npm:7.21.4" - dependencies: - "@babel/helper-plugin-utils": ^7.20.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: bb7309402a1d4e155f32aa0cf216e1fa8324d6c4cfd248b03280028a015a10e46b6efd6565f515f8913918a3602b39255999c06046f7d4b8a5106be2165d724a - languageName: node - linkType: hard - -"@babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.18.6 - resolution: "@babel/plugin-syntax-jsx@npm:7.18.6" +"@babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" dependencies: - "@babel/helper-plugin-utils": ^7.18.6 + "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 6d37ea972970195f1ffe1a54745ce2ae456e0ac6145fae9aa1480f297248b262ea6ebb93010eddb86ebfacb94f57c05a1fc5d232b9a67325b09060299d515c67 + checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce languageName: node linkType: hard @@ -511,178 +439,156 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.20.0": - version: 7.21.4 - resolution: "@babel/plugin-syntax-typescript@npm:7.21.4" - dependencies: - "@babel/helper-plugin-utils": ^7.20.2 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a59ce2477b7ae8c8945dc37dda292fef9ce46a6507b3d76b03ce7f3a6c9451a6567438b20a78ebcb3955d04095fd1ccd767075a863f79fcc30aa34dcfa441fe0 - languageName: node - linkType: hard - -"@babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.20.0 - resolution: "@babel/plugin-syntax-typescript@npm:7.20.0" +"@babel/plugin-syntax-typescript@npm:^7.22.5, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" dependencies: - "@babel/helper-plugin-utils": ^7.19.0 + "@babel/helper-plugin-utils": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 6189c0b5c32ba3c9a80a42338bd50719d783b20ef29b853d4f03929e971913d3cefd80184e924ae98ad6db09080be8fe6f1ffde9a6db8972523234f0274d36f7 + checksum: 8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.21.2": - version: 7.21.2 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.21.2" +"@babel/plugin-transform-modules-commonjs@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5" dependencies: - "@babel/helper-module-transforms": ^7.21.2 - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/helper-simple-access": ^7.20.2 + "@babel/helper-module-transforms": ^7.22.5 + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 65aa06e3e3792f39b99eb5f807034693ff0ecf80438580f7ae504f4c4448ef04147b1889ea5e6f60f3ad4a12ebbb57c6f1f979a249dadbd8d11fe22f4441918b + checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.21.3": - version: 7.21.3 - resolution: "@babel/plugin-transform-typescript@npm:7.21.3" +"@babel/plugin-transform-typescript@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/plugin-transform-typescript@npm:7.22.5" dependencies: - "@babel/helper-annotate-as-pure": ^7.18.6 - "@babel/helper-create-class-features-plugin": ^7.21.0 - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/plugin-syntax-typescript": ^7.20.0 + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.22.5 + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/plugin-syntax-typescript": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c16fd577bf43f633deb76fca2a8527d8ae25968c8efdf327c1955472c3e0257e62992473d1ad7f9ee95379ce2404699af405ea03346055adadd3478ad0ecd117 + checksum: d12f1ca1ef1f2a54432eb044d2999705d1205ebe211c2a7f05b12e8eb2d2a461fd7657b5486b2f2f1efe7c0c0dc8e80725b767073d40fe4ae059a7af057b05e4 languageName: node linkType: hard "@babel/preset-typescript@npm:^7.20.2": - version: 7.21.4 - resolution: "@babel/preset-typescript@npm:7.21.4" - dependencies: - "@babel/helper-plugin-utils": ^7.20.2 - "@babel/helper-validator-option": ^7.21.0 - "@babel/plugin-syntax-jsx": ^7.21.4 - "@babel/plugin-transform-modules-commonjs": ^7.21.2 - "@babel/plugin-transform-typescript": ^7.21.3 + version: 7.22.5 + resolution: "@babel/preset-typescript@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + "@babel/helper-validator-option": ^7.22.5 + "@babel/plugin-syntax-jsx": ^7.22.5 + "@babel/plugin-transform-modules-commonjs": ^7.22.5 + "@babel/plugin-transform-typescript": ^7.22.5 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 83b2f2bf7be3a970acd212177525f58bbb1f2e042b675a47d021a675ae27cf00b6b6babfaf3ae5c980592c9ed1b0712e5197796b691905d25c99f9006478ea06 + checksum: 7be1670cb4404797d3a473bd72d66eb2b3e0f2f8a672a5e40bdb0812cc66085ec84bcd7b896709764cabf042fdc6b7f2d4755ac7cce10515eb596ff61dab5154 languageName: node linkType: hard -"@babel/template@npm:^7.18.10, @babel/template@npm:^7.20.7, @babel/template@npm:^7.3.3": - version: 7.20.7 - resolution: "@babel/template@npm:7.20.7" +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": + version: 7.22.5 + resolution: "@babel/template@npm:7.22.5" dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/parser": ^7.20.7 - "@babel/types": ^7.20.7 - checksum: 2eb1a0ab8d415078776bceb3473d07ab746e6bb4c2f6ca46ee70efb284d75c4a32bb0cd6f4f4946dec9711f9c0780e8e5d64b743208deac6f8e9858afadc349e + "@babel/code-frame": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 languageName: node linkType: hard -"@babel/traverse@npm:^7.20.10, @babel/traverse@npm:^7.20.12, @babel/traverse@npm:^7.20.13, @babel/traverse@npm:^7.7.2": - version: 7.20.13 - resolution: "@babel/traverse@npm:7.20.13" +"@babel/traverse@npm:^7.22.5, @babel/traverse@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/traverse@npm:7.22.5" dependencies: - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.20.7 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.19.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.20.13 - "@babel/types": ^7.20.7 + "@babel/code-frame": ^7.22.5 + "@babel/generator": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-function-name": ^7.22.5 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 debug: ^4.1.0 globals: ^11.1.0 - checksum: 30ca6e0bd18233fda48fa09315efd14dfc61dcf5b8fa3712b343bfc61b32bc63b5e85ea1773cc9576c9b293b96f46b4589aaeb0a52e1f3eeac4edc076d049fc7 + checksum: 560931422dc1761f2df723778dcb4e51ce0d02e560cf2caa49822921578f49189a5a7d053b78a32dca33e59be886a6b2200a6e24d4ae9b5086ca0ba803815694 languageName: node linkType: hard -"@babel/traverse@npm:^7.20.7, @babel/traverse@npm:^7.21.2": - version: 7.21.4 - resolution: "@babel/traverse@npm:7.21.4" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": + version: 7.22.5 + resolution: "@babel/types@npm:7.22.5" dependencies: - "@babel/code-frame": ^7.21.4 - "@babel/generator": ^7.21.4 - "@babel/helper-environment-visitor": ^7.18.9 - "@babel/helper-function-name": ^7.21.0 - "@babel/helper-hoist-variables": ^7.18.6 - "@babel/helper-split-export-declaration": ^7.18.6 - "@babel/parser": ^7.21.4 - "@babel/types": ^7.21.4 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: f22f067c2d9b6497abf3d4e53ea71f3aa82a21f2ed434dd69b8c5767f11f2a4c24c8d2f517d2312c9e5248e5c69395fdca1c95a2b3286122c75f5783ddb6f53c + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + to-fast-properties: ^2.0.0 + checksum: c13a9c1dc7d2d1a241a2f8363540cb9af1d66e978e8984b400a20c4f38ba38ca29f06e26a0f2d49a70bad9e57615dac09c35accfddf1bb90d23cd3e0a0bab892 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.6, @babel/types@npm:^7.19.0, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.7, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": - version: 7.20.7 - resolution: "@babel/types@npm:7.20.7" - dependencies: - "@babel/helper-string-parser": ^7.19.4 - "@babel/helper-validator-identifier": ^7.19.1 - to-fast-properties: ^2.0.0 - checksum: b39af241f0b72bba67fd6d0d23914f6faec8c0eba8015c181cbd5ea92e59fc91a52a1ab490d3520c7dbd19ddb9ebb76c476308f6388764f16d8201e37fae6811 +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27 languageName: node linkType: hard -"@babel/types@npm:^7.20.0, @babel/types@npm:^7.21.0, @babel/types@npm:^7.21.2, @babel/types@npm:^7.21.4": - version: 7.21.4 - resolution: "@babel/types@npm:7.21.4" +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" dependencies: - "@babel/helper-string-parser": ^7.19.4 - "@babel/helper-validator-identifier": ^7.19.1 - to-fast-properties: ^2.0.0 - checksum: 587bc55a91ce003b0f8aa10d70070f8006560d7dc0360dc0406d306a2cb2a10154e2f9080b9c37abec76907a90b330a536406cb75e6bdc905484f37b75c73219 + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 languageName: node linkType: hard -"@bcoe/v8-coverage@npm:^0.2.3": - version: 0.2.3 - resolution: "@bcoe/v8-coverage@npm:0.2.3" - checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27 +"@eslint-community/regexpp@npm:^4.4.0": + version: 4.5.1 + resolution: "@eslint-community/regexpp@npm:4.5.1" + checksum: 6d901166d64998d591fab4db1c2f872981ccd5f6fe066a1ad0a93d4e11855ecae6bfb76660869a469563e8882d4307228cebd41142adb409d182f2966771e57e languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.4.1": - version: 1.4.1 - resolution: "@eslint/eslintrc@npm:1.4.1" +"@eslint/eslintrc@npm:^2.0.3": + version: 2.0.3 + resolution: "@eslint/eslintrc@npm:2.0.3" dependencies: ajv: ^6.12.4 debug: ^4.3.2 - espree: ^9.4.0 + espree: ^9.5.2 globals: ^13.19.0 ignore: ^5.2.0 import-fresh: ^3.2.1 js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: cd3e5a8683db604739938b1c1c8b77927dc04fce3e28e0c88e7f2cd4900b89466baf83dfbad76b2b9e4d2746abdd00dd3f9da544d3e311633d8693f327d04cd7 + checksum: ddc51f25f8524d8231db9c9bf03177e503d941a332e8d5ce3b10b09241be4d5584a378a529a27a527586bfbccf3031ae539eb891352033c340b012b4d0c81d92 languageName: node linkType: hard -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 +"@eslint/js@npm:8.43.0": + version: 8.43.0 + resolution: "@eslint/js@npm:8.43.0" + checksum: 580487a09c82ac169744d36e4af77bc4f582c9a37749d1e9481eb93626c8f3991b2390c6e4e69e5642e3b6e870912b839229a0e23594fae348156ea5a8ed7e2e languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.8": - version: 0.11.8 - resolution: "@humanwhocodes/config-array@npm:0.11.8" +"@humanwhocodes/config-array@npm:^0.11.10": + version: 0.11.10 + resolution: "@humanwhocodes/config-array@npm:0.11.10" dependencies: "@humanwhocodes/object-schema": ^1.2.1 debug: ^4.1.1 minimatch: ^3.0.5 - checksum: 0fd6b3c54f1674ce0a224df09b9c2f9846d20b9e54fabae1281ecfc04f2e6ad69bf19e1d6af6a28f88e8aa3990168b6cb9e1ef755868c3256a630605ec2cb1d3 + checksum: 1b1302e2403d0e35bc43e66d67a2b36b0ad1119efc704b5faff68c41f791a052355b010fb2d27ef022670f550de24cd6d08d5ecf0821c16326b7dcd0ee5d5d8a languageName: node linkType: hard @@ -700,6 +606,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -720,50 +640,50 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/console@npm:29.4.1" +"@jest/console@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/console@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 - jest-message-util: ^29.4.1 - jest-util: ^29.4.1 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 slash: ^3.0.0 - checksum: 5b061e4fec29016d42ab1dbbc0fd8386cfa28f921deb6880ff1a82203c7df0776827c2819f2fe1feb8872c8a5cf6d0a04aaf008e80c239813357ccf8790332e9 + checksum: 9f4f4b8fabd1221361b7f2e92d4a90f5f8c2e2b29077249996ab3c8b7f765175ffee795368f8d6b5b2bb3adb32dc09319f7270c7c787b0d259e624e00e0f64a5 languageName: node linkType: hard -"@jest/core@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/core@npm:29.4.1" +"@jest/core@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/core@npm:29.5.0" dependencies: - "@jest/console": ^29.4.1 - "@jest/reporters": ^29.4.1 - "@jest/test-result": ^29.4.1 - "@jest/transform": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/console": ^29.5.0 + "@jest/reporters": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 ci-info: ^3.2.0 exit: ^0.1.2 graceful-fs: ^4.2.9 - jest-changed-files: ^29.4.0 - jest-config: ^29.4.1 - jest-haste-map: ^29.4.1 - jest-message-util: ^29.4.1 - jest-regex-util: ^29.2.0 - jest-resolve: ^29.4.1 - jest-resolve-dependencies: ^29.4.1 - jest-runner: ^29.4.1 - jest-runtime: ^29.4.1 - jest-snapshot: ^29.4.1 - jest-util: ^29.4.1 - jest-validate: ^29.4.1 - jest-watcher: ^29.4.1 + jest-changed-files: ^29.5.0 + jest-config: ^29.5.0 + jest-haste-map: ^29.5.0 + jest-message-util: ^29.5.0 + jest-regex-util: ^29.4.3 + jest-resolve: ^29.5.0 + jest-resolve-dependencies: ^29.5.0 + jest-runner: ^29.5.0 + jest-runtime: ^29.5.0 + jest-snapshot: ^29.5.0 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 + jest-watcher: ^29.5.0 micromatch: ^4.0.4 - pretty-format: ^29.4.1 + pretty-format: ^29.5.0 slash: ^3.0.0 strip-ansi: ^6.0.0 peerDependencies: @@ -771,76 +691,76 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: 70bf65187bdc14825512bbb5afda6f578cca62cda70d8fc2bf08377d916785cfa5da3f3b6aabda42e535c1353fc9a1073b8370f49b2d49ad8fca798119219c3e + checksum: 9e8f5243fe82d5a57f3971e1b96f320058df7c315328a3a827263f3b17f64be10c80f4a9c1b1773628b64d2de6d607c70b5b2d5bf13e7f5ad04223e9ef6aac06 languageName: node linkType: hard -"@jest/environment@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/environment@npm:29.4.1" +"@jest/environment@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/environment@npm:29.5.0" dependencies: - "@jest/fake-timers": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/fake-timers": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" - jest-mock: ^29.4.1 - checksum: f6fed37d2e4aede2930f0a030432b72efeed6d3ea2eee165c1e64afd9fb3af8cf827e306c800cdb3f7bbd106bc5b2405cdec98b91a85695e3f62b1e228cb8d09 + jest-mock: ^29.5.0 + checksum: 921de6325cd4817dec6685e5ff299b499b6379f3f9cf489b4b13588ee1f3820a0c77b49e6a087996b6de8f629f6f5251e636cba08d1bdb97d8071cc7d033c88a languageName: node linkType: hard -"@jest/expect-utils@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/expect-utils@npm:29.4.1" +"@jest/expect-utils@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/expect-utils@npm:29.5.0" dependencies: - jest-get-type: ^29.2.0 - checksum: 865b4ee79d43e2457efb8ce3f58108f2fe141ce620350fe21d0baaf7e2f00b9b67f6e9c1c89760b1008c100e844fb03a6dda264418ed378243956904d9a88c69 + jest-get-type: ^29.4.3 + checksum: c46fb677c88535cf83cf29f0a5b1f376c6a1109ddda266ad7da1a9cbc53cb441fa402dd61fc7b111ffc99603c11a9b3357ee41a1c0e035a58830bcb360871476 languageName: node linkType: hard -"@jest/expect@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/expect@npm:29.4.1" +"@jest/expect@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/expect@npm:29.5.0" dependencies: - expect: ^29.4.1 - jest-snapshot: ^29.4.1 - checksum: 5e9979822a83847f2671e6ed8482e1afc6553ea6579527fdcc6f31ac4f54975e74f1410b9ca133e80ad30dfc38510a9e731ffe70e9eecea61abad487095d969a + expect: ^29.5.0 + jest-snapshot: ^29.5.0 + checksum: bd10e295111547e6339137107d83986ab48d46561525393834d7d2d8b2ae9d5626653f3f5e48e5c3fa742ac982e97bdf1f541b53b9e1d117a247b08e938527f6 languageName: node linkType: hard -"@jest/fake-timers@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/fake-timers@npm:29.4.1" +"@jest/fake-timers@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/fake-timers@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@sinonjs/fake-timers": ^10.0.2 "@types/node": "*" - jest-message-util: ^29.4.1 - jest-mock: ^29.4.1 - jest-util: ^29.4.1 - checksum: 6e1f404054cae54291c1aba7e6b16d7895e2f14b2a1814a0133f9859d6bf49b8e91ce5b3ee15517013bcc6061b63e7a9aeebabd32a68f27a1a15a6dfb15644d1 + jest-message-util: ^29.5.0 + jest-mock: ^29.5.0 + jest-util: ^29.5.0 + checksum: 69930c6922341f244151ec0d27640852ec96237f730fc024da1f53143d31b43cde75d92f9d8e5937981cdce3b31416abc3a7090a0d22c2377512c4a6613244ee languageName: node linkType: hard -"@jest/globals@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/globals@npm:29.4.1" +"@jest/globals@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/globals@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.1 - "@jest/expect": ^29.4.1 - "@jest/types": ^29.4.1 - jest-mock: ^29.4.1 - checksum: 492af8f7c1a97c88464951dfe30fdfcc1566138658df87ab4cdd3b0e20245022637ee4636270af35346391fc4dcd18130d21b643c7e317355087b7cece392476 + "@jest/environment": ^29.5.0 + "@jest/expect": ^29.5.0 + "@jest/types": ^29.5.0 + jest-mock: ^29.5.0 + checksum: b309ab8f21b571a7c672608682e84bbdd3d2b554ddf81e4e32617fec0a69094a290ab42e3c8b2c66ba891882bfb1b8b2736720ea1285b3ad646d55c2abefedd9 languageName: node linkType: hard -"@jest/reporters@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/reporters@npm:29.4.1" +"@jest/reporters@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/reporters@npm:29.5.0" dependencies: "@bcoe/v8-coverage": ^0.2.3 - "@jest/console": ^29.4.1 - "@jest/test-result": ^29.4.1 - "@jest/transform": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/console": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@jridgewell/trace-mapping": ^0.3.15 "@types/node": "*" chalk: ^4.0.0 @@ -853,9 +773,9 @@ __metadata: istanbul-lib-report: ^3.0.0 istanbul-lib-source-maps: ^4.0.0 istanbul-reports: ^3.1.3 - jest-message-util: ^29.4.1 - jest-util: ^29.4.1 - jest-worker: ^29.4.1 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 + jest-worker: ^29.5.0 slash: ^3.0.0 string-length: ^4.0.1 strip-ansi: ^6.0.0 @@ -865,109 +785,99 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: fb70886e90eeb45e1df7c4196e1768285d5f1db4c01edd6eeed33619971d8c33031a9a3705004f14dff9c3460f5d605a9dac9779c5a91c73e4f7a4b303ff25ff + checksum: 481268aac9a4a75cc49c4df1273d6b111808dec815e9d009dad717c32383ebb0cebac76e820ad1ab44e207540e1c2fe1e640d44c4f262de92ab1933e057fdeeb languageName: node linkType: hard -"@jest/schemas@npm:^29.4.0": - version: 29.4.0 - resolution: "@jest/schemas@npm:29.4.0" +"@jest/schemas@npm:^29.4.3": + version: 29.4.3 + resolution: "@jest/schemas@npm:29.4.3" dependencies: "@sinclair/typebox": ^0.25.16 - checksum: 005c90b7b641af029133fa390c0c8a75b63edf651da6253d7c472a8f15ddd18aa139edcd4236e57f974006e39c67217925768115484dbd7bfed2eba224de8b7d + checksum: ac754e245c19dc39e10ebd41dce09040214c96a4cd8efa143b82148e383e45128f24599195ab4f01433adae4ccfbe2db6574c90db2862ccd8551a86704b5bebd languageName: node linkType: hard -"@jest/source-map@npm:^29.2.0": - version: 29.2.0 - resolution: "@jest/source-map@npm:29.2.0" +"@jest/source-map@npm:^29.4.3": + version: 29.4.3 + resolution: "@jest/source-map@npm:29.4.3" dependencies: "@jridgewell/trace-mapping": ^0.3.15 callsites: ^3.0.0 graceful-fs: ^4.2.9 - checksum: 09f76ab63d15dcf44b3035a79412164f43be34ec189575930f1a00c87e36ea0211ebd6a4fbe2253c2516e19b49b131f348ddbb86223ca7b6bbac9a6bc76ec96e + checksum: 2301d225145f8123540c0be073f35a80fd26a2f5e59550fd68525d8cea580fb896d12bf65106591ffb7366a8a19790076dbebc70e0f5e6ceb51f81827ed1f89c languageName: node linkType: hard -"@jest/test-result@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/test-result@npm:29.4.1" +"@jest/test-result@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/test-result@npm:29.5.0" dependencies: - "@jest/console": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/console": ^29.5.0 + "@jest/types": ^29.5.0 "@types/istanbul-lib-coverage": ^2.0.0 collect-v8-coverage: ^1.0.0 - checksum: 8909e5033bf52b85840da8bbc7ded98d52a86f63f2708d6c976f204e007739ada8fc2f985394a8950e40b1e17508bd8e26db4fa328a5fb37c411fe534bb192ec + checksum: 2e8ff5242227ab960c520c3ea0f6544c595cc1c42fa3873c158e9f4f685f4ec9670ec08a4af94ae3885c0005a43550a9595191ffbc27a0965df27d9d98bbf901 languageName: node linkType: hard -"@jest/test-sequencer@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/test-sequencer@npm:29.4.1" +"@jest/test-sequencer@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/test-sequencer@npm:29.5.0" dependencies: - "@jest/test-result": ^29.4.1 + "@jest/test-result": ^29.5.0 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.1 + jest-haste-map: ^29.5.0 slash: ^3.0.0 - checksum: ddf26b780579b239076d5eaf445ff17b8cf1d363c2cfdd3842f281c597d2ef1ee42e93f3cd2ac52803a88de0107a6059d72007ecc51bcd535406c17941ef33be + checksum: eca34b4aeb2fda6dfb7f9f4b064c858a7adf64ec5c6091b6f4ed9d3c19549177cbadcf1c615c4c182688fa1cf085c8c55c3ca6eea40719a34554b0bf071d842e languageName: node linkType: hard -"@jest/transform@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/transform@npm:29.4.1" +"@jest/transform@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/transform@npm:29.5.0" dependencies: "@babel/core": ^7.11.6 - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@jridgewell/trace-mapping": ^0.3.15 babel-plugin-istanbul: ^6.1.1 chalk: ^4.0.0 convert-source-map: ^2.0.0 fast-json-stable-stringify: ^2.1.0 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.1 - jest-regex-util: ^29.2.0 - jest-util: ^29.4.1 + jest-haste-map: ^29.5.0 + jest-regex-util: ^29.4.3 + jest-util: ^29.5.0 micromatch: ^4.0.4 pirates: ^4.0.4 slash: ^3.0.0 - write-file-atomic: ^5.0.0 - checksum: ae8aa3ec32d869fbaa45f9513455ae96447de829effc3855d720ff12218f7d5b1b4e782cccf1ad38a9e85d6a762c53148259065075200844c997fe6a6252604e + write-file-atomic: ^4.0.2 + checksum: d55d604085c157cf5112e165ff5ac1fa788873b3b31265fb4734ca59892ee24e44119964cc47eb6d178dd9512bbb6c576d1e20e51a201ff4e24d31e818a1c92d languageName: node linkType: hard -"@jest/types@npm:^29.4.1": - version: 29.4.1 - resolution: "@jest/types@npm:29.4.1" +"@jest/types@npm:^29.5.0": + version: 29.5.0 + resolution: "@jest/types@npm:29.5.0" dependencies: - "@jest/schemas": ^29.4.0 + "@jest/schemas": ^29.4.3 "@types/istanbul-lib-coverage": ^2.0.0 "@types/istanbul-reports": ^3.0.0 "@types/node": "*" "@types/yargs": ^17.0.8 chalk: ^4.0.0 - checksum: 0aa0b6a210b3474289e5dcaa8e7abb2238dba8d0baf2eb5a3f080fb95e9a39e71e8abc96811d4ef7011f5d993755bb54515e9d827d7ebc2a2d4d9579d84f5a04 - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.1.0": - version: 0.1.1 - resolution: "@jridgewell/gen-mapping@npm:0.1.1" - dependencies: - "@jridgewell/set-array": ^1.0.0 - "@jridgewell/sourcemap-codec": ^1.4.10 - checksum: 3bcc21fe786de6ffbf35c399a174faab05eb23ce6a03e8769569de28abbf4facc2db36a9ddb0150545ae23a8d35a7cf7237b2aa9e9356a7c626fb4698287d5cc + checksum: 1811f94b19cf8a9460a289c4f056796cfc373480e0492692a6125a553cd1a63824bd846d7bb78820b7b6f758f6dd3c2d4558293bb676d541b2fa59c70fdf9d39 languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.2": - version: 0.3.2 - resolution: "@jridgewell/gen-mapping@npm:0.3.2" +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" dependencies: "@jridgewell/set-array": ^1.0.1 "@jridgewell/sourcemap-codec": ^1.4.10 "@jridgewell/trace-mapping": ^0.3.9 - checksum: 1832707a1c476afebe4d0fbbd4b9434fdb51a4c3e009ab1e9938648e21b7a97049fa6009393bdf05cab7504108413441df26d8a3c12193996e65493a4efb6882 + checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab languageName: node linkType: hard @@ -978,31 +888,28 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.0.0, @jridgewell/set-array@npm:^1.0.1": +"@jridgewell/set-array@npm:^1.0.1": version: 1.1.2 resolution: "@jridgewell/set-array@npm:1.1.2" checksum: 69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.10": +"@jridgewell/sourcemap-codec@npm:1.4.14": version: 1.4.14 resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" checksum: 61100637b6d173d3ba786a5dff019e1a74b1f394f323c1fee337ff390239f053b87266c7a948777f4b1ee68c01a8ad0ab61e5ff4abb5a012a0b091bec391ab97 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.17 - resolution: "@jridgewell/trace-mapping@npm:0.3.17" - dependencies: - "@jridgewell/resolve-uri": 3.1.0 - "@jridgewell/sourcemap-codec": 1.4.14 - checksum: 9d703b859cff5cd83b7308fd457a431387db5db96bd781a63bf48e183418dd9d3d44e76b9e4ae13237f6abeeb25d739ec9215c1d5bfdd08f66f750a50074a339 +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.17": +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.15, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.18 resolution: "@jridgewell/trace-mapping@npm:0.3.18" dependencies: @@ -1058,48 +965,44 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.2 - resolution: "@npmcli/fs@npm:2.1.2" +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" dependencies: - "@gar/promisify": ^1.1.3 semver: ^7.3.5 - checksum: 405074965e72d4c9d728931b64d2d38e6ea12066d4fad651ac253d175e413c06fe4350970c783db0d749181da8fe49c42d3880bd1cbc12cd68e3a7964d820225 + checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e languageName: node linkType: hard -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.1 - resolution: "@npmcli/move-file@npm:2.0.1" - dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: 52dc02259d98da517fae4cb3a0a3850227bdae4939dda1980b788a7670636ca2b4a01b58df03dd5f65c1e3cb70c50fa8ce5762b582b3f499ec30ee5ce1fd9380 +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f languageName: node linkType: hard "@sinclair/typebox@npm:^0.25.16": - version: 0.25.21 - resolution: "@sinclair/typebox@npm:0.25.21" - checksum: 763af1163fe4eabee9b914d4e4548a39fbba3287d2b3b1ff043c1da3c5a321e99d50a3ca94eb182988131e00b006a6f019799cde8da2f61e2f118b30b0276a00 + version: 0.25.24 + resolution: "@sinclair/typebox@npm:0.25.24" + checksum: 10219c58f40b8414c50b483b0550445e9710d4fe7b2c4dccb9b66533dd90ba8e024acc776026cebe81e87f06fa24b07fdd7bc30dd277eb9cc386ec50151a3026 languageName: node linkType: hard -"@sinonjs/commons@npm:^2.0.0": - version: 2.0.0 - resolution: "@sinonjs/commons@npm:2.0.0" +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.0 + resolution: "@sinonjs/commons@npm:3.0.0" dependencies: type-detect: 4.0.8 - checksum: 5023ba17edf2b85ed58262313b8e9b59e23c6860681a9af0200f239fe939e2b79736d04a260e8270ddd57196851dde3ba754d7230be5c5234e777ae2ca8af137 + checksum: b4b5b73d4df4560fb8c0c7b38c7ad4aeabedd362f3373859d804c988c725889cde33550e4bcc7cd316a30f5152a2d1d43db71b6d0c38f5feef71fd8d016763f8 languageName: node linkType: hard "@sinonjs/fake-timers@npm:^10.0.2": - version: 10.0.2 - resolution: "@sinonjs/fake-timers@npm:10.0.2" + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" dependencies: - "@sinonjs/commons": ^2.0.0 - checksum: c62aa98e7cefda8dedc101ce227abc888dc46b8ff9706c5f0a8dfd9c3ada97d0a5611384738d9ba0b26b59f99c2ba24efece8e779bb08329e9e87358fa309824 + "@sinonjs/commons": ^3.0.0 + checksum: 614d30cb4d5201550c940945d44c9e0b6d64a888ff2cd5b357f95ad6721070d6b8839cd10e15b76bf5e14af0bcc1d8f9ec00d49a46318f1f669a4bec1d7f3148 languageName: node linkType: hard @@ -1111,15 +1014,15 @@ __metadata: linkType: hard "@types/babel__core@npm:^7.1.14": - version: 7.20.0 - resolution: "@types/babel__core@npm:7.20.0" + version: 7.20.1 + resolution: "@types/babel__core@npm:7.20.1" dependencies: "@babel/parser": ^7.20.7 "@babel/types": ^7.20.7 "@types/babel__generator": "*" "@types/babel__template": "*" "@types/babel__traverse": "*" - checksum: 49b601a0a7637f1f387442c8156bd086cfd10ff4b82b0e1994e73a6396643b5435366fb33d6b604eade8467cca594ef97adcbc412aede90bb112ebe88d0ad6df + checksum: 9fcd9691a33074802d9057ff70b0e3ff3778f52470475b68698a0f6714fbe2ccb36c16b43dc924eb978cd8a81c1f845e5ff4699e7a47606043b539eb8c6331a8 languageName: node linkType: hard @@ -1143,11 +1046,11 @@ __metadata: linkType: hard "@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": - version: 7.18.3 - resolution: "@types/babel__traverse@npm:7.18.3" + version: 7.20.1 + resolution: "@types/babel__traverse@npm:7.20.1" dependencies: - "@babel/types": ^7.3.0 - checksum: d20953338b2f012ab7750932ece0a78e7d1645b0a6ff42d49be90f55e9998085da1374a9786a7da252df89555c6586695ba4d1d4b4e88ab2b9f306bcd35e00d3 + "@babel/types": ^7.20.7 + checksum: 58341e23c649c0eba134a1682d4f20d027fad290d92e5740faa1279978f6ed476fc467ae51ce17a877e2566d805aeac64eae541168994367761ec883a4150221 languageName: node linkType: hard @@ -1161,11 +1064,11 @@ __metadata: linkType: hard "@types/better-sqlite3@npm:^7.6.3": - version: 7.6.3 - resolution: "@types/better-sqlite3@npm:7.6.3" + version: 7.6.4 + resolution: "@types/better-sqlite3@npm:7.6.4" dependencies: "@types/node": "*" - checksum: 37ffd2507beb55f284261fc72b2f0b5585aecd65ffaffbc1f48a4d59958c3bcc16e54b83d9fd6af5f6a0edab830e384aef7ed79dbbfc3d443f850cb1eab091f5 + checksum: 75ab00d31b56437cc65fe15ff673cf8d1609edca52628083921bcbab1cbd828d135a2859fb4e68af8ef5a4801705ba99d54b96499f997bce65dd306ade3dbe58 languageName: node linkType: hard @@ -1214,13 +1117,14 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:^4.17.33": - version: 4.17.33 - resolution: "@types/express-serve-static-core@npm:4.17.33" + version: 4.17.35 + resolution: "@types/express-serve-static-core@npm:4.17.35" dependencies: "@types/node": "*" "@types/qs": "*" "@types/range-parser": "*" - checksum: dce580d16b85f207445af9d4053d66942b27d0c72e86153089fa00feee3e96ae336b7bedb31ed4eea9e553c99d6dd356ed6e0928f135375d9f862a1a8015adf2 + "@types/send": "*" + checksum: cc8995d10c6feda475ec1b3a0e69eb0f35f21ab6b49129ad5c6f279e0bc5de8175bc04ec51304cb79a43eec3ed2f5a1e01472eb6d5f827b8c35c6ca8ad24eb6e languageName: node linkType: hard @@ -1271,37 +1175,30 @@ __metadata: linkType: hard "@types/jest@npm:^29.2.3": - version: 29.4.0 - resolution: "@types/jest@npm:29.4.0" + version: 29.5.2 + resolution: "@types/jest@npm:29.5.2" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: 23760282362a252e6690314584d83a47512d4cd61663e957ed3398ecf98195fe931c45606ee2f9def12f8ed7d8aa102d492ec42d26facdaf8b78094a31e6568e + checksum: 7d205599ea3cccc262bad5cc173d3242d6bf8138c99458509230e4ecef07a52d6ddcde5a1dbd49ace655c0af51d2dbadef3748697292ea4d86da19d9e03e19c0 languageName: node linkType: hard "@types/json-schema@npm:^7.0.9": - version: 7.0.11 - resolution: "@types/json-schema@npm:7.0.11" - checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d + version: 7.0.12 + resolution: "@types/json-schema@npm:7.0.12" + checksum: 00239e97234eeb5ceefb0c1875d98ade6e922bfec39dd365ec6bd360b5c2f825e612ac4f6e5f1d13601b8b30f378f15e6faa805a3a732f4a1bbe61915163d293 languageName: node linkType: hard -"@types/mime@npm:*": - version: 3.0.1 - resolution: "@types/mime@npm:3.0.1" - checksum: 4040fac73fd0cea2460e29b348c1a6173da747f3a87da0dbce80dd7a9355a3d0e51d6d9a401654f3e5550620e3718b5a899b2ec1debf18424e298a2c605346e7 - languageName: node - linkType: hard - -"@types/node@npm:*": - version: 17.0.31 - resolution: "@types/node@npm:17.0.31" - checksum: 704618350f8420d5c47db0f7778398e821b7724369946f5c441a7e6b9343295553936400eb8309f0b07d5e39c240988ab3456b983712ca86265dabc9aee4ad3d +"@types/mime@npm:*, @types/mime@npm:^1": + version: 1.3.2 + resolution: "@types/mime@npm:1.3.2" + checksum: 0493368244cced1a69cb791b485a260a422e6fcc857782e1178d1e6f219f1b161793e9f87f5fae1b219af0f50bee24fcbe733a18b4be8fdd07a38a8fb91146fd languageName: node linkType: hard -"@types/node@npm:^17.0.45": +"@types/node@npm:*, @types/node@npm:^17.0.45": version: 17.0.45 resolution: "@types/node@npm:17.0.45" checksum: aa04366b9103b7d6cfd6b2ef64182e0eaa7d4462c3f817618486ea0422984c51fc69fd0d436eae6c9e696ddfdbec9ccaa27a917f7c2e8c75c5d57827fe3d95e8 @@ -1309,9 +1206,9 @@ __metadata: linkType: hard "@types/prettier@npm:^2.1.5": - version: 2.7.2 - resolution: "@types/prettier@npm:2.7.2" - checksum: b47d76a5252265f8d25dd2fe2a5a61dc43ba0e6a96ffdd00c594cb4fd74c1982c2e346497e3472805d97915407a09423804cc2110a0b8e1b22cffcab246479b7 + version: 2.7.3 + resolution: "@types/prettier@npm:2.7.3" + checksum: 705384209cea6d1433ff6c187c80dcc0b95d99d5c5ce21a46a9a58060c527973506822e428789d842761e0280d25e3359300f017fbe77b9755bc772ab3dc2f83 languageName: node linkType: hard @@ -1330,19 +1227,29 @@ __metadata: linkType: hard "@types/semver@npm:^7.3.12": - version: 7.3.13 - resolution: "@types/semver@npm:7.3.13" - checksum: 00c0724d54757c2f4bc60b5032fe91cda6410e48689633d5f35ece8a0a66445e3e57fa1d6e07eb780f792e82ac542948ec4d0b76eb3484297b79bd18b8cf1cb0 + version: 7.5.0 + resolution: "@types/semver@npm:7.5.0" + checksum: 0a64b9b9c7424d9a467658b18dd70d1d781c2d6f033096a6e05762d20ebbad23c1b69b0083b0484722aabf35640b78ccc3de26368bcae1129c87e9df028a22e2 + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.1 + resolution: "@types/send@npm:0.17.1" + dependencies: + "@types/mime": ^1 + "@types/node": "*" + checksum: 10b620a5960058ef009afbc17686f680d6486277c62f640845381ec4baa0ea683fdd77c3afea4803daf5fcddd3fb2972c8aa32e078939f1d4e96f83195c89793 languageName: node linkType: hard "@types/serve-static@npm:*": - version: 1.15.0 - resolution: "@types/serve-static@npm:1.15.0" + version: 1.15.1 + resolution: "@types/serve-static@npm:1.15.1" dependencies: "@types/mime": "*" "@types/node": "*" - checksum: b6ac93d471fb0f53ddcac1f9b67572a09cd62806f7db5855244b28f6f421139626f24799392566e97d1ffc61b12f9de7f30380c39fcae3c8a161fe161d44edf2 + checksum: 2e078bdc1e458c7dfe69e9faa83cc69194b8896cce57cb745016580543c7ab5af07fdaa8ac1765eb79524208c81017546f66056f44d1204f812d72810613de36 languageName: node linkType: hard @@ -1354,12 +1261,12 @@ __metadata: linkType: hard "@types/superagent@npm:*": - version: 4.1.16 - resolution: "@types/superagent@npm:4.1.16" + version: 4.1.18 + resolution: "@types/superagent@npm:4.1.18" dependencies: "@types/cookiejar": "*" "@types/node": "*" - checksum: 187d1d32fdafd20b27e81728c46283160d3296ad904d56e0780769cf524105c94cc64bf5bafa170400cf5f1063d30826427de42ff0894d15b54df6d0fa31be4e + checksum: 4e50cb41e6f0ac55917dddae4665e5251ce0ec086f89172c8b53432c0c3ee026b9243ba4c994aa2702720d7c288fd7ae77f241f9fb9fb15d2d7c4b6bc2ee7079 languageName: node linkType: hard @@ -1373,9 +1280,9 @@ __metadata: linkType: hard "@types/uuid@npm:^9.0.0": - version: 9.0.0 - resolution: "@types/uuid@npm:9.0.0" - checksum: 59ae56d9547c8758588659da2a2b4c97cce79c2aae1798c892bb29452ef08e87859dea2ec3a66bfa88d0d2153147520be2b1893be920f9f0bc9c53a3207ea6aa + version: 9.0.2 + resolution: "@types/uuid@npm:9.0.2" + checksum: 1754bcf3444e1e3aeadd6e774fc328eb53bc956665e2e8fb6ec127aa8e1f43d9a224c3d22a9a6233dca8dd81a12dc7fed4d84b8876dd5ec82d40f574f7ff8b68 languageName: node linkType: hard @@ -1387,26 +1294,26 @@ __metadata: linkType: hard "@types/yargs@npm:^17.0.8": - version: 17.0.20 - resolution: "@types/yargs@npm:17.0.20" + version: 17.0.24 + resolution: "@types/yargs@npm:17.0.24" dependencies: "@types/yargs-parser": "*" - checksum: dc2edbb0e4b6bfe5189b86c057bb6991139af02372b1d3591083e4ce8f9605b19d598e56413e30f41453733f7a048f732f899cb637f3938f90ed3eb13f23cc90 + checksum: 5f3ac4dc4f6e211c1627340160fbe2fd247ceba002190da6cf9155af1798450501d628c9165a183f30a224fc68fa5e700490d740ff4c73e2cdef95bc4e8ba7bf languageName: node linkType: hard "@typescript-eslint/eslint-plugin@npm:^5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.51.0" + version: 5.60.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.60.0" dependencies: - "@typescript-eslint/scope-manager": 5.51.0 - "@typescript-eslint/type-utils": 5.51.0 - "@typescript-eslint/utils": 5.51.0 + "@eslint-community/regexpp": ^4.4.0 + "@typescript-eslint/scope-manager": 5.60.0 + "@typescript-eslint/type-utils": 5.60.0 + "@typescript-eslint/utils": 5.60.0 debug: ^4.3.4 grapheme-splitter: ^1.0.4 ignore: ^5.2.0 natural-compare-lite: ^1.4.0 - regexpp: ^3.2.0 semver: ^7.3.7 tsutils: ^3.21.0 peerDependencies: @@ -1415,43 +1322,43 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 5351d8cec13bd9867ce4aaf7052aa31c9ca867fc89c620fc0fe5718ac2cbc165903275db59974324d98e45df0d33a73a4367d236668772912731031a672cfdcd + checksum: 61dd70a1ea9787e69d0d4cd14f6a4c94ba786b535a3f519ade7926d965ee1d4f8fefa8bf0224ee57c5c6517eec3674c0fd06f9226536aa428c2bdddeed1e70f4 languageName: node linkType: hard "@typescript-eslint/parser@npm:^5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/parser@npm:5.51.0" + version: 5.60.0 + resolution: "@typescript-eslint/parser@npm:5.60.0" dependencies: - "@typescript-eslint/scope-manager": 5.51.0 - "@typescript-eslint/types": 5.51.0 - "@typescript-eslint/typescript-estree": 5.51.0 + "@typescript-eslint/scope-manager": 5.60.0 + "@typescript-eslint/types": 5.60.0 + "@typescript-eslint/typescript-estree": 5.60.0 debug: ^4.3.4 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 096ec819132839febd4f390c4bbf31687e06191092c244dbd189a64cd7383fbaba728f2765e8809cd9834c0069163ab38b0e5f0f6360157d831647d4c295f8cd + checksum: 94e7931a5b356b16638b281b8e1d661f8b1660f0c75a323537f68b311dae91b7a575a0a019d4ea05a79cc5d42b5cb41cc367205691cdfd292ef96a3b66b1e58b languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/scope-manager@npm:5.51.0" +"@typescript-eslint/scope-manager@npm:5.60.0": + version: 5.60.0 + resolution: "@typescript-eslint/scope-manager@npm:5.60.0" dependencies: - "@typescript-eslint/types": 5.51.0 - "@typescript-eslint/visitor-keys": 5.51.0 - checksum: b3c9f48b6b7a7ae2ebcad4745ef91e4727776b2cf56d31be6456b1aa063aa649539e20f9fffa83cad9ccaaa9c492f2354a1c15526a2b789e235ec58b3a82d22c + "@typescript-eslint/types": 5.60.0 + "@typescript-eslint/visitor-keys": 5.60.0 + checksum: b21ee1ef57be948a806aa31fd65a9186766b3e1a727030dc47025edcadc54bd1aa6133a439acd5f44a93e2b983dd55bc5571bb01cb834461dab733682d66256a languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/type-utils@npm:5.51.0" +"@typescript-eslint/type-utils@npm:5.60.0": + version: 5.60.0 + resolution: "@typescript-eslint/type-utils@npm:5.60.0" dependencies: - "@typescript-eslint/typescript-estree": 5.51.0 - "@typescript-eslint/utils": 5.51.0 + "@typescript-eslint/typescript-estree": 5.60.0 + "@typescript-eslint/utils": 5.60.0 debug: ^4.3.4 tsutils: ^3.21.0 peerDependencies: @@ -1459,23 +1366,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: ab9747b0c629cfaaab903eed8ce1e39d34d69a402ce5faf2f1fff2bbb461bdbe034044b1368ba67ba8e5c1c512172e07d83c8a563635d8de811bf148d95c7dec + checksum: b90ce97592f2db899d88d7a325fec4d2ea11a7b8b4306787310890c27fb51862a6c003675252e9dc465908f791ad5320ea7307260ecd10e89ca1d209fbf8616d languageName: node linkType: hard -"@typescript-eslint/types@npm:5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/types@npm:5.51.0" - checksum: b31021a0866f41ba5d71b6c4c7e20cc9b99d49c93bb7db63b55b2e51542fb75b4e27662ee86350da3c1318029e278a5a807facaf4cb5aeea724be8b0e021e836 +"@typescript-eslint/types@npm:5.60.0": + version: 5.60.0 + resolution: "@typescript-eslint/types@npm:5.60.0" + checksum: 48f29e5c084c5663cfed1a6c4458799a6690a213e7861a24501f9b96698ae59e89a1df1c77e481777e4da78f1b0a5573a549f7b8880e3f4071a7a8b686254db8 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.51.0" +"@typescript-eslint/typescript-estree@npm:5.60.0": + version: 5.60.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.60.0" dependencies: - "@typescript-eslint/types": 5.51.0 - "@typescript-eslint/visitor-keys": 5.51.0 + "@typescript-eslint/types": 5.60.0 + "@typescript-eslint/visitor-keys": 5.60.0 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -1484,35 +1391,35 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: aec23e5cab48ee72fefa6d1ac266639ebabf6cebec1e0207ad47011d3a48186ac9a632c8e34c3bac896155f54895a497230c11d789fd81263b08eb267d7113ce + checksum: 0f4f342730ead42ba60b5fca4bf1950abebd83030010c38b5df98ff9fd95d0ce1cfc3974a44c90c65f381f4f172adcf1a540e018d7968cc845d937bf6c734dae languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/utils@npm:5.51.0" +"@typescript-eslint/utils@npm:5.60.0": + version: 5.60.0 + resolution: "@typescript-eslint/utils@npm:5.60.0" dependencies: + "@eslint-community/eslint-utils": ^4.2.0 "@types/json-schema": ^7.0.9 "@types/semver": ^7.3.12 - "@typescript-eslint/scope-manager": 5.51.0 - "@typescript-eslint/types": 5.51.0 - "@typescript-eslint/typescript-estree": 5.51.0 + "@typescript-eslint/scope-manager": 5.60.0 + "@typescript-eslint/types": 5.60.0 + "@typescript-eslint/typescript-estree": 5.60.0 eslint-scope: ^5.1.1 - eslint-utils: ^3.0.0 semver: ^7.3.7 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: c6e28c942fbac5500f0e8ed67ef304b484ba296486e55306f78fb090dc9d5bb1f25a0bedc065e14680041eadce5e95fa10aab618cb0c316599ec987e6ea72442 + checksum: cbe56567f0b53e24ad7ef7d2fb4cdc8596e2559c21ee639aa0560879b6216208550e51e9d8ae4b388ff21286809c6dc985cec66738294871051396a8ae5bccbc languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.51.0": - version: 5.51.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.51.0" +"@typescript-eslint/visitor-keys@npm:5.60.0": + version: 5.60.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.60.0" dependencies: - "@typescript-eslint/types": 5.51.0 + "@typescript-eslint/types": 5.60.0 eslint-visitor-keys: ^3.3.0 - checksum: b49710f3c6b3b62a846a163afffd81be5eb2b1f44e25bec51ff3c9f4c3b579d74aa4cbd3753b4fc09ea3dbc64a7062f9c658c08d22bb2740a599cb703d876220 + checksum: d39b2485d030f9755820d0f6f3748a8ec44e1ca23cb36ddcba67a9eb1f258c8ec83c61fc015c50e8f4a00d05df62d719dbda445625e3e71a64a659f1d248157e languageName: node linkType: hard @@ -1543,11 +1450,11 @@ __metadata: linkType: hard "acorn@npm:^8.8.0": - version: 8.8.2 - resolution: "acorn@npm:8.8.2" + version: 8.9.0 + resolution: "acorn@npm:8.9.0" bin: acorn: bin/acorn - checksum: f790b99a1bf63ef160c967e23c46feea7787e531292bb827126334612c234ed489a0dc2c7ba33156416f0ffa8d25bf2b0fdb7f35c2ba60eb3e960572bece4001 + checksum: 25dfb94952386ecfb847e61934de04a4e7c2dc21c2e700fc4e2ef27ce78cb717700c4c4f279cd630bb4774948633c3859fc16063ec8573bda4568e0a312e6744 languageName: node linkType: hard @@ -1583,6 +1490,7 @@ __metadata: jest: ^29.3.1 jws: ^4.0.0 nordigen-node: ^1.2.3 + openid-client: ^5.4.2 prettier: ^2.8.3 supertest: ^6.3.1 typescript: ^4.9.5 @@ -1600,13 +1508,13 @@ __metadata: linkType: hard "agentkeepalive@npm:^4.2.1": - version: 4.2.1 - resolution: "agentkeepalive@npm:4.2.1" + version: 4.3.0 + resolution: "agentkeepalive@npm:4.3.0" dependencies: debug: ^4.1.0 - depd: ^1.1.2 + depd: ^2.0.0 humanize-ms: ^1.2.1 - checksum: 39cb49ed8cf217fd6da058a92828a0a84e0b74c35550f82ee0a10e1ee403c4b78ade7948be2279b188b7a7303f5d396ea2738b134731e464bf28de00a4f72a18 + checksum: 982453aa44c11a06826c836025e5162c846e1200adb56f2d075400da7d32d87021b3b0a58768d949d824811f5654223d5a8a3dad120921a2439625eb847c6260 languageName: node linkType: hard @@ -1648,6 +1556,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -1673,6 +1588,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + "anymatch@npm:^3.0.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -1755,30 +1677,30 @@ __metadata: linkType: hard "axios@npm:^1.2.1": - version: 1.3.2 - resolution: "axios@npm:1.3.2" + version: 1.4.0 + resolution: "axios@npm:1.4.0" dependencies: follow-redirects: ^1.15.0 form-data: ^4.0.0 proxy-from-env: ^1.1.0 - checksum: 9791af75a6df137b15ef45d13ad11eb357b3860d2496347ee18778db9d0abc2320362a4452f1e070e3160f1dbcc518fcefdc9e005be097e7db39acb22cf608e5 + checksum: 7fb6a4313bae7f45e89d62c70a800913c303df653f19eafec88e56cea2e3821066b8409bc68be1930ecca80e861c52aa787659df0ffec6ad4d451c7816b9386b languageName: node linkType: hard -"babel-jest@npm:^29.4.1": - version: 29.4.1 - resolution: "babel-jest@npm:29.4.1" +"babel-jest@npm:^29.5.0": + version: 29.5.0 + resolution: "babel-jest@npm:29.5.0" dependencies: - "@jest/transform": ^29.4.1 + "@jest/transform": ^29.5.0 "@types/babel__core": ^7.1.14 babel-plugin-istanbul: ^6.1.1 - babel-preset-jest: ^29.4.0 + babel-preset-jest: ^29.5.0 chalk: ^4.0.0 graceful-fs: ^4.2.9 slash: ^3.0.0 peerDependencies: "@babel/core": ^7.8.0 - checksum: 4a2971ee50d0e467ccc9ca3557c2e721aaac1a165c34cd82fd056be8fc0bce258247b3c960059ecf05beddafe06b37dceeb8b8c32fa7393b8a42d2055a70559f + checksum: eafb6d37deb71f0c80bf3c80215aa46732153e5e8bcd73f6ff47d92e5c0c98c8f7f75995d0efec6289c371edad3693cd8fa2367b0661c4deb71a3a7117267ede languageName: node linkType: hard @@ -1795,15 +1717,15 @@ __metadata: languageName: node linkType: hard -"babel-plugin-jest-hoist@npm:^29.4.0": - version: 29.4.0 - resolution: "babel-plugin-jest-hoist@npm:29.4.0" +"babel-plugin-jest-hoist@npm:^29.5.0": + version: 29.5.0 + resolution: "babel-plugin-jest-hoist@npm:29.5.0" dependencies: "@babel/template": ^7.3.3 "@babel/types": ^7.3.3 "@types/babel__core": ^7.1.14 "@types/babel__traverse": ^7.0.6 - checksum: c18369a9aa5e29f8d1c00b19f513f6c291df8d531c344ef7951e7e3d3b95ae5dd029817510544ceb668a96e156f05ee73eadb228428956b9239f1714d99fecb6 + checksum: 099b5254073b6bc985b6d2d045ad26fb8ed30ff8ae6404c4fe8ee7cd0e98a820f69e3dfb871c7c65aae0f4b65af77046244c07bb92d49ef9005c90eedf681539 languageName: node linkType: hard @@ -1829,22 +1751,22 @@ __metadata: languageName: node linkType: hard -"babel-preset-jest@npm:^29.4.0": - version: 29.4.0 - resolution: "babel-preset-jest@npm:29.4.0" +"babel-preset-jest@npm:^29.5.0": + version: 29.5.0 + resolution: "babel-preset-jest@npm:29.5.0" dependencies: - babel-plugin-jest-hoist: ^29.4.0 + babel-plugin-jest-hoist: ^29.5.0 babel-preset-current-node-syntax: ^1.0.0 peerDependencies: "@babel/core": ^7.0.0 - checksum: 38baf965731059ec13cf4038d2a6ec3ac528ba45ce45f4e41710f17fa0cdcba404ff74689cdc9a929c64b2547d6ea9f8d5c41ca4db7770a85f82b7de3fb25024 + checksum: 5566ca2762766c9319b4973d018d2fa08c0fcf6415c72cc54f4c8e7199e851ea8f5e6c6730f03ed7ed44fc8beefa959dd15911f2647dee47c615ff4faeddb1ad languageName: node linkType: hard "balanced-match@npm:^1.0.0": - version: 1.0.0 - resolution: "balanced-match@npm:1.0.0" - checksum: 9b67bfe558772f40cf743a3469b48b286aecec2ea9fe80c48d74845e53aab1cef524fafedf123a63019b49ac397760573ef5f173f539423061f7217cbb5fbd40 + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 languageName: node linkType: hard @@ -1866,13 +1788,13 @@ __metadata: linkType: hard "better-sqlite3@npm:^8.2.0": - version: 8.2.0 - resolution: "better-sqlite3@npm:8.2.0" + version: 8.4.0 + resolution: "better-sqlite3@npm:8.4.0" dependencies: bindings: ^1.5.0 node-gyp: latest prebuild-install: ^7.1.0 - checksum: ab8a00bcc33c4a7467f78fcbb103c784705cf170ecc9c8eb1149a89a2153c03a7f65681064667eb214fa7f555797abd8183380a0396ce04eaf36efef921ce103 + checksum: f8b180c26428a2d381482e83b4519d0d81a918e00f92cafd255f42eb9583f49b2fed1015121ad49ebe1f3f790cc8c6f4697d1e805193d65e70dacf2e8abbd6af languageName: node linkType: hard @@ -1896,7 +1818,7 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.1, body-parser@npm:^1.20.1": +"body-parser@npm:1.20.1": version: 1.20.1 resolution: "body-parser@npm:1.20.1" dependencies: @@ -1916,6 +1838,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^1.20.1": + version: 1.20.2 + resolution: "body-parser@npm:1.20.2" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: ~1.6.18 + unpipe: 1.0.0 + checksum: 14d37ec638ab5c93f6099ecaed7f28f890d222c650c69306872e00b9efa081ff6c596cd9afb9930656aae4d6c4e1c17537bea12bb73c87a217cb3cfea8896737 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -1945,16 +1887,16 @@ __metadata: linkType: hard "browserslist@npm:^4.21.3": - version: 4.21.4 - resolution: "browserslist@npm:4.21.4" + version: 4.21.9 + resolution: "browserslist@npm:4.21.9" dependencies: - caniuse-lite: ^1.0.30001400 - electron-to-chromium: ^1.4.251 - node-releases: ^2.0.6 - update-browserslist-db: ^1.0.9 + caniuse-lite: ^1.0.30001503 + electron-to-chromium: ^1.4.431 + node-releases: ^2.0.12 + update-browserslist-db: ^1.0.11 bin: browserslist: cli.js - checksum: 4af3793704dbb4615bcd29059ab472344dc7961c8680aa6c4bb84f05340e14038d06a5aead58724eae69455b8fade8b8c69f1638016e87e5578969d74c078b79 + checksum: 80d3820584e211484ad1b1a5cfdeca1dd00442f47be87e117e1dda34b628c87e18b81ae7986fa5977b3e6a03154f6d13cd763baa6b8bf5dd9dd19f4926603698 languageName: node linkType: hard @@ -1998,29 +1940,23 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.3 - resolution: "cacache@npm:16.1.3" +"cacache@npm:^17.0.0": + version: 17.1.3 + resolution: "cacache@npm:17.1.3" dependencies: - "@npmcli/fs": ^2.1.0 - "@npmcli/move-file": ^2.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - glob: ^8.0.1 - infer-owner: ^1.0.4 + "@npmcli/fs": ^3.1.0 + fs-minipass: ^3.0.0 + glob: ^10.2.2 lru-cache: ^7.7.1 - minipass: ^3.1.6 + minipass: ^5.0.0 minipass-collect: ^1.0.2 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 - mkdirp: ^1.0.4 p-map: ^4.0.0 - promise-inflight: ^1.0.1 - rimraf: ^3.0.2 - ssri: ^9.0.0 + ssri: ^10.0.0 tar: ^6.1.11 - unique-filename: ^2.0.0 - checksum: d91409e6e57d7d9a3a25e5dcc589c84e75b178ae8ea7de05cbf6b783f77a5fae938f6e8fda6f5257ed70000be27a681e1e44829251bfffe4c10216002f8f14e6 + unique-filename: ^3.0.0 + checksum: 385756781e1e21af089160d89d7462b7ed9883c978e848c7075b90b73cb823680e66092d61513050164588387d2ca87dd6d910e28d64bc13a9ac82cd8580c796 languageName: node linkType: hard @@ -2035,9 +1971,9 @@ __metadata: linkType: hard "callsites@npm:^3.0.0": - version: 3.0.0 - resolution: "callsites@npm:3.0.0" - checksum: 40e3cb2027cceb6c6da0c9cfba2266606beec488f8028382b4cbb8f4320665c13f8012fd400efb3c43ae22e1547e89909fb9b2e8521e3b4e532ae300a5d248f9 + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 languageName: node linkType: hard @@ -2055,10 +1991,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001400": - version: 1.0.30001449 - resolution: "caniuse-lite@npm:1.0.30001449" - checksum: f1b395f0a5495c1931c53f58441e0db79b8b0f8ef72bb6d241d13c49b05827630efe6793d540610e0a014d8fdda330dd42f981c82951bd4bdcf635480e1a0102 +"caniuse-lite@npm:^1.0.30001503": + version: 1.0.30001506 + resolution: "caniuse-lite@npm:1.0.30001506" + checksum: 0a090745824622df146e2f6dde79c7f7920a899dec1b3a599d2ef9acf41cef5e179fd133bb59f2030286a4ea935f4230e05438d7394694c414e8ada345eb5268 languageName: node linkType: hard @@ -2105,16 +2041,16 @@ __metadata: linkType: hard "ci-info@npm:^3.2.0": - version: 3.7.1 - resolution: "ci-info@npm:3.7.1" - checksum: 72d93d5101ea1c186511277fbd8d06ae8a6e028cc2fb94361e92bf735b39c5ebd192e8d15a66ff8c4e3ed569f87c2f844e96f90e141b2de5c649f77ec34ff601 + version: 3.8.0 + resolution: "ci-info@npm:3.8.0" + checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 languageName: node linkType: hard "cjs-module-lexer@npm:^1.0.0": - version: 1.2.2 - resolution: "cjs-module-lexer@npm:1.2.2" - checksum: 977f3f042bd4f08e368c890d91eecfbc4f91da0bc009a3c557bc4dfbf32022ad1141244ac1178d44de70fc9f3dea7add7cd9a658a34b9fae98a55d8f92331ce5 + version: 1.2.3 + resolution: "cjs-module-lexer@npm:1.2.3" + checksum: 5ea3cb867a9bb609b6d476cd86590d105f3cfd6514db38ff71f63992ab40939c2feb68967faa15a6d2b1f90daa6416b79ea2de486e9e2485a6f8b66a21b4fb0a languageName: node linkType: hard @@ -2230,10 +2166,10 @@ __metadata: languageName: node linkType: hard -"content-type@npm:~1.0.4": - version: 1.0.4 - resolution: "content-type@npm:1.0.4" - checksum: 3d93585fda985d1554eca5ebd251994327608d2e200978fdbfba21c0c679914d5faf266d17027de44b34a72c7b0745b18584ecccaa7e1fdfb6a68ac7114f12e0 +"content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 languageName: node linkType: hard @@ -2282,7 +2218,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -2294,9 +2230,9 @@ __metadata: linkType: hard "dayjs@npm:^1.11.3": - version: 1.11.7 - resolution: "dayjs@npm:1.11.7" - checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb + version: 1.11.8 + resolution: "dayjs@npm:1.11.8" + checksum: 4fe04b6df98ba6e5f89b49d80bba603cbf01e21a1b4a24ecb163c94c0ba5324a32ac234a139cee654f89d5277a2bcebca5347e6676c28a0a6d1a90f1d34a42b8 languageName: node linkType: hard @@ -2309,19 +2245,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4": - version: 4.3.2 - resolution: "debug@npm:4.3.2" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 820ea160e267e23c953c9ed87e7ad93494d8cda2f7349af5e7e3bb236d23707ee3022f477d5a7d2ee86ef2bf7d60aa9ab22d1f58080d7deb9dccd073585e1e43 - languageName: node - linkType: hard - -"debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -2364,9 +2288,9 @@ __metadata: linkType: hard "deepmerge@npm:^4.2.2": - version: 4.2.2 - resolution: "deepmerge@npm:4.2.2" - checksum: a8c43a1ed8d6d1ed2b5bf569fa4c8eb9f0924034baf75d5d406e47e157a451075c4db353efea7b6bcc56ec48116a8ce72fccf867b6e078e7c561904b5897530b + version: 4.3.1 + resolution: "deepmerge@npm:4.3.1" + checksum: 2024c6a980a1b7128084170c4cf56b0fd58a63f2da1660dcfe977415f27b17dbe5888668b59d0b063753f3220719d5e400b7f113609489c90160bb9a5518d052 languageName: node linkType: hard @@ -2384,20 +2308,13 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0": +"depd@npm:2.0.0, depd@npm:^2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: abbe19c768c97ee2eed6282d8ce3031126662252c58d711f646921c9623f9052e3e1906443066beec1095832f534e57c523b7333f8e7e0d93051ab6baef5ab3a languageName: node linkType: hard -"depd@npm:^1.1.2": - version: 1.1.2 - resolution: "depd@npm:1.1.2" - checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 - languageName: node - linkType: hard - "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -2429,10 +2346,10 @@ __metadata: languageName: node linkType: hard -"diff-sequences@npm:^29.3.1": - version: 29.3.1 - resolution: "diff-sequences@npm:29.3.1" - checksum: 8edab8c383355022e470779a099852d595dd856f9f5bd7af24f177e74138a668932268b4c4fd54096eed643861575c3652d4ecbbb1a9d710488286aed3ffa443 +"diff-sequences@npm:^29.4.3": + version: 29.4.3 + resolution: "diff-sequences@npm:29.4.3" + checksum: 28b265e04fdddcf7f9f814effe102cc95a9dec0564a579b5aed140edb24fc345c611ca52d76d725a3cab55d3888b915b5e8a4702e0f6058968a90fa5f41fcde7 languageName: node linkType: hard @@ -2461,6 +2378,13 @@ __metadata: languageName: node linkType: hard +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + "ecdsa-sig-formatter@npm:1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" @@ -2477,10 +2401,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.251": - version: 1.4.284 - resolution: "electron-to-chromium@npm:1.4.284" - checksum: be496e9dca6509dbdbb54dc32146fc99f8eb716d28a7ee8ccd3eba0066561df36fc51418d8bd7cf5a5891810bf56c0def3418e74248f51ea4a843d423603d10a +"electron-to-chromium@npm:^1.4.431": + version: 1.4.435 + resolution: "electron-to-chromium@npm:1.4.435" + checksum: cd70746db9432e3134c1e239927d160d38aedb2d65fec995bd1c03b6de51d4a8a33d7199afe5fc85c64e3eae28f4238e290729a2899420b3240e801b18c407a0 languageName: node linkType: hard @@ -2498,6 +2422,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -2606,47 +2537,32 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.1.1": - version: 7.1.1 - resolution: "eslint-scope@npm:7.1.1" +"eslint-scope@npm:^7.2.0": + version: 7.2.0 + resolution: "eslint-scope@npm:7.2.0" dependencies: esrecurse: ^4.3.0 estraverse: ^5.2.0 - checksum: 9f6e974ab2db641ca8ab13508c405b7b859e72afe9f254e8131ff154d2f40c99ad4545ce326fd9fde3212ff29707102562a4834f1c48617b35d98c71a97fbf3e + checksum: 64591a2d8b244ade9c690b59ef238a11d5c721a98bcee9e9f445454f442d03d3e04eda88e95a4daec558220a99fa384309d9faae3d459bd40e7a81b4063980ae languageName: node linkType: hard -"eslint-utils@npm:^3.0.0": - version: 3.0.0 - resolution: "eslint-utils@npm:3.0.0" - dependencies: - eslint-visitor-keys: ^2.0.0 - peerDependencies: - eslint: ">=5" - checksum: 0668fe02f5adab2e5a367eee5089f4c39033af20499df88fe4e6aba2015c20720404d8c3d6349b6f716b08fdf91b9da4e5d5481f265049278099c4c836ccb619 - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^2.0.0": - version: 2.1.0 - resolution: "eslint-visitor-keys@npm:2.1.0" - checksum: e3081d7dd2611a35f0388bbdc2f5da60b3a3c5b8b6e928daffff7391146b434d691577aa95064c8b7faad0b8a680266bcda0a42439c18c717b80e6718d7e267d - languageName: node - linkType: hard - -"eslint-visitor-keys@npm:^3.3.0": - version: 3.3.0 - resolution: "eslint-visitor-keys@npm:3.3.0" - checksum: d59e68a7c5a6d0146526b0eec16ce87fbf97fe46b8281e0d41384224375c4e52f5ffb9e16d48f4ea50785cde93f766b0c898e31ab89978d88b0e1720fbfb7808 +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1": + version: 3.4.1 + resolution: "eslint-visitor-keys@npm:3.4.1" + checksum: f05121d868202736b97de7d750847a328fcfa8593b031c95ea89425333db59676ac087fa905eba438d0a3c5769632f828187e0c1a0d271832a2153c1d3661c2c languageName: node linkType: hard "eslint@npm:^8.33.0": - version: 8.33.0 - resolution: "eslint@npm:8.33.0" - dependencies: - "@eslint/eslintrc": ^1.4.1 - "@humanwhocodes/config-array": ^0.11.8 + version: 8.43.0 + resolution: "eslint@npm:8.43.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.4.0 + "@eslint/eslintrc": ^2.0.3 + "@eslint/js": 8.43.0 + "@humanwhocodes/config-array": ^0.11.10 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 ajv: ^6.10.0 @@ -2655,24 +2571,22 @@ __metadata: debug: ^4.3.2 doctrine: ^3.0.0 escape-string-regexp: ^4.0.0 - eslint-scope: ^7.1.1 - eslint-utils: ^3.0.0 - eslint-visitor-keys: ^3.3.0 - espree: ^9.4.0 - esquery: ^1.4.0 + eslint-scope: ^7.2.0 + eslint-visitor-keys: ^3.4.1 + espree: ^9.5.2 + esquery: ^1.4.2 esutils: ^2.0.2 fast-deep-equal: ^3.1.3 file-entry-cache: ^6.0.1 find-up: ^5.0.0 glob-parent: ^6.0.2 globals: ^13.19.0 - grapheme-splitter: ^1.0.4 + graphemer: ^1.4.0 ignore: ^5.2.0 import-fresh: ^3.0.0 imurmurhash: ^0.1.4 is-glob: ^4.0.0 is-path-inside: ^3.0.3 - js-sdsl: ^4.1.4 js-yaml: ^4.1.0 json-stable-stringify-without-jsonify: ^1.0.1 levn: ^0.4.1 @@ -2680,24 +2594,23 @@ __metadata: minimatch: ^3.1.2 natural-compare: ^1.4.0 optionator: ^0.9.1 - regexpp: ^3.2.0 strip-ansi: ^6.0.1 strip-json-comments: ^3.1.0 text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 727e63ab8b7acf281442323c5971f6afdd5b656fbcebc4476cf54e35af51b2f180617433fc5e1952f0449ca3f43a905527f9407ea4b8a7ea7562fc9c3f278d4c + checksum: 55654ce00b0d128822b57526e40473d0497c7c6be3886afdc0b41b6b0dfbd34d0eae8159911b18451b4db51a939a0e6d2e117e847ae419086884fc3d4fe23c7c languageName: node linkType: hard -"espree@npm:^9.4.0": - version: 9.4.1 - resolution: "espree@npm:9.4.1" +"espree@npm:^9.5.2": + version: 9.5.2 + resolution: "espree@npm:9.5.2" dependencies: acorn: ^8.8.0 acorn-jsx: ^5.3.2 - eslint-visitor-keys: ^3.3.0 - checksum: 4d266b0cf81c7dfe69e542c7df0f246e78d29f5b04dda36e514eb4c7af117ee6cfbd3280e560571ed82ff6c9c3f0003c05b82583fc7a94006db7497c4fe4270e + eslint-visitor-keys: ^3.4.1 + checksum: 6506289d6eb26471c0b383ee24fee5c8ae9d61ad540be956b3127be5ce3bf687d2ba6538ee5a86769812c7c552a9d8239e8c4d150f9ea056c6d5cbe8399c03c1 languageName: node linkType: hard @@ -2711,12 +2624,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.0": - version: 1.4.0 - resolution: "esquery@npm:1.4.0" +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" dependencies: estraverse: ^5.1.0 - checksum: a0807e17abd7fbe5fbd4fab673038d6d8a50675cdae6b04fbaa520c34581be0c5fa24582990e8acd8854f671dd291c78bb2efb9e0ed5b62f33bac4f9cf820210 + checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900 languageName: node linkType: hard @@ -2730,9 +2643,9 @@ __metadata: linkType: hard "estraverse@npm:^4.1.1": - version: 4.2.0 - resolution: "estraverse@npm:4.2.0" - checksum: 88c3ec2ef3550a5ddb0dc88d596e9c87c92e6e6a58183d3e5851fff844206081abc92ce57a0f227e685f18742cbc90b2019d12951f7d7dbe066e4440ab3acda6 + version: 4.3.0 + resolution: "estraverse@npm:4.3.0" + checksum: a6299491f9940bb246124a8d44b7b7a413a8336f5436f9837aaa9330209bd9ee8af7e91a654a3545aee9c54b3308e78ee360cef1d777d37cfef77d2fa33b5827 languageName: node linkType: hard @@ -2744,9 +2657,9 @@ __metadata: linkType: hard "esutils@npm:^2.0.2": - version: 2.0.2 - resolution: "esutils@npm:2.0.2" - checksum: d1ad79417f2ad62a7e37c01a6a2767c6d69d991976ed5b0e5ea446dbb758be58a60a892f388db036333b9815a829117a9eb4c881954f9baca2f65c4add3beeb8 + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 languageName: node linkType: hard @@ -2788,16 +2701,23 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.4.1": - version: 29.4.1 - resolution: "expect@npm:29.4.1" +"expect@npm:^29.0.0, expect@npm:^29.5.0": + version: 29.5.0 + resolution: "expect@npm:29.5.0" dependencies: - "@jest/expect-utils": ^29.4.1 - jest-get-type: ^29.2.0 - jest-matcher-utils: ^29.4.1 - jest-message-util: ^29.4.1 - jest-util: ^29.4.1 - checksum: 5918f69371557bbceb01bc163cd0ac03e8cbbc5de761892a9c27ef17a1f9e94dc91edd8298b4eaca18b71ba4a9d521c74b072f0a46950b13d6b61123b0431836 + "@jest/expect-utils": ^29.5.0 + jest-get-type: ^29.4.3 + jest-matcher-utils: ^29.5.0 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 + checksum: 58f70b38693df6e5c6892db1bcd050f0e518d6f785175dc53917d4fa6a7359a048e5690e19ddcb96b65c4493881dd89a3dabdab1a84dfa55c10cdbdabf37b2d7 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 languageName: node linkType: hard @@ -2876,33 +2796,26 @@ __metadata: linkType: hard "fast-diff@npm:^1.1.2": - version: 1.2.0 - resolution: "fast-diff@npm:1.2.0" - checksum: 1b5306eaa9e826564d9e5ffcd6ebd881eb5f770b3f977fcbf38f05c824e42172b53c79920e8429c54eb742ce15a0caf268b0fdd5b38f6de52234c4a8368131ae + version: 1.3.0 + resolution: "fast-diff@npm:1.3.0" + checksum: d22d371b994fdc8cce9ff510d7b8dc4da70ac327bcba20df607dd5b9cae9f908f4d1028f5fe467650f058d1e7270235ae0b8230809a262b4df587a3b3aa216c3 languageName: node linkType: hard "fast-glob@npm:^3.2.9": - version: 3.2.11 - resolution: "fast-glob@npm:3.2.11" + version: 3.2.12 + resolution: "fast-glob@npm:3.2.12" dependencies: "@nodelib/fs.stat": ^2.0.2 "@nodelib/fs.walk": ^1.2.3 glob-parent: ^5.1.2 merge2: ^1.3.0 micromatch: ^4.0.4 - checksum: f473105324a7780a20c06de842e15ddbb41d3cb7e71d1e4fe6e8373204f22245d54f5ab9e2061e6a1c613047345954d29b022e0e76f5c28b1df9858179a0e6d7 + checksum: 0b1990f6ce831c7e28c4d505edcdaad8e27e88ab9fa65eedadb730438cfc7cde4910d6c975d6b7b8dc8a73da4773702ebcfcd6e3518e73938bb1383badfe01c2 languageName: node linkType: hard -"fast-json-stable-stringify@npm:^2.0.0": - version: 2.0.0 - resolution: "fast-json-stable-stringify@npm:2.0.0" - checksum: 5f776089e60a20ccdf5fd17c90590a4bb7c04c4240b2ffde1caad3949f7876a57af7094323dcb432fa6534367768ac6c6b5433a16c5241d0e2cdf0b51b7d4c9f - languageName: node - linkType: hard - -"fast-json-stable-stringify@npm:^2.1.0": +"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb @@ -2924,11 +2837,11 @@ __metadata: linkType: hard "fastq@npm:^1.6.0": - version: 1.13.0 - resolution: "fastq@npm:1.13.0" + version: 1.15.0 + resolution: "fastq@npm:1.15.0" dependencies: reusify: ^1.0.4 - checksum: 32cf15c29afe622af187d12fc9cd93e160a0cb7c31a3bb6ace86b7dea3b28e7b72acde89c882663f307b2184e14782c6c664fa315973c03626c7d4bff070bb0b + checksum: 0170e6bfcd5d57a70412440b8ef600da6de3b2a6c5966aeaf0a852d542daff506a0ee92d6de7679d1de82e644bce69d7a574a6c93f0b03964b5337eed75ada1a languageName: node linkType: hard @@ -3012,9 +2925,9 @@ __metadata: linkType: hard "flatted@npm:^3.1.0": - version: 3.2.5 - resolution: "flatted@npm:3.2.5" - checksum: 3c436e9695ccca29620b4be5671dd72e5dd0a7500e0856611b7ca9bd8169f177f408c3b9abfa78dfe1493ee2d873e2c119080a8a9bee4e1a186a9e60ca6c89f1 + version: 3.2.7 + resolution: "flatted@npm:3.2.7" + checksum: 427633049d55bdb80201c68f7eb1cbd533e03eac541f97d3aecab8c5526f12a20ccecaeede08b57503e772c769e7f8680b37e8d482d1e5f8d7e2194687f9ea35 languageName: node linkType: hard @@ -3028,6 +2941,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + "form-data@npm:^4.0.0": version: 4.0.0 resolution: "form-data@npm:4.0.0" @@ -3072,7 +2995,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": +"fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -3081,6 +3004,15 @@ __metadata: languageName: node linkType: hard +"fs-minipass@npm:^3.0.0": + version: 3.0.2 + resolution: "fs-minipass@npm:3.0.2" + dependencies: + minipass: ^5.0.0 + checksum: e9cc0e1f2d01c6f6f62f567aee59530aba65c6c7b2ae88c5027bc34c711ebcfcfaefd0caf254afa6adfe7d1fba16bc2537508a6235196bac7276747d078aef0a + languageName: node + linkType: hard + "fs.realpath@npm:^1.0.0": version: 1.0.0 resolution: "fs.realpath@npm:1.0.0" @@ -3162,13 +3094,14 @@ __metadata: linkType: hard "get-intrinsic@npm:^1.0.2": - version: 1.1.1 - resolution: "get-intrinsic@npm:1.1.1" + version: 1.2.1 + resolution: "get-intrinsic@npm:1.2.1" dependencies: function-bind: ^1.1.1 has: ^1.0.3 - has-symbols: ^1.0.1 - checksum: a9fe2ca8fa3f07f9b0d30fb202bcd01f3d9b9b6b732452e79c48e79f7d6d8d003af3f9e38514250e3553fdc83c61650851cb6870832ac89deaaceb08e3721a17 + has-proto: ^1.0.1 + has-symbols: ^1.0.3 + checksum: 5b61d88552c24b0cf6fa2d1b3bc5459d7306f699de060d76442cce49a4721f52b8c560a33ab392cf5575b7810277d54ded9d4d39a1ea61855619ebc005aa7e5f languageName: node linkType: hard @@ -3211,21 +3144,22 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.3": - version: 7.1.3 - resolution: "glob@npm:7.1.3" +"glob@npm:^10.2.2": + version: 10.3.0 + resolution: "glob@npm:10.3.0" 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 - checksum: d72a834a393948d6c4a5cacc6a29fe5fe190e1cd134e55dfba09aee0be6fe15be343e96d8ec43558ab67ff8af28e4420c7f63a4d4db1c779e515015e9c318616 + foreground-child: ^3.1.0 + jackspeak: ^2.0.3 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 + path-scurry: ^1.7.0 + bin: + glob: dist/cjs/src/bin.js + checksum: 6fa4ac0a86ffec1c5715a2e6fbdd63e1e7f1c2c8f5db08cc3256cdfcb81093678e7c80a3d100b502a1b9d141369ecf87bc24fe2bcb72acec7b14626d358a4eb0 languageName: node linkType: hard -"glob@npm:^7.1.4": +"glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -3239,19 +3173,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 - languageName: node - linkType: hard - "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -3290,9 +3211,9 @@ __metadata: linkType: hard "graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 languageName: node linkType: hard @@ -3303,6 +3224,13 @@ __metadata: languageName: node linkType: hard +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 + languageName: node + linkType: hard + "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -3317,7 +3245,14 @@ __metadata: languageName: node linkType: hard -"has-symbols@npm:^1.0.1": +"has-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "has-proto@npm:1.0.1" + checksum: febc5b5b531de8022806ad7407935e2135f1cc9e64636c3916c6842bd7995994ca3b29871ecd7954bd35f9e2986c17b3b227880484d22259e2f8e6ce63fd383e + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.3": version: 1.0.3 resolution: "has-symbols@npm:1.0.3" checksum: a054c40c631c0d5741a8285010a0777ea0c068f99ed43e5d6eb12972da223f8af553a455132fdb0801bdcfa0e0f443c0c03a68d8555aa529b3144b446c3f2410 @@ -3354,7 +3289,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.0": +"http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 @@ -3386,12 +3321,12 @@ __metadata: linkType: hard "https-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "https-proxy-agent@npm:5.0.0" + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" dependencies: agent-base: 6 debug: 4 - checksum: 165bfb090bd26d47693597661298006841ab733d0c7383a8cb2f17373387a94c903a3ac687090aa739de05e379ab6f868bae84ab4eac288ad85c328cd1ec9e53 + checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 languageName: node linkType: hard @@ -3437,23 +3372,13 @@ __metadata: linkType: hard "ignore@npm:^5.2.0": - version: 5.2.0 - resolution: "ignore@npm:5.2.0" - checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77 - languageName: node - linkType: hard - -"import-fresh@npm:^3.0.0": - version: 3.0.0 - resolution: "import-fresh@npm:3.0.0" - dependencies: - parent-module: ^1.0.0 - resolve-from: ^4.0.0 - checksum: 7341cbb4a758268bf9b1a9c45703b66516086eb686138d57a66b37bce9b85414139b7bd12c895764375a7c9377acc466db4faaebbb1aabde6f74d1b6471032a8 + version: 5.2.4 + resolution: "ignore@npm:5.2.4" + checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef languageName: node linkType: hard -"import-fresh@npm:^3.2.1": +"import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -3489,13 +3414,6 @@ __metadata: languageName: node linkType: hard -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -3506,14 +3424,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2": - version: 2.0.3 - resolution: "inherits@npm:2.0.3" - checksum: 78cb8d7d850d20a5e9a7f3620db31483aa00ad5f722ce03a55b110e5a723539b3716a3b463e2b96ce3fe286f33afc7c131fa2f91407528ba80cea98a7545d4c0 - languageName: node - linkType: hard - -"inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -3548,12 +3459,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.9.0": - version: 2.11.0 - resolution: "is-core-module@npm:2.11.0" +"is-core-module@npm:^2.11.0": + version: 2.12.1 + resolution: "is-core-module@npm:2.12.1" dependencies: has: ^1.0.3 - checksum: f96fd490c6b48eb4f6d10ba815c6ef13f410b0ba6f7eb8577af51697de523e5f2cd9de1c441b51d27251bf0e4aebc936545e33a5d26d5d51f28d25698d4a8bab + checksum: f04ea30533b5e62764e7b2e049d3157dc0abd95ef44275b32489ea2081176ac9746ffb1cdb107445cf1ff0e0dfcad522726ca27c27ece64dadf3795428b8e468 languageName: node linkType: hard @@ -3674,57 +3585,71 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:^29.4.0": - version: 29.4.0 - resolution: "jest-changed-files@npm:29.4.0" +"jackspeak@npm:^2.0.3": + version: 2.2.1 + resolution: "jackspeak@npm:2.2.1" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: e29291c0d0f280a063fa18fbd1e891ab8c2d7519fd34052c0ebde38538a15c603140d60c2c7f432375ff7ee4c5f1c10daa8b2ae19a97c3d4affe308c8360c1df + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-changed-files@npm:29.5.0" dependencies: execa: ^5.0.0 p-limit: ^3.1.0 - checksum: d8883b32b8b28f4f63cbbe32ff75283401a11647303bd74e2c522981457a88b9146b77974759023c74215a0a55c1b1d0fc3070fe3cde9d4f33aaa1c76aede4eb + checksum: a67a7cb3c11f8f92bd1b7c79e84f724cbd11a9ad51f3cdadafe3ce7ee3c79ee50dbea128f920f5fddc807e9e4e83f5462143094391feedd959a77dd20ab96cf3 languageName: node linkType: hard -"jest-circus@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-circus@npm:29.4.1" +"jest-circus@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-circus@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.1 - "@jest/expect": ^29.4.1 - "@jest/test-result": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/environment": ^29.5.0 + "@jest/expect": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 co: ^4.6.0 dedent: ^0.7.0 is-generator-fn: ^2.0.0 - jest-each: ^29.4.1 - jest-matcher-utils: ^29.4.1 - jest-message-util: ^29.4.1 - jest-runtime: ^29.4.1 - jest-snapshot: ^29.4.1 - jest-util: ^29.4.1 + jest-each: ^29.5.0 + jest-matcher-utils: ^29.5.0 + jest-message-util: ^29.5.0 + jest-runtime: ^29.5.0 + jest-snapshot: ^29.5.0 + jest-util: ^29.5.0 p-limit: ^3.1.0 - pretty-format: ^29.4.1 + pretty-format: ^29.5.0 + pure-rand: ^6.0.0 slash: ^3.0.0 stack-utils: ^2.0.3 - checksum: e1aff95668c2e17397e65b201d472a430d0713e9a75650b0a73ba7aed71f5eb0c2065c0f593dc2f422dcb817db1ec41b6eb888a3a8c01dbaf5eaeec7429a83d5 + checksum: 44ff5d06acedae6de6c866e20e3b61f83e29ab94cf9f960826e7e667de49c12dd9ab9dffd7fa3b7d1f9688a8b5bfb1ebebadbea69d9ed0d3f66af4a0ff8c2b27 languageName: node linkType: hard -"jest-cli@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-cli@npm:29.4.1" +"jest-cli@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-cli@npm:29.5.0" dependencies: - "@jest/core": ^29.4.1 - "@jest/test-result": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/core": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/types": ^29.5.0 chalk: ^4.0.0 exit: ^0.1.2 graceful-fs: ^4.2.9 import-local: ^3.0.2 - jest-config: ^29.4.1 - jest-util: ^29.4.1 - jest-validate: ^29.4.1 + jest-config: ^29.5.0 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 prompts: ^2.0.1 yargs: ^17.3.1 peerDependencies: @@ -3734,34 +3659,34 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 12318e61d51288f4c43ad38f776df8e31264f31458d4b810583945b137ddf9ebbcdd2018cef9987e973f56cf716892649bff650d8b80cae8d868a35c4f0f3f93 + checksum: 39897bbbc0f0d8a6b975ab12fd13887eaa28d92e3dee9e0173a5cb913ae8cc2ae46e090d38c6d723e84d9d6724429cd08685b4e505fa447d31ca615630c7dbba languageName: node linkType: hard -"jest-config@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-config@npm:29.4.1" +"jest-config@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-config@npm:29.5.0" dependencies: "@babel/core": ^7.11.6 - "@jest/test-sequencer": ^29.4.1 - "@jest/types": ^29.4.1 - babel-jest: ^29.4.1 + "@jest/test-sequencer": ^29.5.0 + "@jest/types": ^29.5.0 + babel-jest: ^29.5.0 chalk: ^4.0.0 ci-info: ^3.2.0 deepmerge: ^4.2.2 glob: ^7.1.3 graceful-fs: ^4.2.9 - jest-circus: ^29.4.1 - jest-environment-node: ^29.4.1 - jest-get-type: ^29.2.0 - jest-regex-util: ^29.2.0 - jest-resolve: ^29.4.1 - jest-runner: ^29.4.1 - jest-util: ^29.4.1 - jest-validate: ^29.4.1 + jest-circus: ^29.5.0 + jest-environment-node: ^29.5.0 + jest-get-type: ^29.4.3 + jest-regex-util: ^29.4.3 + jest-resolve: ^29.5.0 + jest-runner: ^29.5.0 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 micromatch: ^4.0.4 parse-json: ^5.2.0 - pretty-format: ^29.4.1 + pretty-format: ^29.5.0 slash: ^3.0.0 strip-json-comments: ^3.1.1 peerDependencies: @@ -3772,135 +3697,135 @@ __metadata: optional: true ts-node: optional: true - checksum: 7ca9c46b25cdf1bd1dd77edeb9ae1a9669e47e6d3af7097bb21b43883415e8311ef97d7b17da5d8eaae695d89e368cfd427a98836391ffec2bdb683b3f4fa060 + checksum: c37c4dab964c54ab293d4e302d40b09687037ac9d00b88348ec42366970747feeaf265e12e3750cd3660b40c518d4031335eda11ac10b70b10e60797ebbd4b9c languageName: node linkType: hard -"jest-diff@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-diff@npm:29.4.1" +"jest-diff@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-diff@npm:29.5.0" dependencies: chalk: ^4.0.0 - diff-sequences: ^29.3.1 - jest-get-type: ^29.2.0 - pretty-format: ^29.4.1 - checksum: 359af2d11a75bbb3c91e3def8cfd0ede00afc6fb5d69d9495f2af5f6e18f692adb940d8338a186159f75afe48088d82bce14e2cc272cad9a5c2148bf0bc7f6bf + diff-sequences: ^29.4.3 + jest-get-type: ^29.4.3 + pretty-format: ^29.5.0 + checksum: dfd0f4a299b5d127779c76b40106c37854c89c3e0785098c717d52822d6620d227f6234c3a9291df204d619e799e3654159213bf93220f79c8e92a55475a3d39 languageName: node linkType: hard -"jest-docblock@npm:^29.2.0": - version: 29.2.0 - resolution: "jest-docblock@npm:29.2.0" +"jest-docblock@npm:^29.4.3": + version: 29.4.3 + resolution: "jest-docblock@npm:29.4.3" dependencies: detect-newline: ^3.0.0 - checksum: b3f1227b7d73fc9e4952180303475cf337b36fa65c7f730ac92f0580f1c08439983262fee21cf3dba11429aa251b4eee1e3bc74796c5777116b400d78f9d2bbe + checksum: e0e9df1485bb8926e5b33478cdf84b3387d9caf3658e7dc1eaa6dc34cb93dea0d2d74797f6e940f0233a88f3dadd60957f2288eb8f95506361f85b84bf8661df languageName: node linkType: hard -"jest-each@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-each@npm:29.4.1" +"jest-each@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-each@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 chalk: ^4.0.0 - jest-get-type: ^29.2.0 - jest-util: ^29.4.1 - pretty-format: ^29.4.1 - checksum: af44c12c747c4b76534b34f7135176c645ff740b59b20a29a3c6c97590ddb4216e7a2e076a43e98a0132350b4af5af3d8e5334bdd7753bf999a5ee240b7360b8 + jest-get-type: ^29.4.3 + jest-util: ^29.5.0 + pretty-format: ^29.5.0 + checksum: b8b297534d25834c5d4e31e4c687359787b1e402519e42664eb704cc3a12a7a91a017565a75acb02e8cf9afd3f4eef3350bd785276bec0900184641b765ff7a5 languageName: node linkType: hard -"jest-environment-node@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-environment-node@npm:29.4.1" +"jest-environment-node@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-environment-node@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.1 - "@jest/fake-timers": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/environment": ^29.5.0 + "@jest/fake-timers": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" - jest-mock: ^29.4.1 - jest-util: ^29.4.1 - checksum: 1de024edbc8a281b2c54d379d649a2d63e153049848c257be4118eaa5136cc4943a32f3ce44841ca2356e18850ab51f833cb94509f268e25ebcd32c6bfac27a3 + jest-mock: ^29.5.0 + jest-util: ^29.5.0 + checksum: 57981911cc20a4219b0da9e22b2e3c9f31b505e43f78e61c899e3227ded455ce1a3a9483842c69cfa4532f02cfb536ae0995bf245f9211608edacfc1e478d411 languageName: node linkType: hard -"jest-get-type@npm:^29.2.0": - version: 29.2.0 - resolution: "jest-get-type@npm:29.2.0" - checksum: e396fd880a30d08940ed8a8e43cd4595db1b8ff09649018eb358ca701811137556bae82626af73459e3c0f8c5e972ed1e57fd3b1537b13a260893dac60a90942 +"jest-get-type@npm:^29.4.3": + version: 29.4.3 + resolution: "jest-get-type@npm:29.4.3" + checksum: 6ac7f2dde1c65e292e4355b6c63b3a4897d7e92cb4c8afcf6d397f2682f8080e094c8b0b68205a74d269882ec06bf696a9de6cd3e1b7333531e5ed7b112605ce languageName: node linkType: hard -"jest-haste-map@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-haste-map@npm:29.4.1" +"jest-haste-map@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-haste-map@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@types/graceful-fs": ^4.1.3 "@types/node": "*" anymatch: ^3.0.3 fb-watchman: ^2.0.0 fsevents: ^2.3.2 graceful-fs: ^4.2.9 - jest-regex-util: ^29.2.0 - jest-util: ^29.4.1 - jest-worker: ^29.4.1 + jest-regex-util: ^29.4.3 + jest-util: ^29.5.0 + jest-worker: ^29.5.0 micromatch: ^4.0.4 walker: ^1.0.8 dependenciesMeta: fsevents: optional: true - checksum: f9815172f0b5d89b723558c5544db4915e03806590b6b686dabb91811b201f3eac07e7211f021a19fc6f9fa6cb90836efac92970ec16385ea18285d91ba8ffc3 + checksum: 3828ff7783f168e34be2c63887f82a01634261f605dcae062d83f979a61c37739e21b9607ecb962256aea3fbe5a530a1acee062d0026fcb47c607c12796cf3b7 languageName: node linkType: hard -"jest-leak-detector@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-leak-detector@npm:29.4.1" +"jest-leak-detector@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-leak-detector@npm:29.5.0" dependencies: - jest-get-type: ^29.2.0 - pretty-format: ^29.4.1 - checksum: 94f8091e52e163a4e50420112988d8386117dfa92bd21738d9a367dc5e1f87d3e645bee2db4fc7fc25a1d495934761bb7a64750d61a7e7b6477b8f1f54da317c + jest-get-type: ^29.4.3 + pretty-format: ^29.5.0 + checksum: 0fb845da7ac9cdfc9b3b2e35f6f623a41c547d7dc0103ceb0349013459d00de5870b5689a625e7e37f9644934b40e8f1dcdd5422d14d57470600350364676313 languageName: node linkType: hard -"jest-matcher-utils@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-matcher-utils@npm:29.4.1" +"jest-matcher-utils@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-matcher-utils@npm:29.5.0" dependencies: chalk: ^4.0.0 - jest-diff: ^29.4.1 - jest-get-type: ^29.2.0 - pretty-format: ^29.4.1 - checksum: ea84dbcae82241cb28e94ff586660aeec51196d9245413dc516ce3aa78140b3ea728b1168b242281b59ad513b0148b9f12d674729bd043a894a3ba9d6ec164f4 + jest-diff: ^29.5.0 + jest-get-type: ^29.4.3 + pretty-format: ^29.5.0 + checksum: 1d3e8c746e484a58ce194e3aad152eff21fd0896e8b8bf3d4ab1a4e2cbfed95fb143646f4ad9fdf6e42212b9e8fc033268b58e011b044a9929df45485deb5ac9 languageName: node linkType: hard -"jest-message-util@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-message-util@npm:29.4.1" +"jest-message-util@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-message-util@npm:29.5.0" dependencies: "@babel/code-frame": ^7.12.13 - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@types/stack-utils": ^2.0.0 chalk: ^4.0.0 graceful-fs: ^4.2.9 micromatch: ^4.0.4 - pretty-format: ^29.4.1 + pretty-format: ^29.5.0 slash: ^3.0.0 stack-utils: ^2.0.3 - checksum: 7d49823401b6d42f0d2d63dd9c0f11d2f64783416f82a68634190abee46e600e25bb0b380c746726acc56e854687bb03a76e26e617fcdda78e8c6316423b694f + checksum: daddece6bbf846eb6a2ab9be9f2446e54085bef4e5cecd13d2a538fa9c01cb89d38e564c6b74fd8e12d37ed9eface8a362240ae9f21d68b214590631e7a0d8bf languageName: node linkType: hard -"jest-mock@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-mock@npm:29.4.1" +"jest-mock@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-mock@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@types/node": "*" - jest-util: ^29.4.1 - checksum: 7f595a71886a64eda21b9fc2660e86a02f0efe6685496c675e6be921d5609fe9ac5fe97e8c7d1cae811974967439e8daa12c1779e731bdd777c47326f173e4a2 + jest-util: ^29.5.0 + checksum: 2a9cf07509948fa8608898c445f04fe4dd6e2049ff431e5531eee028c808d3ba3c67f226ac87b0cf383feaa1055776900d197c895e89783016886ac17a4ff10c languageName: node linkType: hard @@ -3916,103 +3841,102 @@ __metadata: languageName: node linkType: hard -"jest-regex-util@npm:^29.2.0": - version: 29.2.0 - resolution: "jest-regex-util@npm:29.2.0" - checksum: 7c533e51c51230dac20c0d7395b19b8366cb022f7c6e08e6bcf2921626840ff90424af4c9b4689f02f0addfc9b071c4cd5f8f7a989298a4c8e0f9c94418ca1c3 +"jest-regex-util@npm:^29.4.3": + version: 29.4.3 + resolution: "jest-regex-util@npm:29.4.3" + checksum: 96fc7fc28cd4dd73a63c13a526202c4bd8b351d4e5b68b1a2a2c88da3308c2a16e26feaa593083eb0bac38cca1aa9dd05025412e7de013ba963fb8e66af22b8a languageName: node linkType: hard -"jest-resolve-dependencies@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-resolve-dependencies@npm:29.4.1" +"jest-resolve-dependencies@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-resolve-dependencies@npm:29.5.0" dependencies: - jest-regex-util: ^29.2.0 - jest-snapshot: ^29.4.1 - checksum: 561e588abc1aae3d44a46b53eaeee1bc86419407c2e9b97afb7b3fc6ea2df06ef1523e9561bfc8d790c7a48a40031c3b1e1f38281850d23b0a07351553f7e85e + jest-regex-util: ^29.4.3 + jest-snapshot: ^29.5.0 + checksum: 479d2e5365d58fe23f2b87001e2e0adcbffe0147700e85abdec8f14b9703b0a55758c1929a9989e3f5d5e954fb88870ea4bfa04783523b664562fcf5f10b0edf languageName: node linkType: hard -"jest-resolve@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-resolve@npm:29.4.1" +"jest-resolve@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-resolve@npm:29.5.0" dependencies: chalk: ^4.0.0 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.1 + jest-haste-map: ^29.5.0 jest-pnp-resolver: ^1.2.2 - jest-util: ^29.4.1 - jest-validate: ^29.4.1 + jest-util: ^29.5.0 + jest-validate: ^29.5.0 resolve: ^1.20.0 resolve.exports: ^2.0.0 slash: ^3.0.0 - checksum: 1e19c0156937366b3edc867d38ca4c6c8193067605921544a5f5d2019a96c01be5fb9b385bb61a3600eacaceb7a3333f42dbed4cb699403d8575d476a9d4c5d5 + checksum: 9a125f3cf323ceef512089339d35f3ee37f79fe16a831fb6a26773ea6a229b9e490d108fec7af334142e91845b5996de8e7cdd85a4d8d617078737d804e29c8f languageName: node linkType: hard -"jest-runner@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-runner@npm:29.4.1" +"jest-runner@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-runner@npm:29.5.0" dependencies: - "@jest/console": ^29.4.1 - "@jest/environment": ^29.4.1 - "@jest/test-result": ^29.4.1 - "@jest/transform": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/console": ^29.5.0 + "@jest/environment": ^29.5.0 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 emittery: ^0.13.1 graceful-fs: ^4.2.9 - jest-docblock: ^29.2.0 - jest-environment-node: ^29.4.1 - jest-haste-map: ^29.4.1 - jest-leak-detector: ^29.4.1 - jest-message-util: ^29.4.1 - jest-resolve: ^29.4.1 - jest-runtime: ^29.4.1 - jest-util: ^29.4.1 - jest-watcher: ^29.4.1 - jest-worker: ^29.4.1 + jest-docblock: ^29.4.3 + jest-environment-node: ^29.5.0 + jest-haste-map: ^29.5.0 + jest-leak-detector: ^29.5.0 + jest-message-util: ^29.5.0 + jest-resolve: ^29.5.0 + jest-runtime: ^29.5.0 + jest-util: ^29.5.0 + jest-watcher: ^29.5.0 + jest-worker: ^29.5.0 p-limit: ^3.1.0 source-map-support: 0.5.13 - checksum: b6651d8ac16c9f3ce502b58c97e59b062e83b3b7a9bee91812fbbcf141098ef1456902be6598d7980727a0c22457290cb548913dea5bd25ceaca4e1822f733bf + checksum: 437dea69c5dddca22032259787bac74790d5a171c9d804711415f31e5d1abfb64fa52f54a9015bb17a12b858fd0cf3f75ef6f3c9e94255a8596e179f707229c4 languageName: node linkType: hard -"jest-runtime@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-runtime@npm:29.4.1" +"jest-runtime@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-runtime@npm:29.5.0" dependencies: - "@jest/environment": ^29.4.1 - "@jest/fake-timers": ^29.4.1 - "@jest/globals": ^29.4.1 - "@jest/source-map": ^29.2.0 - "@jest/test-result": ^29.4.1 - "@jest/transform": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/environment": ^29.5.0 + "@jest/fake-timers": ^29.5.0 + "@jest/globals": ^29.5.0 + "@jest/source-map": ^29.4.3 + "@jest/test-result": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 cjs-module-lexer: ^1.0.0 collect-v8-coverage: ^1.0.0 glob: ^7.1.3 graceful-fs: ^4.2.9 - jest-haste-map: ^29.4.1 - jest-message-util: ^29.4.1 - jest-mock: ^29.4.1 - jest-regex-util: ^29.2.0 - jest-resolve: ^29.4.1 - jest-snapshot: ^29.4.1 - jest-util: ^29.4.1 - semver: ^7.3.5 + jest-haste-map: ^29.5.0 + jest-message-util: ^29.5.0 + jest-mock: ^29.5.0 + jest-regex-util: ^29.4.3 + jest-resolve: ^29.5.0 + jest-snapshot: ^29.5.0 + jest-util: ^29.5.0 slash: ^3.0.0 strip-bom: ^4.0.0 - checksum: 6c5fcc350ef019bbc0c0601e41c236f4f666c6cee2eef5048fd07a48cc579133d68c852a0d68d9ebbc9b4e115a4f1d0ab5641f3d204944f312fbcb11b73cef8f + checksum: 7af27bd9d54cf1c5735404cf8d76c6509d5610b1ec0106a21baa815c1aff15d774ce534ac2834bc440dccfe6348bae1885fd9a806f23a94ddafdc0f5bae4b09d languageName: node linkType: hard -"jest-snapshot@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-snapshot@npm:29.4.1" +"jest-snapshot@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-snapshot@npm:29.5.0" dependencies: "@babel/core": ^7.11.6 "@babel/generator": ^7.7.2 @@ -4020,92 +3944,91 @@ __metadata: "@babel/plugin-syntax-typescript": ^7.7.2 "@babel/traverse": ^7.7.2 "@babel/types": ^7.3.3 - "@jest/expect-utils": ^29.4.1 - "@jest/transform": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/expect-utils": ^29.5.0 + "@jest/transform": ^29.5.0 + "@jest/types": ^29.5.0 "@types/babel__traverse": ^7.0.6 "@types/prettier": ^2.1.5 babel-preset-current-node-syntax: ^1.0.0 chalk: ^4.0.0 - expect: ^29.4.1 + expect: ^29.5.0 graceful-fs: ^4.2.9 - jest-diff: ^29.4.1 - jest-get-type: ^29.2.0 - jest-haste-map: ^29.4.1 - jest-matcher-utils: ^29.4.1 - jest-message-util: ^29.4.1 - jest-util: ^29.4.1 + jest-diff: ^29.5.0 + jest-get-type: ^29.4.3 + jest-matcher-utils: ^29.5.0 + jest-message-util: ^29.5.0 + jest-util: ^29.5.0 natural-compare: ^1.4.0 - pretty-format: ^29.4.1 + pretty-format: ^29.5.0 semver: ^7.3.5 - checksum: 0d309d4a5edd985be1a9e2d64a78f588f5d98b8add709cdf72c6ce77508329dccdb0de3f0be45223f67535691f3eb6430c13fdfb7dfcca7a81d4a210de2fa736 + checksum: fe5df54122ed10eed625de6416a45bc4958d5062b018f05b152bf9785ab7f355dcd55e40cf5da63895bf8278f8d7b2bb4059b2cfbfdee18f509d455d37d8aa2b languageName: node linkType: hard -"jest-util@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-util@npm:29.4.1" +"jest-util@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-util@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 "@types/node": "*" chalk: ^4.0.0 ci-info: ^3.2.0 graceful-fs: ^4.2.9 picomatch: ^2.2.3 - checksum: 10a0e6c448ace1386f728ee3b7669f67878bb0c2e668a902d11140cc3f75c89a18f4142a37a24ccb587ede20dad86d497b3e8df4f26848a9be50a44779d92bc9 + checksum: fd9212950d34d2ecad8c990dda0d8ea59a8a554b0c188b53ea5d6c4a0829a64f2e1d49e6e85e812014933d17426d7136da4785f9cf76fff1799de51b88bc85d3 languageName: node linkType: hard -"jest-validate@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-validate@npm:29.4.1" +"jest-validate@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-validate@npm:29.5.0" dependencies: - "@jest/types": ^29.4.1 + "@jest/types": ^29.5.0 camelcase: ^6.2.0 chalk: ^4.0.0 - jest-get-type: ^29.2.0 + jest-get-type: ^29.4.3 leven: ^3.1.0 - pretty-format: ^29.4.1 - checksum: f2cd98293ed961e79bc75935fbc8fc18e57bcd207175a4119baf810da38542704545afa8ca402456e34d298e44c7564570400645537c31dab9bf27e18284a650 + pretty-format: ^29.5.0 + checksum: 43ca5df7cb75572a254ac3e92fbbe7be6b6a1be898cc1e887a45d55ea003f7a112717d814a674d37f9f18f52d8de40873c8f084f17664ae562736c78dd44c6a1 languageName: node linkType: hard -"jest-watcher@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-watcher@npm:29.4.1" +"jest-watcher@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-watcher@npm:29.5.0" dependencies: - "@jest/test-result": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/test-result": ^29.5.0 + "@jest/types": ^29.5.0 "@types/node": "*" ansi-escapes: ^4.2.1 chalk: ^4.0.0 emittery: ^0.13.1 - jest-util: ^29.4.1 + jest-util: ^29.5.0 string-length: ^4.0.1 - checksum: 210c4931e065367bf8fcd08a31506245610f25cf4bf566c67136afd963fdf9ff56730570e794e52d7ae2f9e6e64f6d92b9287691af14b01dd7deacac840415fb + checksum: 62303ac7bdc7e61a8b4239a239d018f7527739da2b2be6a81a7be25b74ca769f1c43ee8558ce8e72bb857245c46d6e03af331227ffb00a57280abb2a928aa776 languageName: node linkType: hard -"jest-worker@npm:^29.4.1": - version: 29.4.1 - resolution: "jest-worker@npm:29.4.1" +"jest-worker@npm:^29.5.0": + version: 29.5.0 + resolution: "jest-worker@npm:29.5.0" dependencies: "@types/node": "*" - jest-util: ^29.4.1 + jest-util: ^29.5.0 merge-stream: ^2.0.0 supports-color: ^8.0.0 - checksum: c3b3eaa09d7ac88e11800a63e96a90ba27b7d609335c73842ee5f8e899e9fd6a6aa68009f54dabb6d6e561c98127def369fc86c8f528639ddfb74dd130f4be9f + checksum: 1151a1ae3602b1ea7c42a8f1efe2b5a7bf927039deaa0827bf978880169899b705744e288f80a63603fb3fc2985e0071234986af7dc2c21c7a64333d8777c7c9 languageName: node linkType: hard "jest@npm:^29.3.1": - version: 29.4.1 - resolution: "jest@npm:29.4.1" + version: 29.5.0 + resolution: "jest@npm:29.5.0" dependencies: - "@jest/core": ^29.4.1 - "@jest/types": ^29.4.1 + "@jest/core": ^29.5.0 + "@jest/types": ^29.5.0 import-local: ^3.0.2 - jest-cli: ^29.4.1 + jest-cli: ^29.5.0 peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -4113,14 +4036,14 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: b2f74b24d74e135460579a34727d5027818ab6d55a84cbb1d6e730064f8c8fec0590092c6a84334178b310b923587798b0091ab8ae40baba372530fc46dfd195 + checksum: a8ff2eb0f421623412236e23cbe67c638127fffde466cba9606bc0c0553b4c1e5cb116d7e0ef990b5d1712851652c8ee461373b578df50857fe635b94ff455d5 languageName: node linkType: hard -"js-sdsl@npm:^4.1.4": - version: 4.3.0 - resolution: "js-sdsl@npm:4.3.0" - checksum: ce908257cf6909e213af580af3a691a736f5ee8b16315454768f917a682a4ea0c11bde1b241bbfaecedc0eb67b72101b2c2df2ffaed32aed5d539fca816f054e +"jose@npm:^4.14.1": + version: 4.14.4 + resolution: "jose@npm:4.14.4" + checksum: 2d820a91a8fd97c05d8bc8eedc373b944a0cd7f5fe41063086da233d0473c73fb523912a9f026ea870782bd221f4a515f441a2d3af4de48c6f2c76dac5082377 languageName: node linkType: hard @@ -4289,9 +4212,16 @@ __metadata: linkType: hard "lru-cache@npm:^7.7.1": - version: 7.14.1 - resolution: "lru-cache@npm:7.14.1" - checksum: d72c6713c6a6d86836a7a6523b3f1ac6764768cca47ec99341c3e76db06aacd4764620e5e2cda719a36848785a52a70e531822dc2b33fb071fa709683746c104 + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 + languageName: node + linkType: hard + +"lru-cache@npm:^9.1.1": + version: 9.1.2 + resolution: "lru-cache@npm:9.1.2" + checksum: d3415634be3908909081fc4c56371a8d562d9081eba70543d86871b978702fffd0e9e362b83921b27a29ae2b37b90f55675aad770a54ac83bb3e4de5049d4b15 languageName: node linkType: hard @@ -4304,27 +4234,26 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.0.3": - version: 10.2.1 - resolution: "make-fetch-happen@npm:10.2.1" +"make-fetch-happen@npm:^11.0.3": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" dependencies: agentkeepalive: ^4.2.1 - cacache: ^16.1.0 - http-cache-semantics: ^4.1.0 + cacache: ^17.0.0 + http-cache-semantics: ^4.1.1 http-proxy-agent: ^5.0.0 https-proxy-agent: ^5.0.0 is-lambda: ^1.0.1 lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-fetch: ^2.0.3 + minipass: ^5.0.0 + minipass-fetch: ^3.0.0 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 negotiator: ^0.6.3 promise-retry: ^2.0.1 socks-proxy-agent: ^7.0.0 - ssri: ^9.0.0 - checksum: 2332eb9a8ec96f1ffeeea56ccefabcb4193693597b132cd110734d50f2928842e22b84cfa1508e921b8385cdfd06dda9ad68645fed62b50fff629a580f5fb72c + ssri: ^10.0.0 + checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 languageName: node linkType: hard @@ -4439,19 +4368,19 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" +"minimatch@npm:^9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" dependencies: brace-expansion: ^2.0.1 - checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77 + checksum: 97f5f5284bb57dc65b9415dec7f17a0f6531a33572193991c60ff18450dcfad5c2dad24ffeaf60b5261dccd63aae58cc3306e2209d57e7f88c51295a532d8ec3 languageName: node linkType: hard "minimist@npm:^1.2.0, minimist@npm:^1.2.3": - version: 1.2.6 - resolution: "minimist@npm:1.2.6" - checksum: d15428cd1e11eb14e1233bcfb88ae07ed7a147de251441d61158619dfb32c4d7e9061d09cab4825fdee18ecd6fce323228c8c47b5ba7cd20af378ca4048fb3fb + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 languageName: node linkType: hard @@ -4464,18 +4393,18 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^2.0.3": - version: 2.1.2 - resolution: "minipass-fetch@npm:2.1.2" +"minipass-fetch@npm:^3.0.0": + version: 3.0.3 + resolution: "minipass-fetch@npm:3.0.3" dependencies: encoding: ^0.1.13 - minipass: ^3.1.6 + minipass: ^5.0.0 minipass-sized: ^1.0.3 minizlib: ^2.1.2 dependenciesMeta: encoding: optional: true - checksum: 3f216be79164e915fc91210cea1850e488793c740534985da017a4cbc7a5ff50506956d0f73bb0cb60e4fe91be08b6b61ef35101706d3ef5da2c8709b5f08f91 + checksum: af5ab2552a16fcf505d35fd7ffb84b57f4a0eeb269e6e1d9a2a75824dda48b36e527083250b7cca4a4def21d9544e2ade441e4730e233c0bc2133f6abda31e18 languageName: node linkType: hard @@ -4507,15 +4436,6 @@ __metadata: linkType: hard "minipass@npm:^3.0.0": - version: 3.1.6 - resolution: "minipass@npm:3.1.6" - dependencies: - yallist: ^4.0.0 - checksum: 57a04041413a3531a65062452cb5175f93383ef245d6f4a2961d34386eb9aa8ac11ac7f16f791f5e8bbaf1dfb1ef01596870c88e8822215db57aa591a5bb0a77 - languageName: node - linkType: hard - -"minipass@npm:^3.1.1, minipass@npm:^3.1.6": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -4524,12 +4444,17 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^4.0.0": - version: 4.0.0 - resolution: "minipass@npm:4.0.0" - dependencies: - yallist: ^4.0.0 - checksum: 7a609afbf394abfcf9c48e6c90226f471676c8f2a67f07f6838871afb03215ede431d1433feffe1b855455bcb13ef0eb89162841b9796109d6fed8d89790f381 +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2": + version: 6.0.2 + resolution: "minipass@npm:6.0.2" + checksum: d140b91f4ab2e5ce5a9b6c468c0e82223504acc89114c1a120d4495188b81fedf8cade72a9f4793642b4e66672f990f1e0d902dd858485216a07cd3c8a62fac9 languageName: node linkType: hard @@ -4616,11 +4541,11 @@ __metadata: linkType: hard "node-abi@npm:^3.3.0": - version: 3.8.0 - resolution: "node-abi@npm:3.8.0" + version: 3.45.0 + resolution: "node-abi@npm:3.45.0" dependencies: semver: ^7.3.5 - checksum: 3644dd51f4f189358ef56055407501aa698632d67448585b38c46c81a482a0c3bfb06da513ac4060a12ce5f607f208ba9d9c8280f1c38329670b709bd735fcae + checksum: 18c4305d7de5f1132741a2a66ba652941518210d02c9268702abe97ce1c166db468b4fc3e85fff04b9c19218c2e47f4e295f9a46422dc834932f4e11443400cd languageName: node linkType: hard @@ -4634,8 +4559,8 @@ __metadata: linkType: hard "node-fetch@npm:^2.6.7": - version: 2.6.9 - resolution: "node-fetch@npm:2.6.9" + version: 2.6.11 + resolution: "node-fetch@npm:2.6.11" dependencies: whatwg-url: ^5.0.0 peerDependencies: @@ -4643,18 +4568,19 @@ __metadata: peerDependenciesMeta: encoding: optional: true - checksum: acb04f9ce7224965b2b59e71b33c639794d8991efd73855b0b250921382b38331ffc9d61bce502571f6cc6e11a8905ca9b1b6d4aeb586ab093e2756a1fd190d0 + checksum: 249d0666a9497553384d46b5ab296ba223521ac88fed4d8a17d6ee6c2efb0fc890f3e8091cafe7f9fba8151a5b8d925db2671543b3409a56c3cd522b468b47b3 languageName: node linkType: hard "node-gyp@npm:latest": - version: 9.3.1 - resolution: "node-gyp@npm:9.3.1" + version: 9.4.0 + resolution: "node-gyp@npm:9.4.0" dependencies: env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 glob: ^7.1.4 graceful-fs: ^4.2.6 - make-fetch-happen: ^10.0.3 + make-fetch-happen: ^11.0.3 nopt: ^6.0.0 npmlog: ^6.0.0 rimraf: ^3.0.2 @@ -4663,7 +4589,7 @@ __metadata: which: ^2.0.2 bin: node-gyp: bin/node-gyp.js - checksum: b860e9976fa645ca0789c69e25387401b4396b93c8375489b5151a6c55cf2640a3b6183c212b38625ef7c508994930b72198338e3d09b9d7ade5acc4aaf51ea7 + checksum: 78b404e2e0639d64e145845f7f5a3cb20c0520cdaf6dda2f6e025e9b644077202ea7de1232396ba5bde3fee84cdc79604feebe6ba3ec84d464c85d407bb5da99 languageName: node linkType: hard @@ -4674,10 +4600,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.6": - version: 2.0.8 - resolution: "node-releases@npm:2.0.8" - checksum: b1ab02c0d5d8e081bf9537232777a7a787dc8fef07f70feabe70a344599b220fe16462f746ac30f3eed5a58549445ad69368964d12a1f8b3b764f6caab7ba34a +"node-releases@npm:^2.0.12": + version: 2.0.12 + resolution: "node-releases@npm:2.0.12" + checksum: b8c56db82c4642a0f443332b331a4396dae452a2ac5a65c8dbd93ef89ecb2fbb0da9d42ac5366d4764973febadca816cf7587dad492dce18d2a6b2af59cda260 languageName: node linkType: hard @@ -4704,12 +4630,12 @@ __metadata: linkType: hard "nordigen-node@npm:^1.2.3": - version: 1.2.3 - resolution: "nordigen-node@npm:1.2.3" + version: 1.2.4 + resolution: "nordigen-node@npm:1.2.4" dependencies: axios: ^1.2.1 dotenv: ^10.0.0 - checksum: 721b1b87e750ddde72e97de6b77791da71b0f7206397b485ceb8c271121d26d0e76613b95896b762f9b88eb32fd9cf83202c638a3aeade956910c6971639146b + checksum: fc955bf618099e28db9317115837bce1560ae064402516b7307be4af9241c4969c973765e9d6eaf8f312f13c7ab103a423e3ee938997477f0793f5419d3d54f9 languageName: node linkType: hard @@ -4760,10 +4686,24 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 55ba841e3adce9c4f1b9b46b41983eda40f854e0d01af2802d3ae18a7085a17168d6b81731d43fdf1d6bcbb3c9f9c56d22c8fea992203ad90a38d7d919bc28f1 + languageName: node + linkType: hard + "object-inspect@npm:^1.9.0": - version: 1.12.0 - resolution: "object-inspect@npm:1.12.0" - checksum: 2b36d4001a9c921c6b342e2965734519c9c58c355822243c3207fbf0aac271f8d44d30d2d570d450b2cc6f0f00b72bcdba515c37827d2560e5f22b1899a31cf4 + version: 1.12.3 + resolution: "object-inspect@npm:1.12.3" + checksum: dabfd824d97a5f407e6d5d24810d888859f6be394d8b733a77442b277e0808860555176719c5905e765e3743a7cada6b8b0a3b85e5331c530fd418cc8ae991db + languageName: node + linkType: hard + +"oidc-token-hash@npm:^5.0.3": + version: 5.0.3 + resolution: "oidc-token-hash@npm:5.0.3" + checksum: 35fa19aea9ff2c509029ec569d74b778c8a215b92bd5e6e9bc4ebbd7ab035f44304ff02430a6397c3fb7c1d15ebfa467807ca0bcd31d06ba610b47798287d303 languageName: node linkType: hard @@ -4801,6 +4741,18 @@ __metadata: languageName: node linkType: hard +"openid-client@npm:^5.4.2": + version: 5.4.2 + resolution: "openid-client@npm:5.4.2" + dependencies: + jose: ^4.14.1 + lru-cache: ^6.0.0 + object-hash: ^2.2.0 + oidc-token-hash: ^5.0.3 + checksum: 0f3570990a4979aff8581de35078c82d21d45baed9805e0e385dfc62c24fa295ee82d86386846a33bc33ed3b524a5406d8564f9f927420027719f84bf1d8b741 + languageName: node + linkType: hard + "optionator@npm:^0.9.1": version: 0.9.1 resolution: "optionator@npm:0.9.1" @@ -4868,11 +4820,11 @@ __metadata: linkType: hard "parent-module@npm:^1.0.0": - version: 1.0.0 - resolution: "parent-module@npm:1.0.0" + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" dependencies: callsites: ^3.0.0 - checksum: 5ee62c5ac2980114482051f5441720df5f413c87d6531d8e780c8988ab4f136e4430715f4c323a456e25ec03434c92a96ecd68c3bf2db26c1530a6ed8b364ee9 + checksum: 6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff languageName: node linkType: hard @@ -4923,6 +4875,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.7.0": + version: 1.9.2 + resolution: "path-scurry@npm:1.9.2" + dependencies: + lru-cache: ^9.1.1 + minipass: ^5.0.0 || ^6.0.2 + checksum: 92888dfb68e285043c6d3291c8e971d5d2bc2f5082f4d7b5392896f34be47024c9d0a8b688dd7ae6d125acc424699195474927cb4f00049a9b1ec7c4256fa8e0 + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.7": version: 0.1.7 resolution: "path-to-regexp@npm:0.1.7" @@ -4952,9 +4914,9 @@ __metadata: linkType: hard "pirates@npm:^4.0.4": - version: 4.0.5 - resolution: "pirates@npm:4.0.5" - checksum: c9994e61b85260bec6c4fc0307016340d9b0c4f4b6550a957afaaff0c9b1ad58fbbea5cfcf083860a25cb27a375442e2b0edf52e2e1e40e69934e08dcc52d227 + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 46a65fefaf19c6f57460388a5af9ab81e3d7fd0e7bc44ca59d753cb5c4d0df97c6c6e583674869762101836d68675f027d60f841c105d72734df9dfca97cbcc6 languageName: node linkType: hard @@ -5006,29 +4968,22 @@ __metadata: linkType: hard "prettier@npm:^2.8.3": - version: 2.8.4 - resolution: "prettier@npm:2.8.4" + version: 2.8.8 + resolution: "prettier@npm:2.8.8" bin: prettier: bin-prettier.js - checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e + checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.4.1": - version: 29.4.1 - resolution: "pretty-format@npm:29.4.1" +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.5.0": + version: 29.5.0 + resolution: "pretty-format@npm:29.5.0" dependencies: - "@jest/schemas": ^29.4.0 + "@jest/schemas": ^29.4.3 ansi-styles: ^5.0.0 react-is: ^18.0.0 - checksum: bcc8e86bcf8e7f5106c96e2ea7905912bd17ae2aac76e4e0745d2a50df4b340638ed95090ee455a1c0f78189efa05077bd655ca08bf66292e83ebd7035fc46fd - languageName: node - linkType: hard - -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 22749483091d2c594261517f4f80e05226d4d5ecc1fc917e1886929da56e22b5718b7f2a75f3807e7a7d471bc3be2907fe92e6e8f373ddf5c64bae35b5af3981 + checksum: 4065356b558e6db25b4d41a01efb386935a6c06a0c9c104ef5ce59f2f476b8210edb8b3949b386e60ada0a6dc5ebcb2e6ccddc8c64dfd1a9943c3c3a9e7eaf89 languageName: node linkType: hard @@ -5089,9 +5044,16 @@ __metadata: linkType: hard "punycode@npm:^2.1.0": - version: 2.1.1 - resolution: "punycode@npm:2.1.1" - checksum: 823bf443c6dd14f669984dea25757b37993f67e8d94698996064035edd43bed8a5a17a9f12e439c2b35df1078c6bec05a6c86e336209eb1061e8025c481168e8 + version: 2.3.0 + resolution: "punycode@npm:2.3.0" + checksum: 39f760e09a2a3bbfe8f5287cf733ecdad69d6af2fe6f97ca95f24b8921858b91e9ea3c9eeec6e08cede96181b3bb33f95c6ffd8c77e63986508aa2e8159fa200 + languageName: node + linkType: hard + +"pure-rand@npm:^6.0.0": + version: 6.0.2 + resolution: "pure-rand@npm:6.0.2" + checksum: 79de33876a4f515d759c48e98d00756bbd916b4ea260cc572d7adfa4b62cace9952e89f0241d0410214554503d25061140fe325c66f845213d2b1728ba8d413e languageName: node linkType: hard @@ -5130,6 +5092,18 @@ __metadata: languageName: node linkType: hard +"raw-body@npm:2.5.2": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + checksum: ba1583c8d8a48e8fbb7a873fdbb2df66ea4ff83775421bfe21ee120140949ab048200668c47d9ae3880012f6e217052690628cf679ddfbd82c9fc9358d574676 + languageName: node + linkType: hard + "rc@npm:^1.2.7": version: 1.2.8 resolution: "rc@npm:1.2.8" @@ -5152,20 +5126,13 @@ __metadata: linkType: hard "readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": - version: 3.6.0 - resolution: "readable-stream@npm:3.6.0" + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" dependencies: inherits: ^2.0.3 string_decoder: ^1.1.1 util-deprecate: ^1.0.1 - checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 - languageName: node - linkType: hard - -"regexpp@npm:^3.2.0": - version: 3.2.0 - resolution: "regexpp@npm:3.2.0" - checksum: a78dc5c7158ad9ddcfe01aa9144f46e192ddbfa7b263895a70a5c6c73edd9ce85faf7c0430e59ac38839e1734e275b9c3de5c57ee3ab6edc0e0b1bdebefccef8 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d languageName: node linkType: hard @@ -5200,35 +5167,35 @@ __metadata: linkType: hard "resolve.exports@npm:^2.0.0": - version: 2.0.0 - resolution: "resolve.exports@npm:2.0.0" - checksum: d8bee3b0cc0a0ae6c8323710983505bc6a3a2574f718e96f01e048a0f0af035941434b386cc9efc7eededc5e1199726185c306ec6f6a1aa55d5fbad926fd0634 + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 1c7778ca1b86a94f8ab4055d196c7d87d1874b96df4d7c3e67bbf793140f0717fd506dcafd62785b079cd6086b9264424ad634fb904409764c3509c3df1653f2 languageName: node linkType: hard "resolve@npm:^1.20.0": - version: 1.22.1 - resolution: "resolve@npm:1.22.1" + version: 1.22.2 + resolution: "resolve@npm:1.22.2" dependencies: - is-core-module: ^2.9.0 + is-core-module: ^2.11.0 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: 07af5fc1e81aa1d866cbc9e9460fbb67318a10fa3c4deadc35c3ad8a898ee9a71a86a65e4755ac3195e0ea0cfbe201eb323ebe655ce90526fd61917313a34e4e + checksum: 7e5df75796ebd429445d102d5824482ee7e567f0070b2b45897b29bb4f613dcbc262e0257b8aeedb3089330ccaea0d6a0464df1a77b2992cf331dcda0f4cb549 languageName: node linkType: hard "resolve@patch:resolve@^1.20.0#~builtin": - version: 1.22.1 - resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" + version: 1.22.2 + resolution: "resolve@patch:resolve@npm%3A1.22.2#~builtin::version=1.22.2&hash=c3c19d" dependencies: - is-core-module: ^2.9.0 + is-core-module: ^2.11.0 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: 5656f4d0bedcf8eb52685c1abdf8fbe73a1603bb1160a24d716e27a57f6cecbe2432ff9c89c2bd57542c3a7b9d14b1882b73bfe2e9d7849c9a4c0b8b39f02b8b + checksum: 66cc788f13b8398de18eb4abb3aed90435c84bb8935953feafcf7231ba4cd191b2c10b4a87b1e9681afc34fb138c705f91f7330ff90bfa36f457e5584076a2b8 languageName: node linkType: hard @@ -5289,25 +5256,14 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5": - version: 7.3.5 - resolution: "semver@npm:7.3.5" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: 5eafe6102bea2a7439897c1856362e31cc348ccf96efd455c8b5bc2c61e6f7e7b8250dc26b8828c1d76a56f818a7ee907a36ae9fb37a599d3d24609207001d60 - languageName: node - linkType: hard - -"semver@npm:^7.3.7, semver@npm:^7.3.8": - version: 7.3.8 - resolution: "semver@npm:7.3.8" +"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8": + version: 7.5.2 + resolution: "semver@npm:7.5.2" dependencies: lru-cache: ^6.0.0 bin: semver: bin/semver.js - checksum: ba9c7cbbf2b7884696523450a61fee1a09930d888b7a8d7579025ad93d459b2d1949ee5bbfeb188b2be5f4ac163544c5e98491ad6152df34154feebc2cc337c1 + checksum: 3fdf5d1e6f170fe8bcc41669e31787649af91af7f54f05c71d0865bb7aa27e8b92f68b3e6b582483e2c1c648008bc84249d2cd86301771fe5cbf7621d1fe5375 languageName: node linkType: hard @@ -5385,20 +5341,20 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0": - version: 3.0.3 - resolution: "signal-exit@npm:3.0.3" - checksum: f0169d3f1263d06df32ca072b0bf33b34c6f8f0341a7a1621558a2444dfbe8f5fec76b35537fcc6f0bc4944bdb5336fe0bdcf41a5422c4e45a1dba3f45475e6c - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.0.2 + resolution: "signal-exit@npm:4.0.2" + checksum: 41f5928431cc6e91087bf0343db786a6313dd7c6fd7e551dbc141c95bb5fb26663444fd9df8ea47c5d7fc202f60aa7468c3162a9365cbb0615fc5e1b1328fe31 + languageName: node + linkType: hard + "simple-concat@npm:^1.0.0": version: 1.0.1 resolution: "simple-concat@npm:1.0.1" @@ -5483,12 +5439,12 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^9.0.0": - version: 9.0.1 - resolution: "ssri@npm:9.0.1" +"ssri@npm:^10.0.0": + version: 10.0.4 + resolution: "ssri@npm:10.0.4" dependencies: - minipass: ^3.1.1 - checksum: fb58f5e46b6923ae67b87ad5ef1c5ab6d427a17db0bead84570c2df3cd50b4ceb880ebdba2d60726588272890bae842a744e1ecce5bd2a2a582fccd5068309eb + minipass: ^5.0.0 + checksum: fb14da9f8a72b04eab163eb13a9dda11d5962cd2317f85457c4e0b575e9a6e0e3a6a87b5bf122c75cb36565830cd5f263fb457571bf6f1587eb5f95d095d6165 languageName: node linkType: hard @@ -5518,7 +5474,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -5529,6 +5485,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + "string_decoder@npm:^1.1.1": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" @@ -5538,7 +5505,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -5547,6 +5514,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: ^6.0.1 + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + "strip-bom@npm:^4.0.0": version: 4.0.0 resolution: "strip-bom@npm:4.0.0" @@ -5662,31 +5638,17 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.1.11": - version: 6.1.11 - resolution: "tar@npm:6.1.11" +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.1.15 + resolution: "tar@npm:6.1.15" dependencies: chownr: ^2.0.0 fs-minipass: ^2.0.0 - minipass: ^3.0.0 + minipass: ^5.0.0 minizlib: ^2.1.1 mkdirp: ^1.0.3 yallist: ^4.0.0 - checksum: a04c07bb9e2d8f46776517d4618f2406fb977a74d914ad98b264fc3db0fe8224da5bec11e5f8902c5b9bcb8ace22d95fbe3c7b36b8593b7dfc8391a25898f32f - languageName: node - linkType: hard - -"tar@npm:^6.1.2": - version: 6.1.13 - resolution: "tar@npm:6.1.13" - dependencies: - chownr: ^2.0.0 - fs-minipass: ^2.0.0 - minipass: ^4.0.0 - minizlib: ^2.1.1 - mkdirp: ^1.0.3 - yallist: ^4.0.0 - checksum: 8a278bed123aa9f53549b256a36b719e317c8b96fe86a63406f3c62887f78267cea9b22dc6f7007009738509800d4a4dccc444abd71d762287c90f35b002eb1c + checksum: f23832fceeba7578bf31907aac744ae21e74a66f4a17a9e94507acf460e48f6db598c7023882db33bab75b80e027c21f276d405e4a0322d58f51c7088d428268 languageName: node linkType: hard @@ -5832,21 +5794,21 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^2.0.0": - version: 2.0.1 - resolution: "unique-filename@npm:2.0.1" +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" dependencies: - unique-slug: ^3.0.0 - checksum: 807acf3381aff319086b64dc7125a9a37c09c44af7620bd4f7f3247fcd5565660ac12d8b80534dcbfd067e6fe88a67e621386dd796a8af828d1337a8420a255f + unique-slug: ^4.0.0 + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df languageName: node linkType: hard -"unique-slug@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-slug@npm:3.0.0" +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" dependencies: imurmurhash: ^0.1.4 - checksum: 49f8d915ba7f0101801b922062ee46b7953256c93ceca74303bd8e6413ae10aa7e8216556b54dc5382895e8221d04f1efaf75f945c2e4a515b4139f77aa6640c + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 languageName: node linkType: hard @@ -5857,26 +5819,26 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.9": - version: 1.0.10 - resolution: "update-browserslist-db@npm:1.0.10" +"update-browserslist-db@npm:^1.0.11": + version: 1.0.11 + resolution: "update-browserslist-db@npm:1.0.11" dependencies: escalade: ^3.1.1 picocolors: ^1.0.0 peerDependencies: browserslist: ">= 4.21.0" bin: - browserslist-lint: cli.js - checksum: 12db73b4f63029ac407b153732e7cd69a1ea8206c9100b482b7d12859cd3cd0bc59c602d7ae31e652706189f1acb90d42c53ab24a5ba563ed13aebdddc5561a0 + update-browserslist-db: cli.js + checksum: b98327518f9a345c7cad5437afae4d2ae7d865f9779554baf2a200fdf4bac4969076b679b1115434bd6557376bdd37ca7583d0f9b8f8e302d7d4cc1e91b5f231 languageName: node linkType: hard "uri-js@npm:^4.2.2": - version: 4.2.2 - resolution: "uri-js@npm:4.2.2" + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" dependencies: punycode: ^2.1.0 - checksum: 5a91c55d8ae6d9a1ff9dc1b0774888a99aae7cc6e9056c57b709275c0f6753b05cd1a9f2728a1479244b93a9f57ab37c60d277a48d9f2d032d6ae65837bf9bc7 + checksum: 7167432de6817fe8e9e0c9684f1d2de2bb688c94388f7569f7dbdb1587c9f4ca2a77962f134ec90be0cc4d004c939ff0d05acc9f34a0db39a3c797dada262633 languageName: node linkType: hard @@ -5913,13 +5875,13 @@ __metadata: linkType: hard "v8-to-istanbul@npm:^9.0.1": - version: 9.0.1 - resolution: "v8-to-istanbul@npm:9.0.1" + version: 9.1.0 + resolution: "v8-to-istanbul@npm:9.1.0" dependencies: "@jridgewell/trace-mapping": ^0.3.12 "@types/istanbul-lib-coverage": ^2.0.1 convert-source-map: ^1.6.0 - checksum: a49c34bf0a3af0c11041a3952a2600913904a983bd1bc87148b5c033bc5c1d02d5a13620fcdbfa2c60bc582a2e2970185780f0c844b4c3a220abf405f8af6311 + checksum: 2069d59ee46cf8d83b4adfd8a5c1a90834caffa9f675e4360f1157ffc8578ef0f763c8f32d128334424159bb6b01f3876acd39cd13297b2769405a9da241f8d1 languageName: node linkType: hard @@ -5983,7 +5945,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -5994,6 +5956,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -6001,13 +5974,13 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^5.0.0": - version: 5.0.0 - resolution: "write-file-atomic@npm:5.0.0" +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" dependencies: imurmurhash: ^0.1.4 signal-exit: ^3.0.7 - checksum: 6ee16b195572386cb1c905f9d29808f77f4de2fd063d74a6f1ab6b566363832d8906a493b764ee715e57ab497271d5fc91642a913724960e8e845adf504a9837 + checksum: 5da60bd4eeeb935eec97ead3df6e28e5917a6bd317478e4a85a5285e8480b8ed96032bbcc6ecd07b236142a24f3ca871c924ec4a6575e623ec1b11bf8c1c253c languageName: node linkType: hard @@ -6040,8 +6013,8 @@ __metadata: linkType: hard "yargs@npm:^17.3.1": - version: 17.6.2 - resolution: "yargs@npm:17.6.2" + version: 17.7.2 + resolution: "yargs@npm:17.7.2" dependencies: cliui: ^8.0.1 escalade: ^3.1.1 @@ -6050,7 +6023,7 @@ __metadata: string-width: ^4.2.3 y18n: ^5.0.5 yargs-parser: ^21.1.1 - checksum: 47da1b0d854fa16d45a3ded57b716b013b2179022352a5f7467409da5a04a1eef5b3b3d97a2dfc13e8bbe5f2ffc0afe3bc6a4a72f8254e60f5a4bd7947138643 + checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a languageName: node linkType: hard From d4100867a128ba6c52335b8d97e8ed14b052c46a Mon Sep 17 00:00:00 2001 From: apilat Date: Thu, 22 Jun 2023 00:41:01 +0100 Subject: [PATCH 002/139] Fix password login and session token initialization --- src/account-db.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 393d94eba..fd46c08ef 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -114,6 +114,9 @@ export function bootstrap(loginSettings) { ); } + const token = uuid.v4(); + accountDb.mutate('INSERT INTO sessions (token) VALUES (?)', [token]); + return {}; } @@ -122,14 +125,14 @@ export function loginWithPassword(password) { let row = accountDb.first( "SELECT extra_data FROM auth WHERE method = 'password'", ); - let confirmed = row && bcrypt.compareSync(password, row.password); + let confirmed = row && bcrypt.compareSync(password, row.extra_data); if (confirmed) { // Right now, tokens are permanent and there's just one in the // system. In the future this should probably evolve to be a // "session" that times out after a long time or something, and // maybe each device has a different token - let row = accountDb.first('SELECT * FROM sessions'); + let row = accountDb.first('SELECT token FROM sessions'); return row.token; } else { return null; From 0331665764a2102bf536c9413cd8df0c4f7ed7ea Mon Sep 17 00:00:00 2001 From: apilat Date: Fri, 23 Jun 2023 11:50:14 +0100 Subject: [PATCH 003/139] Disallow multiple authentication methods simultaneously. Note this is only disallowed at bootstrap. If the database is edited manually, we make no attempt to detect this. --- src/account-db.js | 40 ++++++++++++++++++++-------------------- src/load-config.js | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index fd46c08ef..1cb142f80 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -60,21 +60,34 @@ export function bootstrap(loginSettings) { return { error: 'already-bootstrapped' }; } - if ( - !loginSettings.hasOwnProperty('password') && - !loginSettings.hasOwnProperty('openid') - ) { + const passEnabled = loginSettings.hasOwnProperty('password'); + const openIdEnabled = loginSettings.hasOwnProperty('openid'); + + if (!passEnabled && !openIdEnabled) { return { error: 'no-auth-method-selected' }; } - if (loginSettings.hasOwnProperty('password')) { + if (passEnabled && openIdEnabled) { + return { error: 'max-one-method-allowed' }; + } + + if (passEnabled) { const password = loginSettings.password; if (password === null || password === '') { return { error: 'invalid-password' }; } + + // Hash the password. There's really not a strong need for this + // since this is a self-hosted instance owned by the user. + // However, just in case we do it. + let hashed = hashPassword(loginSettings.password); + accountDb.mutate( + "INSERT INTO auth (method, extra_data) VALUES ('password', ?)", + [hashed], + ); } - if (loginSettings.hasOwnProperty('openid')) { + if (openIdEnabled) { const config = loginSettings.openid; if (!config.hasOwnProperty('issuer')) { return { error: 'missing-issuer' }; @@ -88,26 +101,13 @@ export function bootstrap(loginSettings) { if (!config.hasOwnProperty('server_hostname')) { return { error: 'missing-server-hostname' }; } + // Beyond verifying that the configuration exists, we do not attempt // to check if the configuration is actually correct. // If the user improperly configures this during bootstrap, there is // no way to recover without manually editing the database. However, // this might not be a real issue since an analogous situation happens // if they forget their password. - } - - if (loginSettings.hasOwnProperty('password')) { - // Hash the password. There's really not a strong need for this - // since this is a self-hosted instance owned by the user. - // However, just in case we do it. - let hashed = hashPassword(loginSettings.password); - accountDb.mutate( - "INSERT INTO auth (method, extra_data) VALUES ('password', ?)", - [hashed], - ); - } - - if (loginSettings.hasOwnProperty('openid')) { accountDb.mutate( "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", [JSON.stringify(loginSettings.openid)], diff --git a/src/load-config.js b/src/load-config.js index ead48d462..45fecc262 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -10,7 +10,7 @@ const projectRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))); debug(`project root: '${projectRoot}'`); export const sqlDir = path.join(projectRoot, 'src', 'sql'); -let defaultDataDir = projectRoot; //fs.existsSync('/data') ? '/data' : projectRoot; +let defaultDataDir = fs.existsSync('/data') ? '/data' : projectRoot; debug(`default data directory: '${defaultDataDir}'`); function parseJSON(path, allowMissing = false) { From 8f87966f4d62ed4db6773596dfb01f09b531c0fb Mon Sep 17 00:00:00 2001 From: apilat Date: Fri, 23 Jun 2023 12:07:41 +0100 Subject: [PATCH 004/139] Refactored account-db.js into separate files in accounts/ --- src/account-db.js | 280 -------------------------------- src/accounts/index.js | 100 ++++++++++++ src/accounts/openid.js | 142 ++++++++++++++++ src/accounts/password.js | 63 +++++++ src/app-account.js | 9 +- src/app-sync.js | 2 +- src/app-sync.test.js | 2 +- src/scripts/reset-password.js | 3 +- src/services/secrets-service.js | 2 +- src/util/validate-user.js | 2 +- 10 files changed, 316 insertions(+), 289 deletions(-) delete mode 100644 src/account-db.js create mode 100644 src/accounts/index.js create mode 100644 src/accounts/openid.js create mode 100644 src/accounts/password.js diff --git a/src/account-db.js b/src/account-db.js deleted file mode 100644 index 1cb142f80..000000000 --- a/src/account-db.js +++ /dev/null @@ -1,280 +0,0 @@ -import fs from 'node:fs'; -import { join } from 'node:path'; -import openDatabase from './db.js'; -import config, { sqlDir } from './load-config.js'; -import createDebug from 'debug'; -import * as uuid from 'uuid'; -import * as bcrypt from 'bcrypt'; -import { generators, Issuer } from 'openid-client'; - -const debug = createDebug('actual:account-db'); - -let _accountDb = null; - -export default function getAccountDb() { - if (_accountDb == null) { - if (!fs.existsSync(config.serverFiles)) { - debug(`creating server files directory: '${config.serverFiles}'`); - fs.mkdirSync(config.serverFiles); - } - - let dbPath = join(config.serverFiles, 'account.sqlite'); - let needsInit = !fs.existsSync(dbPath); - - _accountDb = openDatabase(dbPath); - - if (needsInit) { - debug(`initializing account database: '${dbPath}'`); - let initSql = fs.readFileSync(join(sqlDir, 'account.sql'), 'utf8'); - _accountDb.exec(initSql); - } else { - debug(`opening account database: '${dbPath}'`); - } - } - - return _accountDb; -} - -function hashPassword(password) { - return bcrypt.hashSync(password, 12); -} - -export function needsBootstrap() { - let accountDb = getAccountDb(); - let row = accountDb.first('SELECT count(*) FROM auth'); - return row['count(*)'] === 0; -} - -// Supported login settings: -// "password": "secret_password", -// "openid": { -// "issuer": "https://example.org", -// "client_id": "your_client_id", -// "client_secret": "your_client_secret", -// "server_hostname": "https://actual.your_website.com" -// } -export function bootstrap(loginSettings) { - let accountDb = getAccountDb(); - - if (!needsBootstrap()) { - return { error: 'already-bootstrapped' }; - } - - const passEnabled = loginSettings.hasOwnProperty('password'); - const openIdEnabled = loginSettings.hasOwnProperty('openid'); - - if (!passEnabled && !openIdEnabled) { - return { error: 'no-auth-method-selected' }; - } - - if (passEnabled && openIdEnabled) { - return { error: 'max-one-method-allowed' }; - } - - if (passEnabled) { - const password = loginSettings.password; - if (password === null || password === '') { - return { error: 'invalid-password' }; - } - - // Hash the password. There's really not a strong need for this - // since this is a self-hosted instance owned by the user. - // However, just in case we do it. - let hashed = hashPassword(loginSettings.password); - accountDb.mutate( - "INSERT INTO auth (method, extra_data) VALUES ('password', ?)", - [hashed], - ); - } - - if (openIdEnabled) { - const config = loginSettings.openid; - if (!config.hasOwnProperty('issuer')) { - return { error: 'missing-issuer' }; - } - if (!config.hasOwnProperty('client_id')) { - return { error: 'missing-client-id' }; - } - if (!config.hasOwnProperty('client_secret')) { - return { error: 'missing-client-secret' }; - } - if (!config.hasOwnProperty('server_hostname')) { - return { error: 'missing-server-hostname' }; - } - - // Beyond verifying that the configuration exists, we do not attempt - // to check if the configuration is actually correct. - // If the user improperly configures this during bootstrap, there is - // no way to recover without manually editing the database. However, - // this might not be a real issue since an analogous situation happens - // if they forget their password. - accountDb.mutate( - "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", - [JSON.stringify(loginSettings.openid)], - ); - } - - const token = uuid.v4(); - accountDb.mutate('INSERT INTO sessions (token) VALUES (?)', [token]); - - return {}; -} - -export function loginWithPassword(password) { - let accountDb = getAccountDb(); - let row = accountDb.first( - "SELECT extra_data FROM auth WHERE method = 'password'", - ); - let confirmed = row && bcrypt.compareSync(password, row.extra_data); - - if (confirmed) { - // Right now, tokens are permanent and there's just one in the - // system. In the future this should probably evolve to be a - // "session" that times out after a long time or something, and - // maybe each device has a different token - let row = accountDb.first('SELECT token FROM sessions'); - return row.token; - } else { - return null; - } -} - -async function setupOpenIdClient(config) { - const issuer = await Issuer.discover(config.issuer); - - const client = new issuer.Client({ - client_id: config.client_id, - client_secret: config.client_secret, - redirect_uri: config.server_hostname + '/account/login-openid/cb', - }); - - return client; -} - -export async function loginWithOpenIdSetup(body) { - if (!body.return_url) { - return { error: 'return-url-missing' }; - } - - let accountDb = getAccountDb(); - let config = accountDb.first( - "SELECT extra_data FROM auth WHERE method = 'openid'", - ); - if (!config) { - return { error: 'openid-not-configured' }; - } - config = JSON.parse(config['extra_data']); - - let client; - try { - client = await setupOpenIdClient(config); - } catch (err) { - return { error: 'openid-setup-failed: ' + err }; - } - - const state = generators.state(); - const code_verifier = generators.codeVerifier(); - const code_challenge = generators.codeChallenge(code_verifier); - - const now_time = Date.now(); - const expiry_time = now_time + 300 * 1000; - - accountDb.mutate( - 'DELETE FROM pending_openid_requests WHERE expiry_time < ?', - [now_time], - ); - accountDb.mutate( - 'INSERT INTO pending_openid_requests (state, code_verifier, return_url, expiry_time) VALUES (?, ?, ?, ?)', - [state, code_verifier, body.return_url, expiry_time], - ); - - const url = client.authorizationUrl({ - response_type: 'code', - scope: 'openid', - state, - code_challenge, - code_challenge_method: 'S256', - }); - - return { url }; -} - -export async function loginWithOpenIdFinalize(body) { - if (!body.code) { - return { error: 'missing-authorization-code' }; - } - if (!body.state) { - return { error: 'missing-state' }; - } - - let accountDb = getAccountDb(); - let config = accountDb.first( - "SELECT extra_data FROM auth WHERE method = 'openid'", - ); - if (!config) { - return { error: 'openid-not-configured' }; - } - config = JSON.parse(config['extra_data']); - - let client; - try { - client = await setupOpenIdClient(config); - } catch (err) { - return { error: 'openid-setup-failed: ' + err }; - } - - let { code_verifier, return_url } = accountDb.first( - 'SELECT code_verifier, return_url FROM pending_openid_requests WHERE state = ? AND expiry_time > ?', - [body.state, Date.now()], - ); - - try { - let grant = await client.grant({ - grant_type: 'authorization_code', - code: body.code, - code_verifier, - redirect_uri: client.redirect_uris[0], - }); - await client.userinfo(grant); - // The server requests have succeeded, so the user has been authenticated. - // Ideally, we would create a session token here tied to the returned access token - // and verify it with the server whenever the user connects. - // However, the rest of this server code uses only a single permanent token, - // so that is what we do here as well. - } catch (err) { - return { error: 'openid-grant-failed: ' + err }; - } - - let { token } = accountDb.first('SELECT token FROM sessions'); - return { url: `${return_url}/login/openid-cb?token=${token}` }; -} - -export function changePassword(newPassword) { - let accountDb = getAccountDb(); - - if (newPassword == null || newPassword === '') { - return { error: 'invalid-password' }; - } - - let hashed = hashPassword(newPassword); - let token = uuid.v4(); - // This query does nothing if password authentication was disabled during - // bootstrap (then no row with method=password exists). Maybe we should - // return an error here if that is the case? - accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [ - hashed, - ]); - accountDb.mutate('UPDATE sessions SET token = ?', [token]); - return {}; -} - -export function getSession(token) { - let accountDb = getAccountDb(); - return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); -} - -export function listLoginMethods() { - let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT method FROM auth'); - return rows.map((r) => r['method']); -} diff --git a/src/accounts/index.js b/src/accounts/index.js new file mode 100644 index 000000000..e36a69502 --- /dev/null +++ b/src/accounts/index.js @@ -0,0 +1,100 @@ +import fs from 'node:fs'; +import { join } from 'node:path'; +import openDatabase from '../db.js'; +import config, { sqlDir } from '../load-config.js'; +import createDebug from 'debug'; +import * as uuid from 'uuid'; +import { bootstrapPassword } from './password.js'; +import { bootstrapOpenId } from './openid.js'; + +const debug = createDebug('actual:account-db'); + +let _accountDb = null; + +export default function getAccountDb() { + if (_accountDb == null) { + if (!fs.existsSync(config.serverFiles)) { + debug(`creating server files directory: '${config.serverFiles}'`); + fs.mkdirSync(config.serverFiles); + } + + let dbPath = join(config.serverFiles, 'account.sqlite'); + let needsInit = !fs.existsSync(dbPath); + + _accountDb = openDatabase(dbPath); + + if (needsInit) { + debug(`initializing account database: '${dbPath}'`); + let initSql = fs.readFileSync(join(sqlDir, 'account.sql'), 'utf8'); + _accountDb.exec(initSql); + } else { + debug(`opening account database: '${dbPath}'`); + } + } + + return _accountDb; +} + +export function needsBootstrap() { + let accountDb = getAccountDb(); + let row = accountDb.first('SELECT count(*) FROM auth'); + return row['count(*)'] === 0; +} + +// Supported login settings: +// "password": "secret_password", +// "openid": { +// "issuer": "https://example.org", +// "client_id": "your_client_id", +// "client_secret": "your_client_secret", +// "server_hostname": "https://actual.your_website.com" +// } +export function bootstrap(loginSettings) { + let accountDb = getAccountDb(); + // TODO We should use a transaction here to make bootstrap atomic + + if (!needsBootstrap()) { + return { error: 'already-bootstrapped' }; + } + + const passEnabled = loginSettings.hasOwnProperty('password'); + const openIdEnabled = loginSettings.hasOwnProperty('openid'); + + if (!passEnabled && !openIdEnabled) { + return { error: 'no-auth-method-selected' }; + } + + if (passEnabled && openIdEnabled) { + return { error: 'max-one-method-allowed' }; + } + + if (passEnabled) { + let { error } = bootstrapPassword(loginSettings.password); + if (error) { + return { error }; + } + } + + if (openIdEnabled) { + let { error } = bootstrapOpenId(loginSettings.openid); + if (error) { + return { error }; + } + } + + const token = uuid.v4(); + accountDb.mutate('INSERT INTO sessions (token) VALUES (?)', [token]); + + return {}; +} + +export function getSession(token) { + let accountDb = getAccountDb(); + return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); +} + +export function listLoginMethods() { + let accountDb = getAccountDb(); + let rows = accountDb.all('SELECT method FROM auth'); + return rows.map((r) => r['method']); +} diff --git a/src/accounts/openid.js b/src/accounts/openid.js new file mode 100644 index 000000000..202436022 --- /dev/null +++ b/src/accounts/openid.js @@ -0,0 +1,142 @@ +import getAccountDb from "./index.js"; +import { generators, Issuer } from 'openid-client'; + +export function bootstrapOpenId(config) { + if (!config.hasOwnProperty('issuer')) { + return { error: 'missing-issuer' }; + } + if (!config.hasOwnProperty('client_id')) { + return { error: 'missing-client-id' }; + } + if (!config.hasOwnProperty('client_secret')) { + return { error: 'missing-client-secret' }; + } + if (!config.hasOwnProperty('server_hostname')) { + return { error: 'missing-server-hostname' }; + } + + // Beyond verifying that the configuration exists, we do not attempt + // to check if the configuration is actually correct. + // If the user improperly configures this during bootstrap, there is + // no way to recover without manually editing the database. However, + // this might not be a real issue since an analogous situation happens + // if they forget their password. + let accountDb = getAccountDb(); + accountDb.mutate( + "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", + [JSON.stringify(config)], + ); + + return {}; +} + +async function setupOpenIdClient(config) { + const issuer = await Issuer.discover(config.issuer); + + const client = new issuer.Client({ + client_id: config.client_id, + client_secret: config.client_secret, + redirect_uri: config.server_hostname + '/account/login-openid/cb', + }); + + return client; +} + +export async function loginWithOpenIdSetup(body) { + if (!body.return_url) { + return { error: 'return-url-missing' }; + } + + let accountDb = getAccountDb(); + let config = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'openid'", + ); + if (!config) { + return { error: 'openid-not-configured' }; + } + config = JSON.parse(config['extra_data']); + + let client; + try { + client = await setupOpenIdClient(config); + } catch (err) { + return { error: 'openid-setup-failed: ' + err }; + } + + const state = generators.state(); + const code_verifier = generators.codeVerifier(); + const code_challenge = generators.codeChallenge(code_verifier); + + const now_time = Date.now(); + const expiry_time = now_time + 300 * 1000; + + accountDb.mutate( + 'DELETE FROM pending_openid_requests WHERE expiry_time < ?', + [now_time], + ); + accountDb.mutate( + 'INSERT INTO pending_openid_requests (state, code_verifier, return_url, expiry_time) VALUES (?, ?, ?, ?)', + [state, code_verifier, body.return_url, expiry_time], + ); + + const url = client.authorizationUrl({ + response_type: 'code', + scope: 'openid', + state, + code_challenge, + code_challenge_method: 'S256', + }); + + return { url }; +} + +export async function loginWithOpenIdFinalize(body) { + if (!body.code) { + return { error: 'missing-authorization-code' }; + } + if (!body.state) { + return { error: 'missing-state' }; + } + + let accountDb = getAccountDb(); + let config = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'openid'", + ); + if (!config) { + return { error: 'openid-not-configured' }; + } + config = JSON.parse(config['extra_data']); + + let client; + try { + client = await setupOpenIdClient(config); + } catch (err) { + return { error: 'openid-setup-failed: ' + err }; + } + + let { code_verifier, return_url } = accountDb.first( + 'SELECT code_verifier, return_url FROM pending_openid_requests WHERE state = ? AND expiry_time > ?', + [body.state, Date.now()], + ); + + try { + let grant = await client.grant({ + grant_type: 'authorization_code', + code: body.code, + code_verifier, + redirect_uri: client.redirect_uris[0], + }); + await client.userinfo(grant); + // The server requests have succeeded, so the user has been authenticated. + // Ideally, we would create a session token here tied to the returned access token + // and verify it with the server whenever the user connects. + // However, the rest of this server code uses only a single permanent token, + // so that is what we do here as well. + } catch (err) { + return { error: 'openid-grant-failed: ' + err }; + } + + let { token } = accountDb.first('SELECT token FROM sessions'); + return { url: `${return_url}/login/openid-cb?token=${token}` }; +} + diff --git a/src/accounts/password.js b/src/accounts/password.js new file mode 100644 index 000000000..c23450417 --- /dev/null +++ b/src/accounts/password.js @@ -0,0 +1,63 @@ +import * as bcrypt from 'bcrypt'; +import getAccountDb from './index.js'; +import * as uuid from 'uuid'; + +function hashPassword(password) { + return bcrypt.hashSync(password, 12); +} + +export function bootstrapPassword(password) { + if (password === null || password === '') { + return { error: 'invalid-password' }; + } + + // Hash the password. There's really not a strong need for this + // since this is a self-hosted instance owned by the user. + // However, just in case we do it. + let hashed = hashPassword(password); + let accountDb = getAccountDb(); + accountDb.mutate( + "INSERT INTO auth (method, extra_data) VALUES ('password', ?)", + [hashed], + ); + + return {}; +} + +export function loginWithPassword(password) { + let accountDb = getAccountDb(); + let row = accountDb.first( + "SELECT extra_data FROM auth WHERE method = 'password'", + ); + let confirmed = row && bcrypt.compareSync(password, row.extra_data); + + if (confirmed) { + // Right now, tokens are permanent and there's just one in the + // system. In the future this should probably evolve to be a + // "session" that times out after a long time or something, and + // maybe each device has a different token + let row = accountDb.first('SELECT token FROM sessions'); + return row.token; + } else { + return null; + } +} + +export function changePassword(newPassword) { + let accountDb = getAccountDb(); + + if (newPassword == null || newPassword === '') { + return { error: 'invalid-password' }; + } + + let hashed = hashPassword(newPassword); + let token = uuid.v4(); + // This query does nothing if password authentication was disabled during + // bootstrap (then no row with method=password exists). Maybe we should + // return an error here if that is the case? + accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [ + hashed, + ]); + accountDb.mutate('UPDATE sessions SET token = ?', [token]); + return {}; +} diff --git a/src/app-account.js b/src/app-account.js index 594317dd8..aa7402fad 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -4,12 +4,13 @@ import validateUser from './util/validate-user.js'; import { bootstrap, listLoginMethods, - loginWithPassword, + needsBootstrap, +} from './accounts/index.js'; +import { loginWithPassword, changePassword } from './accounts/password.js'; +import { loginWithOpenIdSetup, loginWithOpenIdFinalize, - changePassword, - needsBootstrap, -} from './account-db.js'; +} from './accounts/openid.js'; let app = express(); app.use(errorMiddleware); diff --git a/src/app-sync.js b/src/app-sync.js index b95e8262f..d4623655e 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -4,7 +4,7 @@ import express from 'express'; import * as uuid from 'uuid'; import validateUser from './util/validate-user.js'; import errorMiddleware from './util/error-middleware.js'; -import getAccountDb from './account-db.js'; +import getAccountDb from './accounts/index.js'; import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; import * as simpleSync from './sync-simple.js'; diff --git a/src/app-sync.test.js b/src/app-sync.test.js index c8d5e22f9..0aa7f2d73 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -1,7 +1,7 @@ import fs from 'node:fs'; import request from 'supertest'; import { handlers as app } from './app-sync.js'; -import getAccountDb from './account-db.js'; +import getAccountDb from '../accounts/index.js'; import { getPathForUserFile } from './util/paths.js'; describe('/download-user-file', () => { diff --git a/src/scripts/reset-password.js b/src/scripts/reset-password.js index c1aff8943..a9faa2363 100644 --- a/src/scripts/reset-password.js +++ b/src/scripts/reset-password.js @@ -1,4 +1,5 @@ -import { needsBootstrap, bootstrap, changePassword } from '../account-db.js'; +import { needsBootstrap, bootstrap } from '../accounts/index.js'; +import { changePassword } from '../accounts/password.js'; import { promptPassword } from '../util/prompt.js'; if (needsBootstrap()) { diff --git a/src/services/secrets-service.js b/src/services/secrets-service.js index b5177fad8..2e632a75f 100644 --- a/src/services/secrets-service.js +++ b/src/services/secrets-service.js @@ -2,7 +2,7 @@ import createDebug from 'debug'; import fs from 'node:fs'; import config, { sqlDir } from '../load-config.js'; import { join } from 'node:path'; -import getAccountDb from '../account-db.js'; +import getAccountDb from '../accounts/index.js'; /** * An enum of valid secret names. diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 9cb319563..140339b57 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -1,4 +1,4 @@ -import { getSession } from '../account-db.js'; +import { getSession } from '../accounts/index.js'; /** * @param {import('express').Request} req From 579115c211c7348e97688d0ece04ccf8831fe259 Mon Sep 17 00:00:00 2001 From: apilat Date: Sun, 25 Jun 2023 20:35:22 +0100 Subject: [PATCH 005/139] Migrate old database version --- src/accounts/index.js | 58 ++++++++++++++++--- .../20000000_old_schema.sql} | 9 +-- src/sql/migrations/20230625_extend_auth.sql | 14 +++++ 3 files changed, 66 insertions(+), 15 deletions(-) rename src/sql/{account.sql => migrations/20000000_old_schema.sql} (61%) create mode 100644 src/sql/migrations/20230625_extend_auth.sql diff --git a/src/accounts/index.js b/src/accounts/index.js index e36a69502..fd262e821 100644 --- a/src/accounts/index.js +++ b/src/accounts/index.js @@ -19,20 +19,64 @@ export default function getAccountDb() { } let dbPath = join(config.serverFiles, 'account.sqlite'); - let needsInit = !fs.existsSync(dbPath); _accountDb = openDatabase(dbPath); + _accountDb.transaction(() => runMigrations(_accountDb)); + } + + return _accountDb; +} - if (needsInit) { - debug(`initializing account database: '${dbPath}'`); - let initSql = fs.readFileSync(join(sqlDir, 'account.sql'), 'utf8'); - _accountDb.exec(initSql); +function runMigrations(db) { + const migrationsDir = join(sqlDir, 'migrations'); + let migrations = fs.readdirSync(migrationsDir); + migrations.sort(); + + // Detection due to https://stackoverflow.com/a/1604121 + const tableExistsMigrations = !!db.first( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='migrations'", + ); + const tableExistsAuth = !!db.first( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='auth'", + ); + + if (!tableExistsMigrations) { + // If the definition of the migrations table ever changes we might have + // difficulty properly applying this change, but this is unlikely. + db.mutate('CREATE TABLE migrations (id TEXT PRIMARY KEY)'); + if (tableExistsAuth) { + // Original version of the database before migrations were introduced, + // register the fact that the old schema already exists. + db.mutate("INSERT INTO migrations VALUES ('20000000_old_schema.sql')"); + } + } + + let firstUnapplied = null; + for (var i = 0; i < migrations.length; i++) { + const applied = !db.first('SELECT 1 FROM migrations WHERE id = ?', [ + migrations[i], + ]); + + if (applied) { + if (firstUnapplied === null) { + firstUnapplied = i; + } } else { - debug(`opening account database: '${dbPath}'`); + if (firstUnapplied !== null) { + throw new Error('out-of-sync migrations'); + } } } - return _accountDb; + if (firstUnapplied !== null) { + for (var i = firstUnapplied; i < migrations.length; i++) { + const migrationSql = fs.readFileSync(join(migrationsDir, migrations[i]), { + encoding: 'utf8', + }); + db.exec(migrationSql); + db.mutate('INSERT INTO migrations (id) VALUES (?)', [migrations[i]]); + } + } } export function needsBootstrap() { diff --git a/src/sql/account.sql b/src/sql/migrations/20000000_old_schema.sql similarity index 61% rename from src/sql/account.sql rename to src/sql/migrations/20000000_old_schema.sql index 70aafec4e..f8a184759 100644 --- a/src/sql/account.sql +++ b/src/sql/migrations/20000000_old_schema.sql @@ -1,16 +1,9 @@ CREATE TABLE auth - (method TEXT PRIMARY KEY, - extra_data TEXT); + (password TEXT PRIMARY KEY); CREATE TABLE sessions (token TEXT PRIMARY KEY); -CREATE TABLE pending_openid_requests - (state TEXT PRIMARY KEY, - code_verifier TEXT, - return_url TEXT, - expiry_time INTEGER); - CREATE TABLE files (id TEXT PRIMARY KEY, group_id TEXT, diff --git a/src/sql/migrations/20230625_extend_auth.sql b/src/sql/migrations/20230625_extend_auth.sql new file mode 100644 index 000000000..1be5e8be8 --- /dev/null +++ b/src/sql/migrations/20230625_extend_auth.sql @@ -0,0 +1,14 @@ +CREATE TABLE auth_new + (method TEXT PRIMARY KEY, + extra_data TEXT); + +INSERT INTO auth_new (method, extra_data) + SELECT 'password', password FROM auth; +DROP TABLE auth; +ALTER TABLE auth_new RENAME TO auth; + +CREATE TABLE pending_openid_requests + (state TEXT PRIMARY KEY, + code_verifier TEXT, + return_url TEXT, + expiry_time INTEGER); From 78b0df709134455ec5d23832dae06ac2d7d8f80f Mon Sep 17 00:00:00 2001 From: apilat Date: Tue, 27 Jun 2023 19:24:34 +0100 Subject: [PATCH 006/139] Fix lint errors --- jest.setup.js | 2 +- src/accounts/index.js | 14 ++++++++--- src/accounts/openid.js | 55 +++++++++++++++++++++--------------------- src/app-sync.test.js | 2 +- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/jest.setup.js b/jest.setup.js index 18208ceb8..63042aa42 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import getAccountDb from './src/account-db.js'; +import getAccountDb from './src/accounts/index.js'; import config from './src/load-config.js'; // Delete previous test database (force creation of a new one) diff --git a/src/accounts/index.js b/src/accounts/index.js index fd262e821..bc8d37327 100644 --- a/src/accounts/index.js +++ b/src/accounts/index.js @@ -52,7 +52,7 @@ function runMigrations(db) { } let firstUnapplied = null; - for (var i = 0; i < migrations.length; i++) { + for (let i = 0; i < migrations.length; i++) { const applied = !db.first('SELECT 1 FROM migrations WHERE id = ?', [ migrations[i], ]); @@ -69,7 +69,7 @@ function runMigrations(db) { } if (firstUnapplied !== null) { - for (var i = firstUnapplied; i < migrations.length; i++) { + for (let i = firstUnapplied; i < migrations.length; i++) { const migrationSql = fs.readFileSync(join(migrationsDir, migrations[i]), { encoding: 'utf8', }); @@ -101,8 +101,14 @@ export function bootstrap(loginSettings) { return { error: 'already-bootstrapped' }; } - const passEnabled = loginSettings.hasOwnProperty('password'); - const openIdEnabled = loginSettings.hasOwnProperty('openid'); + const passEnabled = Object.prototype.hasOwnProperty.call( + loginSettings, + 'password', + ); + const openIdEnabled = Object.prototype.hasOwnProperty.call( + loginSettings, + 'openid', + ); if (!passEnabled && !openIdEnabled) { return { error: 'no-auth-method-selected' }; diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 202436022..ce7d1d534 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -1,33 +1,33 @@ -import getAccountDb from "./index.js"; +import getAccountDb from './index.js'; import { generators, Issuer } from 'openid-client'; export function bootstrapOpenId(config) { - if (!config.hasOwnProperty('issuer')) { - return { error: 'missing-issuer' }; - } - if (!config.hasOwnProperty('client_id')) { - return { error: 'missing-client-id' }; - } - if (!config.hasOwnProperty('client_secret')) { - return { error: 'missing-client-secret' }; - } - if (!config.hasOwnProperty('server_hostname')) { - return { error: 'missing-server-hostname' }; - } - - // Beyond verifying that the configuration exists, we do not attempt - // to check if the configuration is actually correct. - // If the user improperly configures this during bootstrap, there is - // no way to recover without manually editing the database. However, - // this might not be a real issue since an analogous situation happens - // if they forget their password. - let accountDb = getAccountDb(); - accountDb.mutate( - "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", - [JSON.stringify(config)], - ); - - return {}; + if (!Object.prototype.hasOwnProperty.call(config, 'issuer')) { + return { error: 'missing-issuer' }; + } + if (!Object.prototype.hasOwnProperty.call(config, 'client_id')) { + return { error: 'missing-client-id' }; + } + if (!Object.prototype.hasOwnProperty.call(config, 'client_secret')) { + return { error: 'missing-client-secret' }; + } + if (!Object.prototype.hasOwnProperty.call(config, 'server_hostname')) { + return { error: 'missing-server-hostname' }; + } + + // Beyond verifying that the configuration exists, we do not attempt + // to check if the configuration is actually correct. + // If the user improperly configures this during bootstrap, there is + // no way to recover without manually editing the database. However, + // this might not be a real issue since an analogous situation happens + // if they forget their password. + let accountDb = getAccountDb(); + accountDb.mutate( + "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", + [JSON.stringify(config)], + ); + + return {}; } async function setupOpenIdClient(config) { @@ -139,4 +139,3 @@ export async function loginWithOpenIdFinalize(body) { let { token } = accountDb.first('SELECT token FROM sessions'); return { url: `${return_url}/login/openid-cb?token=${token}` }; } - diff --git a/src/app-sync.test.js b/src/app-sync.test.js index 0aa7f2d73..2056e5136 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -1,7 +1,7 @@ import fs from 'node:fs'; import request from 'supertest'; import { handlers as app } from './app-sync.js'; -import getAccountDb from '../accounts/index.js'; +import getAccountDb from './accounts/index.js'; import { getPathForUserFile } from './util/paths.js'; describe('/download-user-file', () => { From 26bc94cd8155994516fb5ed7936d1596c5be1db2 Mon Sep 17 00:00:00 2001 From: apilat Date: Tue, 27 Jun 2023 19:27:50 +0100 Subject: [PATCH 007/139] Add release note --- upcoming-release-notes/219.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/219.md diff --git a/upcoming-release-notes/219.md b/upcoming-release-notes/219.md new file mode 100644 index 000000000..b68a7019b --- /dev/null +++ b/upcoming-release-notes/219.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [apilat] +--- + +Add support for authentication using OpenID Connect. From abfe693b375cee0e9b60c2b720a7b0e71e62780c Mon Sep 17 00:00:00 2001 From: apilat Date: Sat, 1 Jul 2023 16:49:28 +0100 Subject: [PATCH 008/139] Add unit tests for runMigrations --- src/accounts/index.js | 5 +- src/accounts/migration.test.js | 114 ++++++++++++++++++ .../migrations/20000000_old_schema.sql | 0 .../migrations/20230625_extend_auth.sql | 0 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/accounts/migration.test.js rename src/sql/{ => account}/migrations/20000000_old_schema.sql (100%) rename src/sql/{ => account}/migrations/20230625_extend_auth.sql (100%) diff --git a/src/accounts/index.js b/src/accounts/index.js index bc8d37327..cb2271408 100644 --- a/src/accounts/index.js +++ b/src/accounts/index.js @@ -27,8 +27,9 @@ export default function getAccountDb() { return _accountDb; } -function runMigrations(db) { - const migrationsDir = join(sqlDir, 'migrations'); +// This function is not for public usage but needs to be exported to support unit tests. +export function runMigrations(db) { + const migrationsDir = join(sqlDir, 'account', 'migrations'); let migrations = fs.readdirSync(migrationsDir); migrations.sort(); diff --git a/src/accounts/migration.test.js b/src/accounts/migration.test.js new file mode 100644 index 000000000..f968ba2b3 --- /dev/null +++ b/src/accounts/migration.test.js @@ -0,0 +1,114 @@ +import fs from 'fs'; +import openDatabase from '../db.js'; +import { runMigrations } from './index.js'; + +function expectCorrectSchema(db) { + expect( + db.all( + "select tbl_name, sql from sqlite_master where type = 'table' order by tbl_name", + ), + ).toEqual([ + { + tbl_name: 'auth', + sql: 'CREATE TABLE "auth"\n (method TEXT PRIMARY KEY,\n extra_data TEXT)', + }, + { + tbl_name: 'files', + sql: 'CREATE TABLE files\n (id TEXT PRIMARY KEY,\n group_id TEXT,\n sync_version SMALLINT,\n encrypt_meta TEXT,\n encrypt_keyid TEXT,\n encrypt_salt TEXT,\n encrypt_test TEXT,\n deleted BOOLEAN DEFAULT FALSE,\n name TEXT)', + }, + { + tbl_name: 'migrations', + sql: 'CREATE TABLE migrations (id TEXT PRIMARY KEY)', + }, + { + tbl_name: 'pending_openid_requests', + sql: 'CREATE TABLE pending_openid_requests\n (state TEXT PRIMARY KEY,\n code_verifier TEXT,\n return_url TEXT,\n expiry_time INTEGER)', + }, + { + tbl_name: 'sessions', + sql: 'CREATE TABLE sessions\n (token TEXT PRIMARY KEY)', + }, + ]); +} + +function expectAllMigrations(db) { + expect(db.all('select id from migrations order by id')).toEqual([ + { id: '20000000_old_schema.sql' }, + { id: '20230625_extend_auth.sql' }, + ]); +} + +describe('database migration', () => { + it('works from fresh install', () => { + let db = openDatabase(':memory:'); + runMigrations(db); + expectCorrectSchema(db); + expectAllMigrations(db); + }); + + it('works if already migrated', () => { + const databaseFile = '/tmp/test_migration_1.sqlite'; + + { + let db = openDatabase(databaseFile); + runMigrations(db); + db.close(); + } + + { + let db = openDatabase(databaseFile); + expectCorrectSchema(db); + expectAllMigrations(db); + } + + fs.unlinkSync(databaseFile); + }); + + it('works from old schema', () => { + // Contents extracted with `sqlite3 db .dump` + // from fresh database created on commit debb33a63 with password `test` + let db = openDatabase(':memory:'); + db.exec(`PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE auth + (password TEXT PRIMARY KEY); +INSERT INTO auth VALUES('$2b$12$Vm4a2d7ClRbxsYeWBS291eBLs4cBYV0zwyWwamdSaaXpFxSTjWSXq'); +CREATE TABLE sessions + (token TEXT PRIMARY KEY); +INSERT INTO sessions VALUES('a4e2e791-b941-4963-be90-1a24b06c21b3'); +CREATE TABLE files + (id TEXT PRIMARY KEY, + group_id TEXT, + sync_version SMALLINT, + encrypt_meta TEXT, + encrypt_keyid TEXT, + encrypt_salt TEXT, + encrypt_test TEXT, + deleted BOOLEAN DEFAULT FALSE, + name TEXT); +CREATE TABLE secrets ( + name TEXT PRIMARY KEY, + value BLOB +); +COMMIT; +`); + + runMigrations(db); + expectCorrectSchema(db); + expectAllMigrations(db); + + expect(db.all('select * from auth')).toEqual([ + { + method: 'password', + extra_data: + '$2b$12$Vm4a2d7ClRbxsYeWBS291eBLs4cBYV0zwyWwamdSaaXpFxSTjWSXq', + }, + ]); + + expect(db.all('select * from sessions')).toEqual([ + { + token: 'a4e2e791-b941-4963-be90-1a24b06c21b3', + }, + ]); + }); +}); diff --git a/src/sql/migrations/20000000_old_schema.sql b/src/sql/account/migrations/20000000_old_schema.sql similarity index 100% rename from src/sql/migrations/20000000_old_schema.sql rename to src/sql/account/migrations/20000000_old_schema.sql diff --git a/src/sql/migrations/20230625_extend_auth.sql b/src/sql/account/migrations/20230625_extend_auth.sql similarity index 100% rename from src/sql/migrations/20230625_extend_auth.sql rename to src/sql/account/migrations/20230625_extend_auth.sql From efc07b880acb49517b0143269d04fc6bcd5d3068 Mon Sep 17 00:00:00 2001 From: apilat Date: Sat, 1 Jul 2023 17:02:27 +0100 Subject: [PATCH 009/139] Integrate secrets database into migration system --- src/accounts/migration.test.js | 5 +++++ src/services/secrets-service.js | 5 ----- .../migrations/20230701_integrate_secretsdb.sql} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename src/sql/{secrets.sql => account/migrations/20230701_integrate_secretsdb.sql} (100%) diff --git a/src/accounts/migration.test.js b/src/accounts/migration.test.js index f968ba2b3..f5021235c 100644 --- a/src/accounts/migration.test.js +++ b/src/accounts/migration.test.js @@ -24,6 +24,10 @@ function expectCorrectSchema(db) { tbl_name: 'pending_openid_requests', sql: 'CREATE TABLE pending_openid_requests\n (state TEXT PRIMARY KEY,\n code_verifier TEXT,\n return_url TEXT,\n expiry_time INTEGER)', }, + { + tbl_name: 'secrets', + sql: 'CREATE TABLE secrets (\n name TEXT PRIMARY KEY,\n value BLOB\n)', + }, { tbl_name: 'sessions', sql: 'CREATE TABLE sessions\n (token TEXT PRIMARY KEY)', @@ -35,6 +39,7 @@ function expectAllMigrations(db) { expect(db.all('select id from migrations order by id')).toEqual([ { id: '20000000_old_schema.sql' }, { id: '20230625_extend_auth.sql' }, + { id: '20230701_integrate_secretsdb.sql' }, ]); } diff --git a/src/services/secrets-service.js b/src/services/secrets-service.js index 2e632a75f..66e478c58 100644 --- a/src/services/secrets-service.js +++ b/src/services/secrets-service.js @@ -41,11 +41,6 @@ class SecretsDb { if (!this.db) { this.db = this.open(); } - - this.debug(`initializing secrets table'`); - //Create secret table if it doesn't exist - const initSql = fs.readFileSync(join(sqlDir, 'secrets.sql'), 'utf8'); - this.db.exec(initSql); } open() { diff --git a/src/sql/secrets.sql b/src/sql/account/migrations/20230701_integrate_secretsdb.sql similarity index 100% rename from src/sql/secrets.sql rename to src/sql/account/migrations/20230701_integrate_secretsdb.sql From ce150974904d5541842e5fea77a808915a258c00 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 8 Jul 2023 14:05:07 -0400 Subject: [PATCH 010/139] Remove unused imports --- src/services/secrets-service.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/secrets-service.js b/src/services/secrets-service.js index 66e478c58..131c56d37 100644 --- a/src/services/secrets-service.js +++ b/src/services/secrets-service.js @@ -1,7 +1,5 @@ import createDebug from 'debug'; -import fs from 'node:fs'; -import config, { sqlDir } from '../load-config.js'; -import { join } from 'node:path'; +import config from '../load-config.js'; import getAccountDb from '../accounts/index.js'; /** From b0a211115b2505d8578259dd75876110c152326d Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 17 Jun 2024 16:12:54 -0300 Subject: [PATCH 011/139] trying local package --- package.json | 2 +- yarn.lock | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 04924440e..a0169b569 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@actual-app/crdt": "2.1.0", - "@actual-app/web": "24.6.0", + "@actual-app/web": "link:../actualextended/packages/desktop-client/", "bcrypt": "^5.1.0", "better-sqlite3": "^9.6.0", "body-parser": "^1.20.1", diff --git a/yarn.lock b/yarn.lock index 8032f2e8a..1c0b9bf32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,12 +16,11 @@ __metadata: languageName: node linkType: hard -"@actual-app/web@npm:24.6.0": - version: 24.6.0 - resolution: "@actual-app/web@npm:24.6.0" - checksum: a31bf7bfb054287e0ca8f192d04e8aa4ab66dae0815fea8ba97e724b15602180112ae52d267e56362f4cf2640aaca16d475e799764c2b70278f5a62da35eeb54 +"@actual-app/web@link:../actual_dev/packages/desktop-client/::locator=actual-sync%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@actual-app/web@link:../actual_dev/packages/desktop-client/::locator=actual-sync%40workspace%3A." languageName: node - linkType: hard + linkType: soft "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 @@ -1436,7 +1435,7 @@ __metadata: resolution: "actual-sync@workspace:." dependencies: "@actual-app/crdt": "npm:2.1.0" - "@actual-app/web": "npm:24.6.0" + "@actual-app/web": "link:../actual_dev/packages/desktop-client/" "@babel/preset-typescript": "npm:^7.20.2" "@types/bcrypt": "npm:^5.0.0" "@types/better-sqlite3": "npm:^7.6.7" From 1bced80951cee5c29fafb7124c7a3cdc3cc97e0d Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 20 Jun 2024 09:28:40 -0300 Subject: [PATCH 012/139] merge --- .migrate | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .migrate diff --git a/.migrate b/.migrate new file mode 100644 index 000000000..55c420ef4 --- /dev/null +++ b/.migrate @@ -0,0 +1,21 @@ +{ + "lastRun": "1702667624000-rename-nordigen-secrets.js", + "migrations": [ + { + "title": "1694360000000-create-folders.js", + "timestamp": 1718651296479 + }, + { + "title": "1694360479680-create-account-db.js", + "timestamp": 1718651296520 + }, + { + "title": "1694362247011-create-secret-table.js", + "timestamp": 1718651296526 + }, + { + "title": "1702667624000-rename-nordigen-secrets.js", + "timestamp": 1718651296527 + } + ] +} \ No newline at end of file From 4f261db6c964aac532f45c7ef7a6b2a0661d9ba7 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 26 Jun 2024 15:23:14 -0300 Subject: [PATCH 013/139] working example --- jest.setup.js | 2 +- migrations/1718889148000-openid.js | 29 +++++ migrations/1719409568000-multiuser.js | 42 +++++++ package.json | 2 +- src/account-db.js | 77 ++++++++++--- src/accounts/index.js | 151 -------------------------- src/accounts/migration.test.js | 119 -------------------- src/accounts/openid.js | 69 ++++++++++-- src/accounts/password.js | 2 +- src/app-account.js | 38 +++---- src/app-sync.js | 2 +- src/app-sync.test.js | 2 +- src/scripts/reset-password.js | 2 +- src/util/validate-user.js | 4 +- 14 files changed, 213 insertions(+), 328 deletions(-) create mode 100644 migrations/1718889148000-openid.js create mode 100644 migrations/1719409568000-multiuser.js delete mode 100644 src/accounts/index.js delete mode 100644 src/accounts/migration.test.js diff --git a/jest.setup.js b/jest.setup.js index 63042aa42..b2583a9c6 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import getAccountDb from './src/accounts/index.js'; import config from './src/load-config.js'; +import getAccountDb from './src/account-db.js'; // Delete previous test database (force creation of a new one) const dbPath = path.join(config.serverFiles, 'account.sqlite'); diff --git a/migrations/1718889148000-openid.js b/migrations/1718889148000-openid.js new file mode 100644 index 000000000..5dacb72a0 --- /dev/null +++ b/migrations/1718889148000-openid.js @@ -0,0 +1,29 @@ +import getAccountDb from '../src/account-db.js'; + +export const up = async function () { + await getAccountDb().exec( + `CREATE TABLE auth_new + (method TEXT PRIMARY KEY, + extra_data TEXT, active INTEGER); + + INSERT INTO auth_new (method, extra_data, active) + SELECT 'password', password, 1 FROM auth; + DROP TABLE auth; + ALTER TABLE auth_new RENAME TO auth; + + CREATE TABLE pending_openid_requests + (state TEXT PRIMARY KEY, + code_verifier TEXT, + return_url TEXT, + expiry_time INTEGER);`, + ); +}; + +export const down = async function () { + await getAccountDb().exec( + ` + DROP TABLE auth; + DROP TABLE pending_openid_requests; + `, + ); +}; diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js new file mode 100644 index 000000000..06e9f20b5 --- /dev/null +++ b/migrations/1719409568000-multiuser.js @@ -0,0 +1,42 @@ +import getAccountDb from '../src/account-db.js'; + +export const up = async function () { + await getAccountDb().exec( + `CREATE TABLE users + (user_id TEXT PRIMARY KEY, + email TEXT, + enabled INTEGER, + master INTEGER); + + CREATE TABLE roles + (role_id TEXT PRIMARY KEY, + name TEXT); + + INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'Admin'); + INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', 'Basic'); + + CREATE TABLE user_roles + (user_id TEXT, + role_id TEXT); + + + DELETE FROM sessions; + + ALTER TABLE sessions + ADD COLUMN expires_in INTEGER; + + ALTER TABLE sessions + ADD user_id TEXT; + `, + ); +}; + +export const down = async function () { + await getAccountDb().exec( + ` + DROP TABLE user; + DROP TABLE roles; + DROP TABLE user_roles; + `, + ); +}; diff --git a/package.json b/package.json index 9b7ebddaa..cf5191c7f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@actual-app/crdt": "2.1.0", - "@actual-app/web": "link:../actualextended/packages/desktop-client/", + "@actual-app/web": "link:../actual_dev/packages/desktop-client/", "bcrypt": "^5.1.0", "better-sqlite3": "^9.6.0", "body-parser": "^1.20.1", diff --git a/src/account-db.js b/src/account-db.js index cc2fe5674..8b9140997 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -3,6 +3,8 @@ import openDatabase from './db.js'; import config from './load-config.js'; import * as uuid from 'uuid'; import * as bcrypt from 'bcrypt'; +import { bootstrapPassword } from './accounts/password.js'; +import { bootstrapOpenId } from './accounts/openid.js'; let _accountDb; @@ -25,6 +27,12 @@ export function needsBootstrap() { return rows.length === 0; } +export function listLoginMethods() { + let accountDb = getAccountDb(); + let rows = accountDb.all('SELECT method FROM auth'); + return rows.map((r) => r['method']); +} + /* * Get the Login Method in the following order * req (the frontend can say which method in the case it wants to resort to forcing password auth) @@ -38,31 +46,70 @@ export function getLoginMethod(req) { ) { return req.body.loginMethod; } - return config.loginMethod || 'password'; -} + let accountDb = getAccountDb(); + let row = accountDb.first('SELECT method FROM auth where active = 1'); -export function bootstrap(password) { - if (password === undefined || password === '') { - return { error: 'invalid-password' }; + if (row !== null && row['method'] !== null) { + return row['method']; } + return config.loginMethod || 'password'; +} + +// Supported login settings: +// "password": "secret_password", +// "openid": { +// "issuer": "https://example.org", +// "client_id": "your_client_id", +// "client_secret": "your_client_secret", +// "server_hostname": "https://actual.your_website.com" +// } +export function bootstrap(loginSettings) { let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT * FROM auth'); + // TODO We should use a transaction here to make bootstrap atomic - if (rows.length !== 0) { + if (!needsBootstrap()) { return { error: 'already-bootstrapped' }; } - // Hash the password. There's really not a strong need for this - // since this is a self-hosted instance owned by the user. - // However, just in case we do it. - let hashed = hashPassword(password); - accountDb.mutate('INSERT INTO auth (password) VALUES (?)', [hashed]); + const passEnabled = Object.prototype.hasOwnProperty.call( + loginSettings, + 'password', + ); + const openIdEnabled = Object.prototype.hasOwnProperty.call( + loginSettings, + 'openid', + ); + + if (!passEnabled && !openIdEnabled) { + return { error: 'no-auth-method-selected' }; + } - let token = uuid.v4(); - accountDb.mutate('INSERT INTO sessions (token) VALUES (?)', [token]); + if (passEnabled && openIdEnabled) { + return { error: 'max-one-method-allowed' }; + } + + if (passEnabled) { + let { error } = bootstrapPassword(loginSettings.password); + if (error) { + return { error }; + } + } + + if (openIdEnabled) { + let { error } = bootstrapOpenId(loginSettings.openid); + if (error) { + return { error }; + } + } - return { token }; + const token = uuid.v4(); + accountDb.mutate( + 'INSERT INTO sessions (token, expires_in, user_id) VALUES (?, -1, ?)', + [token, ''], + ); + + return {}; } export function login(password) { diff --git a/src/accounts/index.js b/src/accounts/index.js deleted file mode 100644 index cb2271408..000000000 --- a/src/accounts/index.js +++ /dev/null @@ -1,151 +0,0 @@ -import fs from 'node:fs'; -import { join } from 'node:path'; -import openDatabase from '../db.js'; -import config, { sqlDir } from '../load-config.js'; -import createDebug from 'debug'; -import * as uuid from 'uuid'; -import { bootstrapPassword } from './password.js'; -import { bootstrapOpenId } from './openid.js'; - -const debug = createDebug('actual:account-db'); - -let _accountDb = null; - -export default function getAccountDb() { - if (_accountDb == null) { - if (!fs.existsSync(config.serverFiles)) { - debug(`creating server files directory: '${config.serverFiles}'`); - fs.mkdirSync(config.serverFiles); - } - - let dbPath = join(config.serverFiles, 'account.sqlite'); - - _accountDb = openDatabase(dbPath); - _accountDb.transaction(() => runMigrations(_accountDb)); - } - - return _accountDb; -} - -// This function is not for public usage but needs to be exported to support unit tests. -export function runMigrations(db) { - const migrationsDir = join(sqlDir, 'account', 'migrations'); - let migrations = fs.readdirSync(migrationsDir); - migrations.sort(); - - // Detection due to https://stackoverflow.com/a/1604121 - const tableExistsMigrations = !!db.first( - "SELECT 1 FROM sqlite_master WHERE type='table' AND name='migrations'", - ); - const tableExistsAuth = !!db.first( - "SELECT 1 FROM sqlite_master WHERE type='table' AND name='auth'", - ); - - if (!tableExistsMigrations) { - // If the definition of the migrations table ever changes we might have - // difficulty properly applying this change, but this is unlikely. - db.mutate('CREATE TABLE migrations (id TEXT PRIMARY KEY)'); - if (tableExistsAuth) { - // Original version of the database before migrations were introduced, - // register the fact that the old schema already exists. - db.mutate("INSERT INTO migrations VALUES ('20000000_old_schema.sql')"); - } - } - - let firstUnapplied = null; - for (let i = 0; i < migrations.length; i++) { - const applied = !db.first('SELECT 1 FROM migrations WHERE id = ?', [ - migrations[i], - ]); - - if (applied) { - if (firstUnapplied === null) { - firstUnapplied = i; - } - } else { - if (firstUnapplied !== null) { - throw new Error('out-of-sync migrations'); - } - } - } - - if (firstUnapplied !== null) { - for (let i = firstUnapplied; i < migrations.length; i++) { - const migrationSql = fs.readFileSync(join(migrationsDir, migrations[i]), { - encoding: 'utf8', - }); - db.exec(migrationSql); - db.mutate('INSERT INTO migrations (id) VALUES (?)', [migrations[i]]); - } - } -} - -export function needsBootstrap() { - let accountDb = getAccountDb(); - let row = accountDb.first('SELECT count(*) FROM auth'); - return row['count(*)'] === 0; -} - -// Supported login settings: -// "password": "secret_password", -// "openid": { -// "issuer": "https://example.org", -// "client_id": "your_client_id", -// "client_secret": "your_client_secret", -// "server_hostname": "https://actual.your_website.com" -// } -export function bootstrap(loginSettings) { - let accountDb = getAccountDb(); - // TODO We should use a transaction here to make bootstrap atomic - - if (!needsBootstrap()) { - return { error: 'already-bootstrapped' }; - } - - const passEnabled = Object.prototype.hasOwnProperty.call( - loginSettings, - 'password', - ); - const openIdEnabled = Object.prototype.hasOwnProperty.call( - loginSettings, - 'openid', - ); - - if (!passEnabled && !openIdEnabled) { - return { error: 'no-auth-method-selected' }; - } - - if (passEnabled && openIdEnabled) { - return { error: 'max-one-method-allowed' }; - } - - if (passEnabled) { - let { error } = bootstrapPassword(loginSettings.password); - if (error) { - return { error }; - } - } - - if (openIdEnabled) { - let { error } = bootstrapOpenId(loginSettings.openid); - if (error) { - return { error }; - } - } - - const token = uuid.v4(); - accountDb.mutate('INSERT INTO sessions (token) VALUES (?)', [token]); - - return {}; -} - -export function getSession(token) { - let accountDb = getAccountDb(); - return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); -} - -export function listLoginMethods() { - let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT method FROM auth'); - return rows.map((r) => r['method']); -} diff --git a/src/accounts/migration.test.js b/src/accounts/migration.test.js deleted file mode 100644 index f5021235c..000000000 --- a/src/accounts/migration.test.js +++ /dev/null @@ -1,119 +0,0 @@ -import fs from 'fs'; -import openDatabase from '../db.js'; -import { runMigrations } from './index.js'; - -function expectCorrectSchema(db) { - expect( - db.all( - "select tbl_name, sql from sqlite_master where type = 'table' order by tbl_name", - ), - ).toEqual([ - { - tbl_name: 'auth', - sql: 'CREATE TABLE "auth"\n (method TEXT PRIMARY KEY,\n extra_data TEXT)', - }, - { - tbl_name: 'files', - sql: 'CREATE TABLE files\n (id TEXT PRIMARY KEY,\n group_id TEXT,\n sync_version SMALLINT,\n encrypt_meta TEXT,\n encrypt_keyid TEXT,\n encrypt_salt TEXT,\n encrypt_test TEXT,\n deleted BOOLEAN DEFAULT FALSE,\n name TEXT)', - }, - { - tbl_name: 'migrations', - sql: 'CREATE TABLE migrations (id TEXT PRIMARY KEY)', - }, - { - tbl_name: 'pending_openid_requests', - sql: 'CREATE TABLE pending_openid_requests\n (state TEXT PRIMARY KEY,\n code_verifier TEXT,\n return_url TEXT,\n expiry_time INTEGER)', - }, - { - tbl_name: 'secrets', - sql: 'CREATE TABLE secrets (\n name TEXT PRIMARY KEY,\n value BLOB\n)', - }, - { - tbl_name: 'sessions', - sql: 'CREATE TABLE sessions\n (token TEXT PRIMARY KEY)', - }, - ]); -} - -function expectAllMigrations(db) { - expect(db.all('select id from migrations order by id')).toEqual([ - { id: '20000000_old_schema.sql' }, - { id: '20230625_extend_auth.sql' }, - { id: '20230701_integrate_secretsdb.sql' }, - ]); -} - -describe('database migration', () => { - it('works from fresh install', () => { - let db = openDatabase(':memory:'); - runMigrations(db); - expectCorrectSchema(db); - expectAllMigrations(db); - }); - - it('works if already migrated', () => { - const databaseFile = '/tmp/test_migration_1.sqlite'; - - { - let db = openDatabase(databaseFile); - runMigrations(db); - db.close(); - } - - { - let db = openDatabase(databaseFile); - expectCorrectSchema(db); - expectAllMigrations(db); - } - - fs.unlinkSync(databaseFile); - }); - - it('works from old schema', () => { - // Contents extracted with `sqlite3 db .dump` - // from fresh database created on commit debb33a63 with password `test` - let db = openDatabase(':memory:'); - db.exec(`PRAGMA foreign_keys=OFF; -BEGIN TRANSACTION; -CREATE TABLE auth - (password TEXT PRIMARY KEY); -INSERT INTO auth VALUES('$2b$12$Vm4a2d7ClRbxsYeWBS291eBLs4cBYV0zwyWwamdSaaXpFxSTjWSXq'); -CREATE TABLE sessions - (token TEXT PRIMARY KEY); -INSERT INTO sessions VALUES('a4e2e791-b941-4963-be90-1a24b06c21b3'); -CREATE TABLE files - (id TEXT PRIMARY KEY, - group_id TEXT, - sync_version SMALLINT, - encrypt_meta TEXT, - encrypt_keyid TEXT, - encrypt_salt TEXT, - encrypt_test TEXT, - deleted BOOLEAN DEFAULT FALSE, - name TEXT); -CREATE TABLE secrets ( - name TEXT PRIMARY KEY, - value BLOB -); -COMMIT; -`); - - runMigrations(db); - expectCorrectSchema(db); - expectAllMigrations(db); - - expect(db.all('select * from auth')).toEqual([ - { - method: 'password', - extra_data: - '$2b$12$Vm4a2d7ClRbxsYeWBS291eBLs4cBYV0zwyWwamdSaaXpFxSTjWSXq', - }, - ]); - - expect(db.all('select * from sessions')).toEqual([ - { - token: 'a4e2e791-b941-4963-be90-1a24b06c21b3', - }, - ]); - }); -}); diff --git a/src/accounts/openid.js b/src/accounts/openid.js index ce7d1d534..23d32b217 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -1,4 +1,5 @@ -import getAccountDb from './index.js'; +import getAccountDb from '../account-db.js'; +import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; export function bootstrapOpenId(config) { @@ -22,8 +23,9 @@ export function bootstrapOpenId(config) { // this might not be a real issue since an analogous situation happens // if they forget their password. let accountDb = getAccountDb(); + accountDb.mutate('UPDATE auth SET active = 0'); accountDb.mutate( - "INSERT INTO auth (method, extra_data) VALUES ('openid', ?)", + "INSERT INTO auth (method, extra_data, active) VALUES ('openid', ?, 1)", [JSON.stringify(config)], ); @@ -81,7 +83,7 @@ export async function loginWithOpenIdSetup(body) { const url = client.authorizationUrl({ response_type: 'code', - scope: 'openid', + scope: 'openid email', state, code_challenge, code_challenge_method: 'S256', @@ -126,16 +128,59 @@ export async function loginWithOpenIdFinalize(body) { code_verifier, redirect_uri: client.redirect_uris[0], }); - await client.userinfo(grant); - // The server requests have succeeded, so the user has been authenticated. - // Ideally, we would create a session token here tied to the returned access token - // and verify it with the server whenever the user connects. - // However, the rest of this server code uses only a single permanent token, - // so that is what we do here as well. + const userInfo = await client.userinfo(grant); + if (userInfo.email == null) { + return { error: 'openid-grant-failed: no email found for the user' }; + } + + let { c } = accountDb.first('SELECT count(*) as C FROM users'); + let userId = null; + if (c === undefined) { + userId = uuid.v4(); + accountDb.mutate( + 'INSERT INTO users (user_id, email, enabled, master) VALUES (?, ?, 1, 1)', + [userId, userInfo.email], + ); + + accountDb.mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], + ); + } else { + let { userId: userIdFromDb } = accountDb.first( + 'SELECT user_id FROM users WHERE email = ?', + userInfo.email, + ); + + if (userIdFromDb == null) { + return { + error: + 'openid-grant-failed: user does not have access to Actual Budget', + }; + } + + userId = userIdFromDb; + } + + const emptySession = accountDb.first( + 'SELECT * FROM sessions WHERE expires_in = -1', + ); + + if (emptySession == null) { + return { + error: 'openid-grant-failed: no empty session found for this user', + }; + } + + accountDb.mutate( + `UPDATE sessions + SET expires_in = ?, user_id = ? + WHERE token = ?`, + [grant.expires_at, userId, emptySession.token], + ); + + return { url: `${return_url}/openid-cb?token=${emptySession.token}` }; } catch (err) { return { error: 'openid-grant-failed: ' + err }; } - - let { token } = accountDb.first('SELECT token FROM sessions'); - return { url: `${return_url}/login/openid-cb?token=${token}` }; } diff --git a/src/accounts/password.js b/src/accounts/password.js index c23450417..d09b89cd5 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -1,6 +1,6 @@ import * as bcrypt from 'bcrypt'; -import getAccountDb from './index.js'; import * as uuid from 'uuid'; +import getAccountDb from '../account-db.js'; function hashPassword(password) { return bcrypt.hashSync(password, 12); diff --git a/src/app-account.js b/src/app-account.js index c5e9cd568..ce0188991 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -3,11 +3,12 @@ import errorMiddleware from './util/error-middleware.js'; import validateUser, { validateAuthHeader } from './util/validate-user.js'; import { bootstrap, - listLoginMethods, needsBootstrap, getLoginMethod, -} from './accounts/index.js'; -import { loginWithPassword, changePassword } from './accounts/password.js'; + listLoginMethods, + login, +} from './account-db.js'; +import { changePassword } from './accounts/password.js'; import { loginWithOpenIdSetup, loginWithOpenIdFinalize, @@ -40,13 +41,6 @@ app.post('/bootstrap', (req, res) => { } else { res.send({ status: 'ok' }); } - - res.send({ status: 'ok', data: { token } }); -}); - -app.get('/login-methods', (req, res) => { - let methods = listLoginMethods(); - res.send({ status: 'ok', methods }); }); app.get('/login-methods', (req, res) => { @@ -54,7 +48,7 @@ app.get('/login-methods', (req, res) => { res.send({ status: 'ok', methods }); }); -app.post('/login', (req, res) => { +app.post('/login', async (req, res) => { let loginMethod = getLoginMethod(req); console.log('Logging in via ' + loginMethod); let tokenRes = null; @@ -75,6 +69,16 @@ app.post('/login', (req, res) => { } break; } + case 'openid': { + let { error, url } = await loginWithOpenIdSetup(req.body); + if (error) { + res.send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok', data: { redirect_url: url } }); + return; + } + case 'password': default: tokenRes = login(req.body.password); @@ -87,21 +91,9 @@ app.post('/login', (req, res) => { return; } - let token = loginWithPassword(req.body.password); res.send({ status: 'ok', data: { token } }); }); -app.post('/login-openid', async (req, res) => { - // req.body needs to contain - // - return_url: address of the actual frontend which we should return to after the openid flow - let { error, url } = await loginWithOpenIdSetup(req.body); - if (error) { - res.send({ status: 'error', reason: error }); - return; - } - res.send({ status: 'ok', data: { redirect_url: url } }); -}); - app.get('/login-openid/cb', async (req, res) => { let { error, url } = await loginWithOpenIdFinalize(req.query); if (error) { diff --git a/src/app-sync.js b/src/app-sync.js index f351790e7..39cddf682 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -4,12 +4,12 @@ import express from 'express'; import * as uuid from 'uuid'; import validateUser from './util/validate-user.js'; import errorMiddleware from './util/error-middleware.js'; -import getAccountDb from './accounts/index.js'; import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; import * as simpleSync from './sync-simple.js'; import { SyncProtoBuf } from '@actual-app/crdt'; +import getAccountDb from './account-db.js'; const app = express(); app.use(errorMiddleware); diff --git a/src/app-sync.test.js b/src/app-sync.test.js index 2056e5136..ef89a79e1 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -1,8 +1,8 @@ import fs from 'node:fs'; import request from 'supertest'; import { handlers as app } from './app-sync.js'; -import getAccountDb from './accounts/index.js'; import { getPathForUserFile } from './util/paths.js'; +import getAccountDb from './account-db.js'; describe('/download-user-file', () => { describe('default version', () => { diff --git a/src/scripts/reset-password.js b/src/scripts/reset-password.js index a9faa2363..5896e9e69 100644 --- a/src/scripts/reset-password.js +++ b/src/scripts/reset-password.js @@ -1,4 +1,4 @@ -import { needsBootstrap, bootstrap } from '../accounts/index.js'; +import { bootstrap, needsBootstrap } from '../account-db.js'; import { changePassword } from '../accounts/password.js'; import { promptPassword } from '../util/prompt.js'; diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 5f8346958..90ec9bf20 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -1,7 +1,7 @@ -import { getSession } from '../accounts/index.js'; import config from '../load-config.js'; import proxyaddr from 'proxy-addr'; import ipaddr from 'ipaddr.js'; +import { getSession } from '../account-db.js'; /** * @param {import('express').Request} req @@ -16,7 +16,7 @@ export default function validateUser(req, res) { let session = getSession(token); - if (!session) { + if (!session || (session.expires_in * 1000) <= Date.now()) { res.status(401); res.send({ status: 'error', From 9c1cfee440c65e3086d207ceacb8d1fbe4fb0122 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 26 Jun 2024 15:23:24 -0300 Subject: [PATCH 014/139] working example --- .migrate | 18 +++++++++++++----- migrations/1719409568000-multiuser.js | 9 +++++---- src/account-db.js | 19 +++++++++++++++++-- src/accounts/openid.js | 23 ++++++----------------- src/app-account.js | 15 +++++++++++---- src/util/validate-user.js | 25 +++++++++++++++++++++++-- 6 files changed, 75 insertions(+), 34 deletions(-) diff --git a/.migrate b/.migrate index 55c420ef4..55f84c86d 100644 --- a/.migrate +++ b/.migrate @@ -1,21 +1,29 @@ { - "lastRun": "1702667624000-rename-nordigen-secrets.js", + "lastRun": "1719409568000-multiuser.js", "migrations": [ { "title": "1694360000000-create-folders.js", - "timestamp": 1718651296479 + "timestamp": 1719422276506 }, { "title": "1694360479680-create-account-db.js", - "timestamp": 1718651296520 + "timestamp": 1719422276546 }, { "title": "1694362247011-create-secret-table.js", - "timestamp": 1718651296526 + "timestamp": 1719422276557 }, { "title": "1702667624000-rename-nordigen-secrets.js", - "timestamp": 1718651296527 + "timestamp": 1719422276558 + }, + { + "title": "1718889148000-openid.js", + "timestamp": 1719422276595 + }, + { + "title": "1719409568000-multiuser.js", + "timestamp": 1719422276674 } ] } \ No newline at end of file diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 06e9f20b5..6985c4689 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -4,16 +4,17 @@ export const up = async function () { await getAccountDb().exec( `CREATE TABLE users (user_id TEXT PRIMARY KEY, - email TEXT, + user_name TEXT, enabled INTEGER, master INTEGER); CREATE TABLE roles (role_id TEXT PRIMARY KEY, + permissions TEXT, name TEXT); - INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'Admin'); - INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', 'Basic'); + INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'CAN_EDIT, CAN_VIEW, CAN_DELETE, SETTINGS','Admin'); + INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', 'CAN_VIEW','Basic'); CREATE TABLE user_roles (user_id TEXT, @@ -23,7 +24,7 @@ export const up = async function () { DELETE FROM sessions; ALTER TABLE sessions - ADD COLUMN expires_in INTEGER; + ADD COLUMN expires_at INTEGER; ALTER TABLE sessions ADD user_id TEXT; diff --git a/src/account-db.js b/src/account-db.js index 8b9140997..74e6775ae 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -66,7 +66,6 @@ export function getLoginMethod(req) { // } export function bootstrap(loginSettings) { let accountDb = getAccountDb(); - // TODO We should use a transaction here to make bootstrap atomic if (!needsBootstrap()) { return { error: 'already-bootstrapped' }; @@ -105,7 +104,7 @@ export function bootstrap(loginSettings) { const token = uuid.v4(); accountDb.mutate( - 'INSERT INTO sessions (token, expires_in, user_id) VALUES (?, -1, ?)', + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, -1, ?)', [token, ''], ); @@ -156,3 +155,19 @@ export function getSession(token) { let accountDb = getAccountDb(); return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); } + +export function getUserInfo(userId) { + let accountDb = getAccountDb(); + return accountDb.first('SELECT * FROM users WHERE user_id = ?', [userId]); +} + +export function getUserPermissions(userId) { + let accountDb = getAccountDb(); + return accountDb.all( + `SELECT roles.permissions FROM users + JOIN user_roles ON user_roles.user_id = users.user_id + JOIN roles ON roles.role_id = user_roles.role_id + WHERE users.user_id = ?`, + [userId], + ); +} diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 23d32b217..508d2ea5a 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -138,7 +138,7 @@ export async function loginWithOpenIdFinalize(body) { if (c === undefined) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (user_id, email, enabled, master) VALUES (?, ?, 1, 1)', + 'INSERT INTO users (user_id, user_name, enabled, master) VALUES (?, ?, 1, 1)', [userId, userInfo.email], ); @@ -148,7 +148,7 @@ export async function loginWithOpenIdFinalize(body) { ); } else { let { userId: userIdFromDb } = accountDb.first( - 'SELECT user_id FROM users WHERE email = ?', + 'SELECT user_id FROM users WHERE user_name = ?', userInfo.email, ); @@ -162,24 +162,13 @@ export async function loginWithOpenIdFinalize(body) { userId = userIdFromDb; } - const emptySession = accountDb.first( - 'SELECT * FROM sessions WHERE expires_in = -1', - ); - - if (emptySession == null) { - return { - error: 'openid-grant-failed: no empty session found for this user', - }; - } - + const token = uuid.v4(); accountDb.mutate( - `UPDATE sessions - SET expires_in = ?, user_id = ? - WHERE token = ?`, - [grant.expires_at, userId, emptySession.token], + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + [token, grant.expires_at, userId], ); - return { url: `${return_url}/openid-cb?token=${emptySession.token}` }; + return { url: `${return_url}/openid-cb?token=${token}` }; } catch (err) { return { error: 'openid-grant-failed: ' + err }; } diff --git a/src/app-account.js b/src/app-account.js index ce0188991..b2737e0f5 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -78,7 +78,7 @@ app.post('/login', async (req, res) => { res.send({ status: 'ok', data: { redirect_url: url } }); return; } - + case 'password': default: tokenRes = login(req.body.password); @@ -119,9 +119,16 @@ app.post('/change-password', (req, res) => { }); app.get('/validate', (req, res) => { - let user = validateUser(req, res); - if (user) { - res.send({ status: 'ok', data: { validated: true } }); + let data = validateUser(req, res); + if (data) { + res.send({ + status: 'ok', + data: { + validated: true, + userName: data?.user?.user_name, + permissions: data?.permissions, + }, + }); } }); diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 90ec9bf20..38af4d62d 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -1,7 +1,7 @@ import config from '../load-config.js'; import proxyaddr from 'proxy-addr'; import ipaddr from 'ipaddr.js'; -import { getSession } from '../account-db.js'; +import { getSession, getUserInfo, getUserPermissions } from '../account-db.js'; /** * @param {import('express').Request} req @@ -16,7 +16,7 @@ export default function validateUser(req, res) { let session = getSession(token); - if (!session || (session.expires_in * 1000) <= Date.now()) { + if (!session || session.expires_at * 1000 <= Date.now()) { res.status(401); res.send({ status: 'error', @@ -26,6 +26,27 @@ export default function validateUser(req, res) { return null; } + if (session.expires_at * 1000 <= Date.now()) { + res.status(401); + res.send({ + status: 'error', + reason: 'unauthorized', + details: 'token-expired', + }); + return null; + } + + session.user = getUserInfo(session.user_id); + let permissions = getUserPermissions(session.user_id); + const uniquePermissions = Array.from( + new Set( + permissions.flatMap((rolePermission) => + rolePermission.permissions.split(',').map((perm) => perm.trim()), + ), + ), + ); + session.permissions = uniquePermissions; + return session; } From 930d0fd4efa9358e832617b5b25cc373e4912c68 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 26 Jun 2024 15:24:15 -0300 Subject: [PATCH 015/139] working example --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cf5191c7f..be598bd1c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@actual-app/crdt": "2.1.0", - "@actual-app/web": "link:../actual_dev/packages/desktop-client/", + "@actual-app/web": "24.6.0", "bcrypt": "^5.1.0", "better-sqlite3": "^9.6.0", "body-parser": "^1.20.1", diff --git a/yarn.lock b/yarn.lock index c0508170e..f123642af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,11 +16,12 @@ __metadata: languageName: node linkType: hard -"@actual-app/web@link:../actual_dev/packages/desktop-client/::locator=actual-sync%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@actual-app/web@link:../actual_dev/packages/desktop-client/::locator=actual-sync%40workspace%3A." +"@actual-app/web@npm:24.6.0": + version: 24.6.0 + resolution: "@actual-app/web@npm:24.6.0" + checksum: a31bf7bfb054287e0ca8f192d04e8aa4ab66dae0815fea8ba97e724b15602180112ae52d267e56362f4cf2640aaca16d475e799764c2b70278f5a62da35eeb54 languageName: node - linkType: soft + linkType: hard "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 @@ -1435,7 +1436,7 @@ __metadata: resolution: "actual-sync@workspace:." dependencies: "@actual-app/crdt": "npm:2.1.0" - "@actual-app/web": "link:../actual_dev/packages/desktop-client/" + "@actual-app/web": "npm:24.6.0" "@babel/preset-typescript": "npm:^7.20.2" "@types/bcrypt": "npm:^5.0.0" "@types/better-sqlite3": "npm:^7.6.7" From 7999fa8d62033a60a4f093c68962f7504500e4b9 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 26 Jun 2024 15:38:11 -0300 Subject: [PATCH 016/139] forbidden instead of 401 when session expired --- src/util/validate-user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 38af4d62d..b5f872198 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -16,7 +16,7 @@ export default function validateUser(req, res) { let session = getSession(token); - if (!session || session.expires_at * 1000 <= Date.now()) { + if (!session) { res.status(401); res.send({ status: 'error', @@ -27,7 +27,7 @@ export default function validateUser(req, res) { } if (session.expires_at * 1000 <= Date.now()) { - res.status(401); + res.status(403); res.send({ status: 'error', reason: 'unauthorized', From 10c9507412e67c1f54b7dd33ddc1289dbd2524cb Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 26 Jun 2024 15:46:55 -0300 Subject: [PATCH 017/139] cleanup --- .migrate | 29 ----------------------------- jest.setup.js | 17 ----------------- 2 files changed, 46 deletions(-) delete mode 100644 .migrate delete mode 100644 jest.setup.js diff --git a/.migrate b/.migrate deleted file mode 100644 index 55f84c86d..000000000 --- a/.migrate +++ /dev/null @@ -1,29 +0,0 @@ -{ - "lastRun": "1719409568000-multiuser.js", - "migrations": [ - { - "title": "1694360000000-create-folders.js", - "timestamp": 1719422276506 - }, - { - "title": "1694360479680-create-account-db.js", - "timestamp": 1719422276546 - }, - { - "title": "1694362247011-create-secret-table.js", - "timestamp": 1719422276557 - }, - { - "title": "1702667624000-rename-nordigen-secrets.js", - "timestamp": 1719422276558 - }, - { - "title": "1718889148000-openid.js", - "timestamp": 1719422276595 - }, - { - "title": "1719409568000-multiuser.js", - "timestamp": 1719422276674 - } - ] -} \ No newline at end of file diff --git a/jest.setup.js b/jest.setup.js deleted file mode 100644 index b2583a9c6..000000000 --- a/jest.setup.js +++ /dev/null @@ -1,17 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import config from './src/load-config.js'; -import getAccountDb from './src/account-db.js'; - -// Delete previous test database (force creation of a new one) -const dbPath = path.join(config.serverFiles, 'account.sqlite'); -if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); - -// Create path for test user files and delete previous files there -if (fs.existsSync(config.userFiles)) - fs.rmSync(config.userFiles, { recursive: true }); -fs.mkdirSync(config.userFiles); - -// Insert a fake "valid-token" fixture that can be reused -const db = getAccountDb(); -db.mutate('INSERT INTO sessions (token) VALUES (?)', ['valid-token']); From 71299b00bceb1b965c3d499affd5578243439858 Mon Sep 17 00:00:00 2001 From: lelemm Date: Fri, 12 Jul 2024 09:15:04 -0300 Subject: [PATCH 018/139] small fix --- src/accounts/openid.js | 6 +++--- upcoming-release-notes/381.md | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 upcoming-release-notes/381.md diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 508d2ea5a..77e7316d8 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -133,7 +133,7 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-grant-failed: no email found for the user' }; } - let { c } = accountDb.first('SELECT count(*) as C FROM users'); + let { c } = accountDb.first('SELECT count(*) as c FROM users'); let userId = null; if (c === undefined) { userId = uuid.v4(); @@ -147,9 +147,9 @@ export async function loginWithOpenIdFinalize(body) { [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], ); } else { - let { userId: userIdFromDb } = accountDb.first( + let { user_id: userIdFromDb } = accountDb.first( 'SELECT user_id FROM users WHERE user_name = ?', - userInfo.email, + [userInfo.email], ); if (userIdFromDb == null) { diff --git a/upcoming-release-notes/381.md b/upcoming-release-notes/381.md new file mode 100644 index 000000000..e1b8c807d --- /dev/null +++ b/upcoming-release-notes/381.md @@ -0,0 +1,6 @@ +--- +category: Features +authors: [apilat, lelemm] +--- + +Add support for authentication using OpenID Connect. From f25824c9679e59a546dae9dd14655187ff4c37c3 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 15 Jul 2024 11:09:52 -0300 Subject: [PATCH 019/139] moving to another pc --- migrations/1719409568000-multiuser.js | 2 +- src/account-db.js | 31 ++++++++++++++++++++++++++- src/accounts/openid.js | 2 +- upcoming-release-notes/219.md | 6 ------ 4 files changed, 32 insertions(+), 9 deletions(-) delete mode 100644 upcoming-release-notes/219.md diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 6985c4689..c62be67e3 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -13,7 +13,7 @@ export const up = async function () { permissions TEXT, name TEXT); - INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'CAN_EDIT, CAN_VIEW, CAN_DELETE, SETTINGS','Admin'); + INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'CAN_ADD_USER, CAN_VIEW','Admin'); INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', 'CAN_VIEW','Basic'); CREATE TABLE user_roles diff --git a/src/account-db.js b/src/account-db.js index 74e6775ae..2c5973cf9 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -119,7 +119,7 @@ export function login(password) { let accountDb = getAccountDb(); let row = accountDb.first('SELECT * FROM auth'); - let confirmed = row && bcrypt.compareSync(password, row.password); + let confirmed = row && bcrypt.compareSync(password, row.extra_data); if (!confirmed) { return { error: 'invalid-password' }; @@ -130,6 +130,35 @@ export function login(password) { // "session" that times out after a long time or something, and // maybe each device has a different token let sessionRow = accountDb.first('SELECT * FROM sessions'); + + let { c } = accountDb.first('SELECT count(*) as c FROM users'); + let userId = null; + if (c === 0) { + userId = uuid.v4(); + accountDb.mutate( + 'INSERT INTO users (user_id, user_name, enabled, master) VALUES (?, ?, 1, 1)', + [userId, ''], + ); + + accountDb.mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], + ); + } else { + let { user_id: userIdFromDb } = accountDb.first( + 'SELECT user_id FROM users WHERE user_name = ?', + [''], + ); + + userId = userIdFromDb; + } + + accountDb.mutate( + 'UPDATE sessions SET expires_at = ?, user_id = ? WHERE expires_at = -1', + [2147483647, userId], + ); + + return { token: sessionRow.token }; } diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 77e7316d8..75a9b77b5 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -135,7 +135,7 @@ export async function loginWithOpenIdFinalize(body) { let { c } = accountDb.first('SELECT count(*) as c FROM users'); let userId = null; - if (c === undefined) { + if (c === 0) { userId = uuid.v4(); accountDb.mutate( 'INSERT INTO users (user_id, user_name, enabled, master) VALUES (?, ?, 1, 1)', diff --git a/upcoming-release-notes/219.md b/upcoming-release-notes/219.md deleted file mode 100644 index b68a7019b..000000000 --- a/upcoming-release-notes/219.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -category: Features -authors: [apilat] ---- - -Add support for authentication using OpenID Connect. From c66712a70d298e4b26d751c8b2d3fa14fe740df4 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 23 Jul 2024 08:15:55 -0300 Subject: [PATCH 020/139] features --- migrations/1719409568000-multiuser.js | 41 ++- src/account-db.js | 16 +- src/accounts/openid.js | 22 +- src/app-account.js | 2 + src/app-admin.js | 436 ++++++++++++++++++++++++++ src/app-sync.js | 24 +- src/app.js | 3 + src/util/validate-user.js | 4 +- 8 files changed, 511 insertions(+), 37 deletions(-) create mode 100644 src/app-admin.js diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index c62be67e3..edddee8e1 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -3,31 +3,38 @@ import getAccountDb from '../src/account-db.js'; export const up = async function () { await getAccountDb().exec( `CREATE TABLE users - (user_id TEXT PRIMARY KEY, + (id TEXT PRIMARY KEY, user_name TEXT, + display_name TEXT, enabled INTEGER, master INTEGER); - CREATE TABLE roles - (role_id TEXT PRIMARY KEY, - permissions TEXT, - name TEXT); + CREATE TABLE roles + (id TEXT PRIMARY KEY, + permissions TEXT, + name TEXT); - INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'CAN_ADD_USER, CAN_VIEW','Admin'); - INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', 'CAN_VIEW','Basic'); - - CREATE TABLE user_roles - (user_id TEXT, - role_id TEXT); - + INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'ADMINISTRATOR','Admin'); + INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', '','Basic'); + + CREATE TABLE user_roles + (user_id TEXT, + role_id TEXT); + + CREATE TABLE user_access + (user_id TEXT, + file_id TEXT); + + ALTER TABLE files + ADD COLUMN owner TEXT; - DELETE FROM sessions; + DELETE FROM sessions; - ALTER TABLE sessions - ADD COLUMN expires_at INTEGER; + ALTER TABLE sessions + ADD COLUMN expires_at INTEGER; - ALTER TABLE sessions - ADD user_id TEXT; + ALTER TABLE sessions + ADD user_id TEXT; `, ); }; diff --git a/src/account-db.js b/src/account-db.js index 2c5973cf9..51982cd3b 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -136,8 +136,8 @@ export function login(password) { if (c === 0) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (user_id, user_name, enabled, master) VALUES (?, ?, 1, 1)', - [userId, ''], + 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, 1, 1)', + [userId, '', ''], ); accountDb.mutate( @@ -145,8 +145,8 @@ export function login(password) { [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], ); } else { - let { user_id: userIdFromDb } = accountDb.first( - 'SELECT user_id FROM users WHERE user_name = ?', + let { id: userIdFromDb } = accountDb.first( + 'SELECT id FROM users WHERE user_name = ?', [''], ); @@ -187,16 +187,16 @@ export function getSession(token) { export function getUserInfo(userId) { let accountDb = getAccountDb(); - return accountDb.first('SELECT * FROM users WHERE user_id = ?', [userId]); + return accountDb.first('SELECT * FROM users WHERE id = ?', [userId]); } export function getUserPermissions(userId) { let accountDb = getAccountDb(); return accountDb.all( `SELECT roles.permissions FROM users - JOIN user_roles ON user_roles.user_id = users.user_id - JOIN roles ON roles.role_id = user_roles.role_id - WHERE users.user_id = ?`, + JOIN user_roles ON user_roles.user_id = users.id + JOIN roles ON roles.id = user_roles.role_id + WHERE users.id = ?`, [userId], ); } diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 75a9b77b5..b6f5d78cf 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -83,7 +83,7 @@ export async function loginWithOpenIdSetup(body) { const url = client.authorizationUrl({ response_type: 'code', - scope: 'openid email', + scope: 'openid email profile', state, code_challenge, code_challenge_method: 'S256', @@ -138,8 +138,8 @@ export async function loginWithOpenIdFinalize(body) { if (c === 0) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (user_id, user_name, enabled, master) VALUES (?, ?, 1, 1)', - [userId, userInfo.email], + 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, 1, 1)', + [userId, userInfo.email, userInfo.name ?? userInfo.email], ); accountDb.mutate( @@ -147,10 +147,11 @@ export async function loginWithOpenIdFinalize(body) { [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], ); } else { - let { user_id: userIdFromDb } = accountDb.first( - 'SELECT user_id FROM users WHERE user_name = ?', - [userInfo.email], - ); + let { id: userIdFromDb, display_name: displayName } = + accountDb.first( + 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', + [userInfo.email], + ) || {}; if (userIdFromDb == null) { return { @@ -159,6 +160,13 @@ export async function loginWithOpenIdFinalize(body) { }; } + if (!displayName && userInfo.name) { + accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ + userInfo.name, + userIdFromDb, + ]); + } + userId = userIdFromDb; } diff --git a/src/app-account.js b/src/app-account.js index b2737e0f5..dc0da2266 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -127,6 +127,8 @@ app.get('/validate', (req, res) => { validated: true, userName: data?.user?.user_name, permissions: data?.permissions, + userId: data?.user_id, + displayName: data?.user?.display_name, }, }); } diff --git a/src/app-admin.js b/src/app-admin.js new file mode 100644 index 000000000..2ff8e9db6 --- /dev/null +++ b/src/app-admin.js @@ -0,0 +1,436 @@ +import express from 'express'; +import * as uuid from 'uuid'; +import errorMiddleware from './util/error-middleware.js'; +import validateUser from './util/validate-user.js'; +import getAccountDb from './account-db.js'; + +let app = express(); +app.use(errorMiddleware); + +export { app as handlers }; + +const sendErrorResponse = (res, status, reason, details) => { + res.status(status).send({ + status: 'error', + reason, + details, + }); +}; + +const getUserByUsername = (userName) => { + return ( + getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ + userName, + ]) || {} + ); +}; + +const getFileById = (fileId) => { + return ( + getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [fileId]) || + {} + ); +}; + +const getUserFromRequest = (req, res) => { + let user = validateUser(req, res); + if (!user) { + return null; + } + + if (user.permissions.indexOf('ADMINISTRATOR') === -1) { + sendErrorResponse(res, 401, 'unauthorized', 'permission-not-found'); + return null; + } + + return user; +}; + +const validateUserInput = (res, user) => { + if (!user.userName) { + sendErrorResponse( + res, + 400, + 'user-cant-be-empty', + 'Username cannot be empty', + ); + return false; + } + + if (!user.role) { + sendErrorResponse(res, 400, 'role-cant-be-empty', 'Role cannot be empty'); + return false; + } + + return true; +}; + +app.get('/users/', (req, res) => { + const users = getAccountDb().all( + `SELECT users.id, user_name as userName, display_name as displayName, enabled, master, roles.name as role + FROM users + JOIN user_roles ON user_roles.user_id = users.id + JOIN roles ON roles.id = user_roles.role_id`, + ); + + res.json(users); +}); + +app.post('/users', (req, res) => { + const user = getUserFromRequest(req, res); + if (!user) return; + + const newUser = req.body; + const { id: userIdInDb } = getUserByUsername(newUser.userName); + + if (!validateUserInput(res, newUser)) return; + if (userIdInDb) { + sendErrorResponse( + res, + 400, + 'user-already-exists', + `User ${newUser.userName} already exists`, + ); + return; + } + + const userId = uuid.v4(); + let displayName = newUser.displayName || null; + let enabled = newUser.enabled ? 1 : 0; + + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, 0)', + [userId, newUser.userName, displayName, enabled], + ); + + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, newUser.role], + ); + + res.status(200).send({ status: 'ok', data: { id: userId } }); +}); + +app.patch('/users', (req, res) => { + const user = getUserFromRequest(req, res); + if (!user) return; + + const userToUpdate = req.body; + const { id: userIdInDb } = + getAccountDb().first('SELECT id FROM users WHERE id = ?', [ + userToUpdate.id, + ]) || {}; + + if (!validateUserInput(res, userToUpdate)) return; + if (!userIdInDb) { + sendErrorResponse( + res, + 400, + 'cannot-find-user-to-update', + `Cannot find ${userToUpdate.userName} to update`, + ); + return; + } + + let displayName = userToUpdate.displayName || null; + let enabled = userToUpdate.enabled ? 1 : 0; + + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', + [userToUpdate.userName, displayName, enabled, userIdInDb], + ); + + getAccountDb().mutate('UPDATE user_roles SET role_id = ? WHERE user_id = ?', [ + userToUpdate.role, + userIdInDb, + ]); + + res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); +}); + +app.post('/users/delete-all', (req, res) => { + const user = getUserFromRequest(req, res); + if (!user) return; + + const ids = req.body.ids; + let totalDeleted = 0; + ids.forEach((item) => { + const { id: masterId } = + getAccountDb().first('SELECT id FROM users WHERE master = 1') || {}; + + getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [item]); + getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]); + getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ + masterId, + item, + ]); + const usersDeleted = getAccountDb().mutate( + 'DELETE FROM users WHERE id = ? and master = 0', + [item], + ).changes; + totalDeleted += usersDeleted; + }); + + if (ids.length === totalDeleted) { + res + .status(200) + .send({ status: 'ok', data: { someDeletionsFailed: false } }); + } else { + sendErrorResponse(res, 400, 'not-all-deleted', ''); + } +}); + +app.get('/access', (req, res) => { + const fileId = req.query.fileId; + const user = validateUser(req, res); + if (!user || !fileId) return; + + const users = getAccountDb().all( + `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName + FROM users + JOIN user_access ON user_access.user_id = users.id + JOIN files ON files.id = user_access.file_id + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [ + fileId, + user.user_id, + user.permissions.indexOf('ADMINISTRATOR') === -1 ? 0 : 1, + ], + ); + + res.json(users); +}); + +app.post('/access', (req, res) => { + const userAccess = req.body || {}; + const user = validateUser(req, res); + if (!user || !userAccess.fileId) return; + + const { id: fileIdInDb } = getFileById(userAccess.fileId); + if (!fileIdInDb) { + sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + return; + } + + const { granted } = + getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [ + userAccess.fileId, + user.user_id, + user.permissions.indexOf('ADMINISTRATOR') === -1 ? 0 : 1, + ], + ) || {}; + + if (granted === 0) { + sendErrorResponse( + res, + 400, + 'file-denied', + "You don't have permissions over this file", + ); + return; + } + + if (!userAccess.userId) { + sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); + return; + } + + const { cnt } = + getAccountDb().first( + 'SELECT count(*) AS cnt FROM user_access WHERE user_access.file_id = ? and user_access.user_id = ?', + [userAccess.fileId, userAccess.userId], + ) || {}; + + if (cnt > 0) { + sendErrorResponse( + res, + 400, + 'user-already-have-access', + 'User already have access', + ); + return; + } + + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + [userAccess.userId, userAccess.fileId], + ); + + res.status(200).send({ status: 'ok', data: {} }); +}); + +app.get('/access/available-users', (req, res) => { + const fileId = req.query.fileId; + const user = validateUser(req, res); + if (!user || !fileId) return; + + let canListAvailableUser = user.permissions.indexOf('ADMINISTRATOR') > -1; + if (!canListAvailableUser) { + const { canListAvaiableUserFromDB } = + getAccountDb().first( + `SELECT count(*) as canListAvaiableUserFromDB + FROM files + WHERE files.id = ? and files.owner = ?`, + [fileId, user.user_id], + ) || {}; + canListAvailableUser = canListAvaiableUserFromDB === 1; + } + + if (canListAvailableUser) { + const users = getAccountDb().all( + `SELECT users.id as userId, user_name as userName, display_name as displayName + FROM users + WHERE NOT EXISTS (SELECT 1 + FROM user_access + WHERE user_access.file_id = ? and user_access.user_id = users.id) + AND NOT EXISTS (SELECT 1 + FROM files + WHERE files.id = ? AND files.owner = users.id) + AND users.enabled = 1`, + [fileId, fileId], + ); + res.json(users); + } + return null; +}); + +app.post('/access/get-bulk', (req, res) => { + const fileIds = req.body || {}; + const accessMap = new Map(); + + fileIds.forEach((fileId) => { + const userAccess = getAccountDb().all( + `SELECT user_access.file_id as fileId, user_access.user_id as userId, users.display_name as displayName, users.user_name as userName + FROM users + JOIN user_access ON user_access.user_id = users.id + WHERE user_access.file_id = ? + UNION + SELECT files.id, users.id, users.display_name, users.user_name + FROM users + JOIN files ON files.owner = users.id + WHERE files.id = ?`, + [fileId, fileId], + ); + accessMap.set(fileId, userAccess); + }); + + res.status(200).send({ status: 'ok', data: Array.from(accessMap.entries()) }); +}); + +app.get('/access/check-access', (req, res) => { + const fileId = req.query.fileId; + const user = validateUser(req, res); + if (!user || !fileId) return; + + if (user.permissions.indexOf('ADMINISTRATOR') > -1) { + res.json({ granted: true }); + return; + } + + const { owner } = + getAccountDb().first( + `SELECT files.owner + FROM files + WHERE files.id = ?`, + [fileId], + ) || {}; + + res.json({ granted: owner === user.user_id }); +}); + +app.post('/access/transfer-ownership/', (req, res) => { + const newUserOwner = req.body || {}; + const user = validateUser(req, res); + if (!user || !newUserOwner.fileId) return; + + const { id: fileIdInDb } = getFileById(newUserOwner.fileId); + if (!fileIdInDb) { + sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + return; + } + + const { granted } = + getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [ + newUserOwner.fileId, + user.user_id, + user.permissions.indexOf('ADMINISTRATOR') === -1 ? 0 : 1, + ], + ) || {}; + + if (granted === 0) { + sendErrorResponse( + res, + 400, + 'file-denied', + "You don't have permissions over this file", + ); + return; + } + + if (!newUserOwner.newUserId) { + sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); + return; + } + + const { cnt } = + getAccountDb().first( + 'SELECT count(*) AS cnt FROM users WHERE users.id = ?', + [newUserOwner.newUserId], + ) || {}; + + if (cnt === 0) { + sendErrorResponse(res, 400, 'new-user-not-found', 'New user not found'); + return; + } + + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + newUserOwner.newUserId, + newUserOwner.fileId, + ]); + + res.status(200).send({ status: 'ok', data: {} }); +}); + +app.get('/file/owner', (req, res) => { + const fileId = req.query.fileId; + const user = validateUser(req, res); + if (!user || !fileId) return; + + let canGetOwner = user.permissions.indexOf('ADMINISTRATOR') > -1; + if (!canGetOwner) { + const { canListAvaiableUserFromDB } = + getAccountDb().first( + `SELECT count(*) as canListAvaiableUserFromDB + FROM files + WHERE files.id = ? and files.owner = ?`, + [fileId, user.user_id], + ) || {}; + canGetOwner = canListAvaiableUserFromDB === 1; + } + + if (canGetOwner) { + const owner = + getAccountDb().first( + `SELECT users.id, users.user_name userName, users.display_name as displayName + FROM files + JOIN users + ON users.id = files.owner + WHERE files.id = ?`, + [fileId], + ) || {}; + + res.json(owner); + } + + return null; +}); + +app.use(errorMiddleware); diff --git a/src/app-sync.js b/src/app-sync.js index 39cddf682..be40e4c8b 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -263,8 +263,8 @@ app.post('/upload-user-file', async (req, res) => { // it's new groupId = uuid.v4(); accountDb.mutate( - 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta) VALUES (?, ?, ?, ?, ?)', - [fileId, groupId, syncFormatVersion, name, encryptMeta], + 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, owner) VALUES (?, ?, ?, ?, ?, ?)', + [fileId, groupId, syncFormatVersion, name, encryptMeta, user.user.id], ); res.send(JSON.stringify({ status: 'ok', groupId })); } else { @@ -342,8 +342,25 @@ app.get('/list-user-files', (req, res) => { return; } + const canSeeAll = + user.master || + user.permissions.findIndex((permission) => permission === 'ADMINISTRATOR') > -1; + let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT * FROM files'); + let rows = canSeeAll + ? accountDb.all('SELECT * FROM files') + : accountDb.all( + `SELECT files.* + FROM files + WHERE files.owner = ? + UNION + SELECT files.* + FROM files + JOIN user_access + ON user_access.file_id = files.id + AND user_access.user_id = ?`, + [user.user_id, user.user_id], + ); res.send( JSON.stringify({ @@ -354,6 +371,7 @@ app.get('/list-user-files', (req, res) => { groupId: row.group_id, name: row.name, encryptKeyId: row.encrypt_keyid, + owner: row.owner, })), }), ); diff --git a/src/app.js b/src/app.js index 6dbf3506d..6777abfb6 100644 --- a/src/app.js +++ b/src/app.js @@ -11,6 +11,7 @@ import * as syncApp from './app-sync.js'; import * as goCardlessApp from './app-gocardless/app-gocardless.js'; import * as simpleFinApp from './app-simplefin/app-simplefin.js'; import * as secretApp from './app-secrets.js'; +import * as adminApp from './app-admin.js'; const app = express(); @@ -48,6 +49,8 @@ app.use('/gocardless', goCardlessApp.handlers); app.use('/simplefin', simpleFinApp.handlers); app.use('/secret', secretApp.handlers); +app.use('/admin', adminApp.handlers); + app.get('/mode', (req, res) => { res.send(config.mode); }); diff --git a/src/util/validate-user.js b/src/util/validate-user.js index b5f872198..67ce0b8e1 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -30,8 +30,8 @@ export default function validateUser(req, res) { res.status(403); res.send({ status: 'error', - reason: 'unauthorized', - details: 'token-expired', + reason: 'token-expired', + details: 'Token Expired. Login again', }); return null; } From 290819bb4e4bbbc6ee00a9fdd7270181af683192 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 24 Jul 2024 14:45:58 -0300 Subject: [PATCH 021/139] more features --- src/account-db.js | 79 ++++++++++++++++++++++++++++------------ src/accounts/openid.js | 25 ++++++++++--- src/accounts/password.js | 7 +++- src/app-account.js | 29 ++++++++++++++- src/app-admin.js | 13 +++++++ src/app-secrets.js | 21 +++++++++++ src/app.js | 26 +++++++++++++ src/config-types.ts | 9 ++++- src/load-config.js | 1 + 9 files changed, 177 insertions(+), 33 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 51982cd3b..09241dc6b 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -35,23 +35,25 @@ export function listLoginMethods() { /* * Get the Login Method in the following order + * Try to search for active method = openid, if enabled, skip the rest of the checks. Otherwise, follow checks bellow. * req (the frontend can say which method in the case it wants to resort to forcing password auth) * config options * fall back to using password */ export function getLoginMethod(req) { + let accountDb = getAccountDb(); + let row = accountDb.first('SELECT method FROM auth where active = 1'); + + if (row !== null && row['method'] === 'openid') { + return row['method']; + } + if ( typeof req !== 'undefined' && (req.body || { loginMethod: null }).loginMethod ) { return req.body.loginMethod; } - let accountDb = getAccountDb(); - let row = accountDb.first('SELECT method FROM auth where active = 1'); - - if (row !== null && row['method'] !== null) { - return row['method']; - } return config.loginMethod || 'password'; } @@ -64,7 +66,7 @@ export function getLoginMethod(req) { // "client_secret": "your_client_secret", // "server_hostname": "https://actual.your_website.com" // } -export function bootstrap(loginSettings) { +export async function bootstrap(loginSettings) { let accountDb = getAccountDb(); if (!needsBootstrap()) { @@ -96,41 +98,73 @@ export function bootstrap(loginSettings) { } if (openIdEnabled) { - let { error } = bootstrapOpenId(loginSettings.openid); + let { error } = await bootstrapOpenId(loginSettings.openid); if (error) { return { error }; } } - const token = uuid.v4(); - accountDb.mutate( - 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, -1, ?)', - [token, ''], - ); - return {}; } +export async function enableOpenID(loginSettings, checkFileConfig = true) { + if (checkFileConfig && config.openId) { + return { error: 'unable-to-change-file-config-enabled' }; + } + + let { error } = (await bootstrapOpenId(loginSettings.openId)) || {}; + if (error) { + return { error }; + } + + getAccountDb().mutate('DELETE FROM sessions'); + getAccountDb().mutate('DELETE FROM users'); + getAccountDb().mutate('DELETE FROM user_roles'); +} + +export async function disableOpenID(loginSettings, checkFileConfig = true) { + if (checkFileConfig && config.password) { + return { error: 'unable-to-change-file-config-enabled' }; + } + + let { error } = (await bootstrapPassword(loginSettings.password)) || {}; + if (error) { + return { error }; + } + + getAccountDb().mutate('DELETE FROM sessions'); + getAccountDb().mutate('DELETE FROM users'); + getAccountDb().mutate('DELETE FROM user_roles'); +} + export function login(password) { if (password === undefined || password === '') { return { error: 'invalid-password' }; } let accountDb = getAccountDb(); - let row = accountDb.first('SELECT * FROM auth'); + const { extra_data: passwordHash } = + accountDb.first( + 'SELECT extra_data FROM auth WHERE method = ? AND active = 1', + ['password'], + ) || {}; - let confirmed = row && bcrypt.compareSync(password, row.extra_data); + let confirmed = passwordHash && bcrypt.compareSync(password, passwordHash); if (!confirmed) { return { error: 'invalid-password' }; } - // Right now, tokens are permanent and there's just one in the - // system. In the future this should probably evolve to be a - // "session" that times out after a long time or something, and - // maybe each device has a different token let sessionRow = accountDb.first('SELECT * FROM sessions'); + let token = sessionRow ? sessionRow.token : uuid.v4(); + if (!sessionRow) { + accountDb.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + [token, -1, ''], + ); + } + let { c } = accountDb.first('SELECT count(*) as c FROM users'); let userId = null; if (c === 0) { @@ -151,15 +185,14 @@ export function login(password) { ); userId = userIdFromDb; - } + } accountDb.mutate( 'UPDATE sessions SET expires_at = ?, user_id = ? WHERE expires_at = -1', [2147483647, userId], ); - - return { token: sessionRow.token }; + return { token }; } export function changePassword(newPassword) { diff --git a/src/accounts/openid.js b/src/accounts/openid.js index b6f5d78cf..95077c2a5 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -2,7 +2,7 @@ import getAccountDb from '../account-db.js'; import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; -export function bootstrapOpenId(config) { +export async function bootstrapOpenId(config) { if (!Object.prototype.hasOwnProperty.call(config, 'issuer')) { return { error: 'missing-issuer' }; } @@ -16,6 +16,16 @@ export function bootstrapOpenId(config) { return { error: 'missing-server-hostname' }; } + try { + await setupOpenIdClient(config); + } catch (err) { + return { error: 'configuration-error' }; + } + + getAccountDb().mutate( + 'DELETE FROM auth WHERE method = ?',['openid'], + ); + // Beyond verifying that the configuration exists, we do not attempt // to check if the configuration is actually correct. // If the user improperly configures this during bootstrap, there is @@ -50,9 +60,9 @@ export async function loginWithOpenIdSetup(body) { } let accountDb = getAccountDb(); - let config = accountDb.first( - "SELECT extra_data FROM auth WHERE method = 'openid'", - ); + let config = accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'openid', + ]); if (!config) { return { error: 'openid-not-configured' }; } @@ -102,7 +112,7 @@ export async function loginWithOpenIdFinalize(body) { let accountDb = getAccountDb(); let config = accountDb.first( - "SELECT extra_data FROM auth WHERE method = 'openid'", + "SELECT extra_data FROM auth WHERE method = 'openid' AND active = 1", ); if (!config) { return { error: 'openid-not-configured' }; @@ -133,7 +143,10 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-grant-failed: no email found for the user' }; } - let { c } = accountDb.first('SELECT count(*) as c FROM users'); + let { c } = accountDb.first( + 'SELECT count(*) as c FROM users WHERE user_name <> ?', + [''], + ); let userId = null; if (c === 0) { userId = uuid.v4(); diff --git a/src/accounts/password.js b/src/accounts/password.js index d09b89cd5..6a43267e9 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -11,13 +11,18 @@ export function bootstrapPassword(password) { return { error: 'invalid-password' }; } + getAccountDb().mutate( + 'DELETE FROM auth WHERE method = ?',['password'], + ); + // Hash the password. There's really not a strong need for this // since this is a self-hosted instance owned by the user. // However, just in case we do it. let hashed = hashPassword(password); let accountDb = getAccountDb(); + accountDb.mutate('UPDATE auth SET active = 0'); accountDb.mutate( - "INSERT INTO auth (method, extra_data) VALUES ('password', ?)", + "INSERT INTO auth (method, extra_data, active) VALUES ('password', ?, 1)", [hashed], ); diff --git a/src/app-account.js b/src/app-account.js index dc0da2266..d51a6c274 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -7,6 +7,8 @@ import { getLoginMethod, listLoginMethods, login, + enableOpenID, + disableOpenID, } from './account-db.js'; import { changePassword } from './accounts/password.js'; import { @@ -32,8 +34,8 @@ app.get('/needs-bootstrap', (req, res) => { }); }); -app.post('/bootstrap', (req, res) => { - let { error } = bootstrap(req.body); +app.post('/bootstrap', async (req, res) => { + let { error } = await bootstrap(req.body); if (error) { res.status(400).send({ status: 'error', reason: error }); @@ -94,6 +96,29 @@ app.post('/login', async (req, res) => { res.send({ status: 'ok', data: { token } }); }); +app.post('/enable-openid', async (req, res) => { + let { error } = await enableOpenID(req.body) || {}; + + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } else { + res.send({ status: 'ok' }); + } +}); + +app.post('/enable-password', async (req, res) => { + let { error } = await disableOpenID(req.body) || {}; + + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } else { + res.send({ status: 'ok' }); + } +}); + +// app.get('/login-openid/cb', async (req, res) => { let { error, url } = await loginWithOpenIdFinalize(req.query); if (error) { diff --git a/src/app-admin.js b/src/app-admin.js index 2ff8e9db6..3174611a4 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -158,6 +158,9 @@ app.post('/users/delete-all', (req, res) => { const { id: masterId } = getAccountDb().first('SELECT id FROM users WHERE master = 1') || {}; + if(item === masterId) + return; + getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [item]); getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]); getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ @@ -433,4 +436,14 @@ app.get('/file/owner', (req, res) => { return null; }); +app.get('/auth-mode', (req, res) => { + const { method } = + getAccountDb().first( + `SELECT method from auth + where active = 1`, + ) || {}; + + res.json({ method }); +}); + app.use(errorMiddleware); diff --git a/src/app-secrets.js b/src/app-secrets.js index 1a6942e58..7728b1e7e 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -1,6 +1,7 @@ import express from 'express'; import validateUser from './util/validate-user.js'; import { secretsService } from './services/secrets-service.js'; +import getAccountDb from './account-db.js'; const app = express(); @@ -16,8 +17,28 @@ app.use(async (req, res, next) => { }); app.post('/', async (req, res) => { + const { method } = + getAccountDb().first('SELECT method FROM auth WHERE active = 1') || {}; + const { name, value } = req.body; + if (method === 'openid') { + const user = validateUser(req, res); + if (!user) return; + + let canSaveSecrets = user.permissions.indexOf('ADMINISTRATOR') > -1; + + if (!canSaveSecrets) { + res.status(400).send({ + status: 'error', + reason: 'not-admin', + details: 'You have to be admin to set secrets', + }); + + return null; + } + } + secretsService.set(name, value); res.status(200).send({ status: 'ok' }); diff --git a/src/app.js b/src/app.js index 6777abfb6..2c3315603 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,9 @@ import * as goCardlessApp from './app-gocardless/app-gocardless.js'; import * as simpleFinApp from './app-simplefin/app-simplefin.js'; import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; +import getAccountDb, { disableOpenID, enableOpenID } from './account-db.js'; +import { exit } from 'node:process'; +import { bootstrapOpenId } from './accounts/openid.js'; const app = express(); @@ -86,5 +89,28 @@ export default async function run() { } else { app.listen(config.port, config.hostname); } + + if(config.loginMethod === "openid") { + const { cnt } = getAccountDb().first("SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1",["openid"]); + if(cnt == 0) { + const { error } = await enableOpenID(config, false) || {}; + + if(error) { + console.error(error); + exit(-1); + } + } + } else if (config.loginMethod !== "openid") { + const { cnt } = getAccountDb().first("SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1",["openid"]); + if(cnt == 0) { + const { error } = await disableOpenID(config, false) || {}; + + if(error) { + console.error(error); + exit(-1); + } + } + } + console.log('Listening on ' + config.hostname + ':' + config.port + '...'); } diff --git a/src/config-types.ts b/src/config-types.ts index 8be7ba49d..feb4b3b46 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -2,7 +2,7 @@ import { ServerOptions } from 'https'; export interface Config { mode: 'test' | 'development'; - loginMethod: 'password' | 'header'; + loginMethod: 'password' | 'header' | 'openid'; trustedProxies: string[]; dataDir: string; projectRoot: string; @@ -20,4 +20,11 @@ export interface Config { syncEncryptedFileSizeLimitMB: number; fileSizeLimitMB: number; }; + openId?: { + issuer: string; + client_id: string; + client_secret: string; + server_hostname: string; + } + password?: string; } diff --git a/src/load-config.js b/src/load-config.js index d99ce4211..7dc9d4987 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -72,6 +72,7 @@ let defaultConfig = { fileSizeLimitMB: 20, }, projectRoot, + password: '' }; /** @type {import('./config-types.js').Config} */ From 54783caef4a9c7b8ca820dc8609226074d1b9358 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 24 Jul 2024 15:08:57 -0300 Subject: [PATCH 022/139] added multiuser configuration --- src/app-admin.js | 8 ++++++-- src/app.js | 1 - src/config-types.ts | 1 + src/load-config.js | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index 3174611a4..4a44b2093 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -3,6 +3,7 @@ import * as uuid from 'uuid'; import errorMiddleware from './util/error-middleware.js'; import validateUser from './util/validate-user.js'; import getAccountDb from './account-db.js'; +import config from './load-config.js'; let app = express(); app.use(errorMiddleware); @@ -158,8 +159,7 @@ app.post('/users/delete-all', (req, res) => { const { id: masterId } = getAccountDb().first('SELECT id FROM users WHERE master = 1') || {}; - if(item === masterId) - return; + if (item === masterId) return; getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [item]); getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]); @@ -446,4 +446,8 @@ app.get('/auth-mode', (req, res) => { res.json({ method }); }); +app.get('/multiuser', (req, res) => { + res.json(config.multiuser); +}); + app.use(errorMiddleware); diff --git a/src/app.js b/src/app.js index 2c3315603..027b9425e 100644 --- a/src/app.js +++ b/src/app.js @@ -14,7 +14,6 @@ import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; import getAccountDb, { disableOpenID, enableOpenID } from './account-db.js'; import { exit } from 'node:process'; -import { bootstrapOpenId } from './accounts/openid.js'; const app = express(); diff --git a/src/config-types.ts b/src/config-types.ts index feb4b3b46..35bd5e4d5 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -27,4 +27,5 @@ export interface Config { server_hostname: string; } password?: string; + multiuser: boolean; } diff --git a/src/load-config.js b/src/load-config.js index 7dc9d4987..4ec506706 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -72,7 +72,8 @@ let defaultConfig = { fileSizeLimitMB: 20, }, projectRoot, - password: '' + password: '', + multiuser: false, }; /** @type {import('./config-types.js').Config} */ From a3a75cf92a07b7b664f5e0cd2d3883325fb698c5 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 24 Jul 2024 15:53:15 -0300 Subject: [PATCH 023/139] lint fixes --- src/account-db.js | 2 -- src/accounts/openid.js | 4 +--- src/accounts/password.js | 4 +--- src/app-account.js | 4 ++-- src/app-sync.js | 3 ++- src/app.js | 30 ++++++++++++++++++------------ src/config-types.ts | 2 +- 7 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 09241dc6b..af357e934 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -67,8 +67,6 @@ export function getLoginMethod(req) { // "server_hostname": "https://actual.your_website.com" // } export async function bootstrap(loginSettings) { - let accountDb = getAccountDb(); - if (!needsBootstrap()) { return { error: 'already-bootstrapped' }; } diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 95077c2a5..8c08e75f6 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -22,9 +22,7 @@ export async function bootstrapOpenId(config) { return { error: 'configuration-error' }; } - getAccountDb().mutate( - 'DELETE FROM auth WHERE method = ?',['openid'], - ); + getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); // Beyond verifying that the configuration exists, we do not attempt // to check if the configuration is actually correct. diff --git a/src/accounts/password.js b/src/accounts/password.js index 6a43267e9..d2314cb0c 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -11,9 +11,7 @@ export function bootstrapPassword(password) { return { error: 'invalid-password' }; } - getAccountDb().mutate( - 'DELETE FROM auth WHERE method = ?',['password'], - ); + getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['password']); // Hash the password. There's really not a strong need for this // since this is a self-hosted instance owned by the user. diff --git a/src/app-account.js b/src/app-account.js index d51a6c274..1d1063dca 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -97,7 +97,7 @@ app.post('/login', async (req, res) => { }); app.post('/enable-openid', async (req, res) => { - let { error } = await enableOpenID(req.body) || {}; + let { error } = (await enableOpenID(req.body)) || {}; if (error) { res.status(400).send({ status: 'error', reason: error }); @@ -108,7 +108,7 @@ app.post('/enable-openid', async (req, res) => { }); app.post('/enable-password', async (req, res) => { - let { error } = await disableOpenID(req.body) || {}; + let { error } = (await disableOpenID(req.body)) || {}; if (error) { res.status(400).send({ status: 'error', reason: error }); diff --git a/src/app-sync.js b/src/app-sync.js index be40e4c8b..44717e33a 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -344,7 +344,8 @@ app.get('/list-user-files', (req, res) => { const canSeeAll = user.master || - user.permissions.findIndex((permission) => permission === 'ADMINISTRATOR') > -1; + user.permissions.findIndex((permission) => permission === 'ADMINISTRATOR') > + -1; let accountDb = getAccountDb(); let rows = canSeeAll diff --git a/src/app.js b/src/app.js index 027b9425e..6f5bfc7e8 100644 --- a/src/app.js +++ b/src/app.js @@ -89,22 +89,28 @@ export default async function run() { app.listen(config.port, config.hostname); } - if(config.loginMethod === "openid") { - const { cnt } = getAccountDb().first("SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1",["openid"]); - if(cnt == 0) { - const { error } = await enableOpenID(config, false) || {}; - - if(error) { + if (config.loginMethod === 'openid') { + const { cnt } = getAccountDb().first( + 'SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1', + ['openid'], + ); + if (cnt == 0) { + const { error } = (await enableOpenID(config, false)) || {}; + + if (error) { console.error(error); exit(-1); } } - } else if (config.loginMethod !== "openid") { - const { cnt } = getAccountDb().first("SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1",["openid"]); - if(cnt == 0) { - const { error } = await disableOpenID(config, false) || {}; - - if(error) { + } else if (config.loginMethod !== 'openid') { + const { cnt } = getAccountDb().first( + 'SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1', + ['openid'], + ); + if (cnt == 0) { + const { error } = (await disableOpenID(config, false)) || {}; + + if (error) { console.error(error); exit(-1); } diff --git a/src/config-types.ts b/src/config-types.ts index 35bd5e4d5..751765307 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -25,7 +25,7 @@ export interface Config { client_id: string; client_secret: string; server_hostname: string; - } + }; password?: string; multiuser: boolean; } From 517586cdd3d79e9dfb4f4632303a7d92ff755310 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 25 Jul 2024 16:34:31 -0300 Subject: [PATCH 024/139] adjustments and linter --- migrations/1718889148000-openid.js | 5 +- migrations/1719409568000-multiuser.js | 3 + src/account-db.js | 86 +++++++++++++------- src/accounts/openid.js | 6 +- src/accounts/password.js | 2 +- src/app-account.js | 20 +++-- src/app-admin.js | 108 ++++++++++++++++---------- src/app-secrets.js | 9 ++- src/app-sync.js | 16 ++-- src/app.js | 2 +- src/config-types.ts | 2 +- src/load-config.js | 3 +- src/util/validate-user.js | 15 +--- 13 files changed, 166 insertions(+), 111 deletions(-) diff --git a/migrations/1718889148000-openid.js b/migrations/1718889148000-openid.js index 5dacb72a0..a8423dcb4 100644 --- a/migrations/1718889148000-openid.js +++ b/migrations/1718889148000-openid.js @@ -4,10 +4,11 @@ export const up = async function () { await getAccountDb().exec( `CREATE TABLE auth_new (method TEXT PRIMARY KEY, + display_name TEXT, extra_data TEXT, active INTEGER); - INSERT INTO auth_new (method, extra_data, active) - SELECT 'password', password, 1 FROM auth; + INSERT INTO auth_new (method, display_name, extra_data, active) + SELECT 'password', 'Password', password, 1 FROM auth; DROP TABLE auth; ALTER TABLE auth_new RENAME TO auth; diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index edddee8e1..ec546f31c 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -35,6 +35,9 @@ export const up = async function () { ALTER TABLE sessions ADD user_id TEXT; + + ALTER TABLE sessions + ADD auth_method TEXT; `, ); }; diff --git a/src/account-db.js b/src/account-db.js index af357e934..4824689b1 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -29,25 +29,21 @@ export function needsBootstrap() { export function listLoginMethods() { let accountDb = getAccountDb(); - let rows = accountDb.all('SELECT method FROM auth'); - return rows.map((r) => r['method']); + let rows = accountDb.all('SELECT method, display_name, active FROM auth'); + return rows.map((r) => ({ + method: r.method, + active: r.active, + displayName: r.display_name, + })); } /* * Get the Login Method in the following order - * Try to search for active method = openid, if enabled, skip the rest of the checks. Otherwise, follow checks bellow. * req (the frontend can say which method in the case it wants to resort to forcing password auth) * config options * fall back to using password */ export function getLoginMethod(req) { - let accountDb = getAccountDb(); - let row = accountDb.first('SELECT method FROM auth where active = 1'); - - if (row !== null && row['method'] === 'openid') { - return row['method']; - } - if ( typeof req !== 'undefined' && (req.body || { loginMethod: null }).loginMethod @@ -106,7 +102,7 @@ export async function bootstrap(loginSettings) { } export async function enableOpenID(loginSettings, checkFileConfig = true) { - if (checkFileConfig && config.openId) { + if (checkFileConfig && config.loginMethod) { return { error: 'unable-to-change-file-config-enabled' }; } @@ -120,11 +116,30 @@ export async function enableOpenID(loginSettings, checkFileConfig = true) { getAccountDb().mutate('DELETE FROM user_roles'); } -export async function disableOpenID(loginSettings, checkFileConfig = true) { - if (checkFileConfig && config.password) { +export async function disableOpenID( + loginSettings, + checkFileConfig = true, + checkForOldPassword = false, +) { + if (checkFileConfig && config.loginMethod) { return { error: 'unable-to-change-file-config-enabled' }; } + if (checkForOldPassword) { + let accountDb = getAccountDb(); + const { extra_data: passwordHash } = + accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'password', + ]) || {}; + + let confirmed = + passwordHash && bcrypt.compareSync(loginSettings.password, passwordHash); + + if (!confirmed) { + return { error: 'invalid-password' }; + } + } + let { error } = (await bootstrapPassword(loginSettings.password)) || {}; if (error) { return { error }; @@ -133,6 +148,7 @@ export async function disableOpenID(loginSettings, checkFileConfig = true) { getAccountDb().mutate('DELETE FROM sessions'); getAccountDb().mutate('DELETE FROM users'); getAccountDb().mutate('DELETE FROM user_roles'); + getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); } export function login(password) { @@ -142,10 +158,9 @@ export function login(password) { let accountDb = getAccountDb(); const { extra_data: passwordHash } = - accountDb.first( - 'SELECT extra_data FROM auth WHERE method = ? AND active = 1', - ['password'], - ) || {}; + accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'password', + ]) || {}; let confirmed = passwordHash && bcrypt.compareSync(password, passwordHash); @@ -156,12 +171,6 @@ export function login(password) { let sessionRow = accountDb.first('SELECT * FROM sessions'); let token = sessionRow ? sessionRow.token : uuid.v4(); - if (!sessionRow) { - accountDb.mutate( - 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', - [token, -1, ''], - ); - } let { c } = accountDb.first('SELECT count(*) as c FROM users'); let userId = null; @@ -185,10 +194,17 @@ export function login(password) { userId = userIdFromDb; } - accountDb.mutate( - 'UPDATE sessions SET expires_at = ?, user_id = ? WHERE expires_at = -1', - [2147483647, userId], - ); + if (!sessionRow) { + accountDb.mutate( + 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', + [token, -1, userId, 'password'], + ); + } else { + accountDb.mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ + userId, + token, + ]); + } return { token }; } @@ -223,11 +239,21 @@ export function getUserInfo(userId) { export function getUserPermissions(userId) { let accountDb = getAccountDb(); - return accountDb.all( - `SELECT roles.permissions FROM users + const permissions = + accountDb.all( + `SELECT roles.permissions FROM users JOIN user_roles ON user_roles.user_id = users.id JOIN roles ON roles.id = user_roles.role_id WHERE users.id = ?`, - [userId], + [userId], + ) || []; + + const uniquePermissions = Array.from( + new Set( + permissions.flatMap((rolePermission) => + rolePermission.permissions.split(',').map((perm) => perm.trim()), + ), + ), ); + return uniquePermissions; } diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 8c08e75f6..616bd35a9 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -33,7 +33,7 @@ export async function bootstrapOpenId(config) { let accountDb = getAccountDb(); accountDb.mutate('UPDATE auth SET active = 0'); accountDb.mutate( - "INSERT INTO auth (method, extra_data, active) VALUES ('openid', ?, 1)", + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", [JSON.stringify(config)], ); @@ -183,8 +183,8 @@ export async function loginWithOpenIdFinalize(body) { const token = uuid.v4(); accountDb.mutate( - 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', - [token, grant.expires_at, userId], + 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', + [token, grant.expires_at, userId, 'openid'], ); return { url: `${return_url}/openid-cb?token=${token}` }; diff --git a/src/accounts/password.js b/src/accounts/password.js index d2314cb0c..dfcc43285 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -20,7 +20,7 @@ export function bootstrapPassword(password) { let accountDb = getAccountDb(); accountDb.mutate('UPDATE auth SET active = 0'); accountDb.mutate( - "INSERT INTO auth (method, extra_data, active) VALUES ('password', ?, 1)", + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)", [hashed], ); diff --git a/src/app-account.js b/src/app-account.js index 1d1063dca..16b1f95f9 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -9,6 +9,8 @@ import { login, enableOpenID, disableOpenID, + getUserInfo, + getUserPermissions, } from './account-db.js'; import { changePassword } from './accounts/password.js'; import { @@ -108,7 +110,7 @@ app.post('/enable-openid', async (req, res) => { }); app.post('/enable-password', async (req, res) => { - let { error } = (await disableOpenID(req.body)) || {}; + let { error } = (await disableOpenID(req.body, true, true)) || {}; if (error) { res.status(400).send({ status: 'error', reason: error }); @@ -144,16 +146,20 @@ app.post('/change-password', (req, res) => { }); app.get('/validate', (req, res) => { - let data = validateUser(req, res); - if (data) { + let session = validateUser(req, res); + if (session) { + const user = getUserInfo(session.user_id); + let permissions = getUserPermissions(session.user_id); + res.send({ status: 'ok', data: { validated: true, - userName: data?.user?.user_name, - permissions: data?.permissions, - userId: data?.user_id, - displayName: data?.user?.display_name, + userName: user?.user_name, + permissions: permissions, + userId: session?.user_id, + displayName: user?.display_name, + loginMethod: session?.auth_method, }, }); } diff --git a/src/app-admin.js b/src/app-admin.js index 4a44b2093..960ad0beb 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -2,7 +2,7 @@ import express from 'express'; import * as uuid from 'uuid'; import errorMiddleware from './util/error-middleware.js'; import validateUser from './util/validate-user.js'; -import getAccountDb from './account-db.js'; +import getAccountDb, { getUserPermissions } from './account-db.js'; import config from './load-config.js'; let app = express(); @@ -33,18 +33,18 @@ const getFileById = (fileId) => { ); }; -const getUserFromRequest = (req, res) => { - let user = validateUser(req, res); - if (!user) { +const getSessionFromRequest = (req, res) => { + let session = validateUser(req, res); + if (!session) { return null; } - if (user.permissions.indexOf('ADMINISTRATOR') === -1) { + if (getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1) { sendErrorResponse(res, 401, 'unauthorized', 'permission-not-found'); return null; } - return user; + return session; }; const validateUserInput = (res, user) => { @@ -63,22 +63,44 @@ const validateUserInput = (res, user) => { return false; } + const { id: roleId } = + getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ + user.role, + ]) || {}; + + if (!roleId) { + sendErrorResponse( + res, + 400, + 'role-does-not-exists', + 'Selected role does not exists', + ); + return false; + } + return true; }; app.get('/users/', (req, res) => { const users = getAccountDb().all( - `SELECT users.id, user_name as userName, display_name as displayName, enabled, master, roles.name as role + `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(master,0) as master, roles.id as role FROM users JOIN user_roles ON user_roles.user_id = users.id - JOIN roles ON roles.id = user_roles.role_id`, + JOIN roles ON roles.id = user_roles.role_id + WHERE users.user_name <> ''`, ); - res.json(users); + res.json( + users.map((u) => ({ + ...u, + master: u.master === 1, + enabled: u.enabled === 1, + })), + ); }); app.post('/users', (req, res) => { - const user = getUserFromRequest(req, res); + const user = getSessionFromRequest(req, res); if (!user) return; const newUser = req.body; @@ -113,7 +135,7 @@ app.post('/users', (req, res) => { }); app.patch('/users', (req, res) => { - const user = getUserFromRequest(req, res); + const user = getSessionFromRequest(req, res); if (!user) return; const userToUpdate = req.body; @@ -150,7 +172,7 @@ app.patch('/users', (req, res) => { }); app.post('/users/delete-all', (req, res) => { - const user = getUserFromRequest(req, res); + const user = getSessionFromRequest(req, res); if (!user) return; const ids = req.body.ids; @@ -185,10 +207,10 @@ app.post('/users/delete-all', (req, res) => { app.get('/access', (req, res) => { const fileId = req.query.fileId; - const user = validateUser(req, res); - if (!user || !fileId) return; + const session = validateUser(req, res); + if (!session || !fileId) return; - const users = getAccountDb().all( + const accesses = getAccountDb().all( `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName FROM users JOIN user_access ON user_access.user_id = users.id @@ -196,18 +218,20 @@ app.get('/access', (req, res) => { WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, [ fileId, - user.user_id, - user.permissions.indexOf('ADMINISTRATOR') === -1 ? 0 : 1, + session.user_id, + getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1 + ? 0 + : 1, ], ); - res.json(users); + res.json(accesses); }); app.post('/access', (req, res) => { const userAccess = req.body || {}; - const user = validateUser(req, res); - if (!user || !userAccess.fileId) return; + const session = validateUser(req, res); + if (!session || !userAccess.fileId) return; const { id: fileIdInDb } = getFileById(userAccess.fileId); if (!fileIdInDb) { @@ -222,8 +246,10 @@ app.post('/access', (req, res) => { WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, [ userAccess.fileId, - user.user_id, - user.permissions.indexOf('ADMINISTRATOR') === -1 ? 0 : 1, + session.user_id, + getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1 + ? 0 + : 1, ], ) || {}; @@ -268,17 +294,18 @@ app.post('/access', (req, res) => { app.get('/access/available-users', (req, res) => { const fileId = req.query.fileId; - const user = validateUser(req, res); - if (!user || !fileId) return; + const session = validateUser(req, res); + if (!session || !fileId) return; - let canListAvailableUser = user.permissions.indexOf('ADMINISTRATOR') > -1; + let canListAvailableUser = + getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1; if (!canListAvailableUser) { const { canListAvaiableUserFromDB } = getAccountDb().first( `SELECT count(*) as canListAvaiableUserFromDB FROM files WHERE files.id = ? and files.owner = ?`, - [fileId, user.user_id], + [fileId, session.user_id], ) || {}; canListAvailableUser = canListAvaiableUserFromDB === 1; } @@ -293,7 +320,7 @@ app.get('/access/available-users', (req, res) => { AND NOT EXISTS (SELECT 1 FROM files WHERE files.id = ? AND files.owner = users.id) - AND users.enabled = 1`, + AND users.enabled = 1 AND users.user_name <> ''`, [fileId, fileId], ); res.json(users); @@ -326,10 +353,10 @@ app.post('/access/get-bulk', (req, res) => { app.get('/access/check-access', (req, res) => { const fileId = req.query.fileId; - const user = validateUser(req, res); - if (!user || !fileId) return; + const session = validateUser(req, res); + if (!session || !fileId) return; - if (user.permissions.indexOf('ADMINISTRATOR') > -1) { + if (getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1) { res.json({ granted: true }); return; } @@ -342,13 +369,13 @@ app.get('/access/check-access', (req, res) => { [fileId], ) || {}; - res.json({ granted: owner === user.user_id }); + res.json({ granted: owner === session.user_id }); }); app.post('/access/transfer-ownership/', (req, res) => { const newUserOwner = req.body || {}; - const user = validateUser(req, res); - if (!user || !newUserOwner.fileId) return; + const session = validateUser(req, res); + if (!session || !newUserOwner.fileId) return; const { id: fileIdInDb } = getFileById(newUserOwner.fileId); if (!fileIdInDb) { @@ -363,8 +390,10 @@ app.post('/access/transfer-ownership/', (req, res) => { WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, [ newUserOwner.fileId, - user.user_id, - user.permissions.indexOf('ADMINISTRATOR') === -1 ? 0 : 1, + session.user_id, + getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1 + ? 0 + : 1, ], ) || {}; @@ -404,17 +433,18 @@ app.post('/access/transfer-ownership/', (req, res) => { app.get('/file/owner', (req, res) => { const fileId = req.query.fileId; - const user = validateUser(req, res); - if (!user || !fileId) return; + const session = validateUser(req, res); + if (!session || !fileId) return; - let canGetOwner = user.permissions.indexOf('ADMINISTRATOR') > -1; + let canGetOwner = + getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1; if (!canGetOwner) { const { canListAvaiableUserFromDB } = getAccountDb().first( `SELECT count(*) as canListAvaiableUserFromDB FROM files WHERE files.id = ? and files.owner = ?`, - [fileId, user.user_id], + [fileId, session.user_id], ) || {}; canGetOwner = canListAvaiableUserFromDB === 1; } diff --git a/src/app-secrets.js b/src/app-secrets.js index 7728b1e7e..91ab95360 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -1,7 +1,7 @@ import express from 'express'; import validateUser from './util/validate-user.js'; import { secretsService } from './services/secrets-service.js'; -import getAccountDb from './account-db.js'; +import getAccountDb, { getUserPermissions } from './account-db.js'; const app = express(); @@ -23,10 +23,11 @@ app.post('/', async (req, res) => { const { name, value } = req.body; if (method === 'openid') { - const user = validateUser(req, res); - if (!user) return; + const session = validateUser(req, res); + if (!session) return; - let canSaveSecrets = user.permissions.indexOf('ADMINISTRATOR') > -1; + let canSaveSecrets = + getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1; if (!canSaveSecrets) { res.status(400).send({ diff --git a/src/app-sync.js b/src/app-sync.js index 44717e33a..1adccdf52 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -9,7 +9,7 @@ import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; import * as simpleSync from './sync-simple.js'; import { SyncProtoBuf } from '@actual-app/crdt'; -import getAccountDb from './account-db.js'; +import getAccountDb, { getUserPermissions } from './account-db.js'; const app = express(); app.use(errorMiddleware); @@ -264,7 +264,7 @@ app.post('/upload-user-file', async (req, res) => { groupId = uuid.v4(); accountDb.mutate( 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, owner) VALUES (?, ?, ?, ?, ?, ?)', - [fileId, groupId, syncFormatVersion, name, encryptMeta, user.user.id], + [fileId, groupId, syncFormatVersion, name, encryptMeta, user.user_id], ); res.send(JSON.stringify({ status: 'ok', groupId })); } else { @@ -337,15 +337,15 @@ app.post('/update-user-filename', (req, res) => { }); app.get('/list-user-files', (req, res) => { - let user = validateUser(req, res); - if (!user) { + let session = validateUser(req, res); + if (!session) { return; } const canSeeAll = - user.master || - user.permissions.findIndex((permission) => permission === 'ADMINISTRATOR') > - -1; + getUserPermissions(session.user_id).findIndex( + (permission) => permission === 'ADMINISTRATOR', + ) > -1; let accountDb = getAccountDb(); let rows = canSeeAll @@ -360,7 +360,7 @@ app.get('/list-user-files', (req, res) => { JOIN user_access ON user_access.file_id = files.id AND user_access.user_id = ?`, - [user.user_id, user.user_id], + [session.user_id, session.user_id], ); res.send( diff --git a/src/app.js b/src/app.js index 6f5bfc7e8..197435288 100644 --- a/src/app.js +++ b/src/app.js @@ -102,7 +102,7 @@ export default async function run() { exit(-1); } } - } else if (config.loginMethod !== 'openid') { + } else if (config.loginMethod) { const { cnt } = getAccountDb().first( 'SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1', ['openid'], diff --git a/src/config-types.ts b/src/config-types.ts index 751765307..9588669ad 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -2,7 +2,7 @@ import { ServerOptions } from 'https'; export interface Config { mode: 'test' | 'development'; - loginMethod: 'password' | 'header' | 'openid'; + loginMethod?: 'password' | 'header' | 'openid' | null; trustedProxies: string[]; dataDir: string; projectRoot: string; diff --git a/src/load-config.js b/src/load-config.js index 4ec506706..a98ec1598 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -48,7 +48,6 @@ if (process.env.ACTUAL_CONFIG_PATH) { /** @type {Omit} */ let defaultConfig = { - loginMethod: 'password', // assume local networks are trusted for header authentication trustedProxies: [ '10.0.0.0/8', @@ -144,7 +143,7 @@ debug(`using data directory ${finalConfig.dataDir}`); debug(`using server files directory ${finalConfig.serverFiles}`); debug(`using user files directory ${finalConfig.userFiles}`); debug(`using web root directory ${finalConfig.webRoot}`); -debug(`using login method ${finalConfig.loginMethod}`); +debug(`using login method ${finalConfig.loginMethod ?? 'password'}`); debug(`using trusted proxies ${finalConfig.trustedProxies.join(', ')}`); if (finalConfig.https) { diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 67ce0b8e1..8d1fb4455 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -1,7 +1,7 @@ import config from '../load-config.js'; import proxyaddr from 'proxy-addr'; import ipaddr from 'ipaddr.js'; -import { getSession, getUserInfo, getUserPermissions } from '../account-db.js'; +import { getSession } from '../account-db.js'; /** * @param {import('express').Request} req @@ -26,7 +26,7 @@ export default function validateUser(req, res) { return null; } - if (session.expires_at * 1000 <= Date.now()) { + if (session.expires_at !== -1 && session.expires_at * 1000 <= Date.now()) { res.status(403); res.send({ status: 'error', @@ -36,17 +36,6 @@ export default function validateUser(req, res) { return null; } - session.user = getUserInfo(session.user_id); - let permissions = getUserPermissions(session.user_id); - const uniquePermissions = Array.from( - new Set( - permissions.flatMap((rolePermission) => - rolePermission.permissions.split(',').map((perm) => perm.trim()), - ), - ), - ); - session.permissions = uniquePermissions; - return session; } From 95f09a038d6e9e140ab4ad0dd4448d87e94f0953 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 25 Jul 2024 17:16:47 -0300 Subject: [PATCH 025/139] making code more readible --- src/account-db.js | 4 ++++ src/app-admin.js | 36 ++++++++---------------------------- src/app-secrets.js | 5 ++--- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 4824689b1..29845cbf8 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -101,6 +101,10 @@ export async function bootstrap(loginSettings) { return {}; } +export async function isAdmin(userId) { + return getUserPermissions(userId).some((value) => value === 'ADMINISTRATOR'); +} + export async function enableOpenID(loginSettings, checkFileConfig = true) { if (checkFileConfig && config.loginMethod) { return { error: 'unable-to-change-file-config-enabled' }; diff --git a/src/app-admin.js b/src/app-admin.js index 960ad0beb..f267c6883 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -2,7 +2,7 @@ import express from 'express'; import * as uuid from 'uuid'; import errorMiddleware from './util/error-middleware.js'; import validateUser from './util/validate-user.js'; -import getAccountDb, { getUserPermissions } from './account-db.js'; +import getAccountDb, { isAdmin } from './account-db.js'; import config from './load-config.js'; let app = express(); @@ -39,7 +39,7 @@ const getSessionFromRequest = (req, res) => { return null; } - if (getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1) { + if (!isAdmin(session.user_id)) { sendErrorResponse(res, 401, 'unauthorized', 'permission-not-found'); return null; } @@ -216,13 +216,7 @@ app.get('/access', (req, res) => { JOIN user_access ON user_access.user_id = users.id JOIN files ON files.id = user_access.file_id WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [ - fileId, - session.user_id, - getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1 - ? 0 - : 1, - ], + [fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], ); res.json(accesses); @@ -244,13 +238,7 @@ app.post('/access', (req, res) => { `SELECT 1 as granted FROM files WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [ - userAccess.fileId, - session.user_id, - getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1 - ? 0 - : 1, - ], + [userAccess.fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], ) || {}; if (granted === 0) { @@ -297,8 +285,7 @@ app.get('/access/available-users', (req, res) => { const session = validateUser(req, res); if (!session || !fileId) return; - let canListAvailableUser = - getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1; + let canListAvailableUser = isAdmin(session.user_id); if (!canListAvailableUser) { const { canListAvaiableUserFromDB } = getAccountDb().first( @@ -356,7 +343,7 @@ app.get('/access/check-access', (req, res) => { const session = validateUser(req, res); if (!session || !fileId) return; - if (getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1) { + if (isAdmin(session.user_id)) { res.json({ granted: true }); return; } @@ -388,13 +375,7 @@ app.post('/access/transfer-ownership/', (req, res) => { `SELECT 1 as granted FROM files WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [ - newUserOwner.fileId, - session.user_id, - getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') === -1 - ? 0 - : 1, - ], + [newUserOwner.fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], ) || {}; if (granted === 0) { @@ -436,8 +417,7 @@ app.get('/file/owner', (req, res) => { const session = validateUser(req, res); if (!session || !fileId) return; - let canGetOwner = - getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1; + let canGetOwner = isAdmin(session.user_id); if (!canGetOwner) { const { canListAvaiableUserFromDB } = getAccountDb().first( diff --git a/src/app-secrets.js b/src/app-secrets.js index 91ab95360..734dc1bae 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -1,7 +1,7 @@ import express from 'express'; import validateUser from './util/validate-user.js'; import { secretsService } from './services/secrets-service.js'; -import getAccountDb, { getUserPermissions } from './account-db.js'; +import getAccountDb, { isAdmin } from './account-db.js'; const app = express(); @@ -26,8 +26,7 @@ app.post('/', async (req, res) => { const session = validateUser(req, res); if (!session) return; - let canSaveSecrets = - getUserPermissions(session.user_id).indexOf('ADMINISTRATOR') > -1; + let canSaveSecrets = isAdmin(session.user_id); if (!canSaveSecrets) { res.status(400).send({ From 2f56c8b2b7d30124f66e07c38ebb9fab42a85188 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 30 Jul 2024 11:53:34 -0300 Subject: [PATCH 026/139] added token expiration and fixes --- src/account-db.js | 11 ++++++- src/accounts/openid.js | 12 +++++++- src/app-admin.js | 61 +++++++++++++++++++++++++++++++++++++-- src/config-types.ts | 1 + src/load-config.js | 1 + src/util/validate-user.js | 3 +- 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 29845cbf8..738f1917d 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -5,6 +5,7 @@ import * as uuid from 'uuid'; import * as bcrypt from 'bcrypt'; import { bootstrapPassword } from './accounts/password.js'; import { bootstrapOpenId } from './accounts/openid.js'; +import { TOKEN_EXPIRATION_NEVER } from './app-admin.js'; let _accountDb; @@ -201,7 +202,15 @@ export function login(password) { if (!sessionRow) { accountDb.mutate( 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', - [token, -1, userId, 'password'], + [ + token, + config.token_expiration == 'never' || + config.token_expiration == 'openid-provider' + ? TOKEN_EXPIRATION_NEVER + : config.token_expiration, + userId, + 'password', + ], ); } else { accountDb.mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 616bd35a9..97f15874d 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -1,6 +1,8 @@ import getAccountDb from '../account-db.js'; import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; +import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; +import finalConfig from '../load-config.js' export async function bootstrapOpenId(config) { if (!Object.prototype.hasOwnProperty.call(config, 'issuer')) { @@ -182,9 +184,17 @@ export async function loginWithOpenIdFinalize(body) { } const token = uuid.v4(); + + let expiration = TOKEN_EXPIRATION_NEVER; + if(finalConfig.token_expiration == 'openid-provider') { + expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; + } else if(finalConfig.token_expiration != 'never') { + expiration = finalConfig.token_expiration; + } + accountDb.mutate( 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', - [token, grant.expires_at, userId, 'openid'], + [token, expiration, userId, 'openid'], ); return { url: `${return_url}/openid-cb?token=${token}` }; diff --git a/src/app-admin.js b/src/app-admin.js index f267c6883..c7d9938e3 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -10,6 +10,8 @@ app.use(errorMiddleware); export { app as handlers }; +export const TOKEN_EXPIRATION_NEVER = -1; + const sendErrorResponse = (res, status, reason, details) => { res.status(status).send({ status: 'error', @@ -81,6 +83,16 @@ const validateUserInput = (res, user) => { return true; }; +app.get('/masterCreated/', (req, res) => { + const { cnt } = getAccountDb().first( + `SELECT count(*) as cnt + FROM users + WHERE users.user_name <> ''`, + ) || {}; + + res.json(cnt > 0); +}); + app.get('/users/', (req, res) => { const users = getAccountDb().all( `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(master,0) as master, roles.id as role @@ -280,6 +292,51 @@ app.post('/access', (req, res) => { res.status(200).send({ status: 'ok', data: {} }); }); +app.post('/access/delete-all', (req, res) => { + const fileId = req.query.fileId; + const session = validateUser(req, res); + if (!session) return; + + const { id: fileIdInDb } = getFileById(fileId); + if (!fileIdInDb) { + sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + return; + } + + const { granted } = + getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], + ) || {}; + + if (granted === 0) { + sendErrorResponse( + res, + 400, + 'file-denied', + "You don't have permissions over this file", + ); + return; + } + + const ids = req.body.ids; + let totalDeleted = 0; + ids.forEach((item) => { + const accessDeleted = getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]).changes; + totalDeleted += accessDeleted; + }); + + if (ids.length === totalDeleted) { + res + .status(200) + .send({ status: 'ok', data: { someDeletionsFailed: false } }); + } else { + sendErrorResponse(res, 400, 'not-all-deleted', ''); + } +}); + app.get('/access/available-users', (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); @@ -307,8 +364,8 @@ app.get('/access/available-users', (req, res) => { AND NOT EXISTS (SELECT 1 FROM files WHERE files.id = ? AND files.owner = users.id) - AND users.enabled = 1 AND users.user_name <> ''`, - [fileId, fileId], + AND users.enabled = 1 AND users.user_name <> '' AND users.id <> ?`, + [fileId, fileId, session.user_id], ); res.json(users); } diff --git a/src/config-types.ts b/src/config-types.ts index 9588669ad..f039480f2 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -28,4 +28,5 @@ export interface Config { }; password?: string; multiuser: boolean; + token_expiration?: 'never' | 'openid-provider' | number; } diff --git a/src/load-config.js b/src/load-config.js index a98ec1598..edec16141 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -73,6 +73,7 @@ let defaultConfig = { projectRoot, password: '', multiuser: false, + token_expiration: 'never', }; /** @type {import('./config-types.js').Config} */ diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 8d1fb4455..a4e3a335a 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -2,6 +2,7 @@ import config from '../load-config.js'; import proxyaddr from 'proxy-addr'; import ipaddr from 'ipaddr.js'; import { getSession } from '../account-db.js'; +import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; /** * @param {import('express').Request} req @@ -26,7 +27,7 @@ export default function validateUser(req, res) { return null; } - if (session.expires_at !== -1 && session.expires_at * 1000 <= Date.now()) { + if (session.expires_at !== TOKEN_EXPIRATION_NEVER && session.expires_at * 1000 <= Date.now()) { res.status(403); res.send({ status: 'error', From 001111249d45aa3e171639f08b94c1e83bed0c06 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 30 Jul 2024 16:56:17 -0300 Subject: [PATCH 027/139] fix on custom token_expiration --- src/accounts/openid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 97f15874d..fdca67c05 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -189,7 +189,7 @@ export async function loginWithOpenIdFinalize(body) { if(finalConfig.token_expiration == 'openid-provider') { expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; } else if(finalConfig.token_expiration != 'never') { - expiration = finalConfig.token_expiration; + expiration = Math.floor(Date.now() / 1000) + (finalConfig.token_expiration * 60); } accountDb.mutate( From 3234c2b422f1f38a7f158fd14116446cc8ad7061 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 30 Jul 2024 16:59:39 -0300 Subject: [PATCH 028/139] lint --- src/accounts/openid.js | 9 +++++---- src/app-admin.js | 12 ++++++++---- src/util/validate-user.js | 5 ++++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index fdca67c05..44ad79e68 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -2,7 +2,7 @@ import getAccountDb from '../account-db.js'; import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; -import finalConfig from '../load-config.js' +import finalConfig from '../load-config.js'; export async function bootstrapOpenId(config) { if (!Object.prototype.hasOwnProperty.call(config, 'issuer')) { @@ -186,10 +186,11 @@ export async function loginWithOpenIdFinalize(body) { const token = uuid.v4(); let expiration = TOKEN_EXPIRATION_NEVER; - if(finalConfig.token_expiration == 'openid-provider') { + if (finalConfig.token_expiration == 'openid-provider') { expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; - } else if(finalConfig.token_expiration != 'never') { - expiration = Math.floor(Date.now() / 1000) + (finalConfig.token_expiration * 60); + } else if (finalConfig.token_expiration != 'never') { + expiration = + Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; } accountDb.mutate( diff --git a/src/app-admin.js b/src/app-admin.js index c7d9938e3..73a12ba16 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -84,11 +84,12 @@ const validateUserInput = (res, user) => { }; app.get('/masterCreated/', (req, res) => { - const { cnt } = getAccountDb().first( - `SELECT count(*) as cnt + const { cnt } = + getAccountDb().first( + `SELECT count(*) as cnt FROM users WHERE users.user_name <> ''`, - ) || {}; + ) || {}; res.json(cnt > 0); }); @@ -324,7 +325,10 @@ app.post('/access/delete-all', (req, res) => { const ids = req.body.ids; let totalDeleted = 0; ids.forEach((item) => { - const accessDeleted = getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]).changes; + const accessDeleted = getAccountDb().mutate( + 'DELETE FROM user_access WHERE user_id = ?', + [item], + ).changes; totalDeleted += accessDeleted; }); diff --git a/src/util/validate-user.js b/src/util/validate-user.js index a4e3a335a..826ef6ef4 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -27,7 +27,10 @@ export default function validateUser(req, res) { return null; } - if (session.expires_at !== TOKEN_EXPIRATION_NEVER && session.expires_at * 1000 <= Date.now()) { + if ( + session.expires_at !== TOKEN_EXPIRATION_NEVER && + session.expires_at * 1000 <= Date.now() + ) { res.status(403); res.send({ status: 'error', From 78b27afdfb0f891d895b72504dbebb57a120d3e1 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 30 Jul 2024 17:05:01 -0300 Subject: [PATCH 029/139] build fixes --- src/app-admin.js | 8 ++++---- src/scripts/reset-password.js | 4 ++-- yarn.lock | 37 ++++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index 73a12ba16..c0c725c26 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -341,12 +341,12 @@ app.post('/access/delete-all', (req, res) => { } }); -app.get('/access/available-users', (req, res) => { +app.get('/access/available-users', async (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); if (!session || !fileId) return; - let canListAvailableUser = isAdmin(session.user_id); + let canListAvailableUser = await isAdmin(session.user_id); if (!canListAvailableUser) { const { canListAvaiableUserFromDB } = getAccountDb().first( @@ -473,12 +473,12 @@ app.post('/access/transfer-ownership/', (req, res) => { res.status(200).send({ status: 'ok', data: {} }); }); -app.get('/file/owner', (req, res) => { +app.get('/file/owner', async (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); if (!session || !fileId) return; - let canGetOwner = isAdmin(session.user_id); + let canGetOwner = await isAdmin(session.user_id); if (!canGetOwner) { const { canListAvaiableUserFromDB } = getAccountDb().first( diff --git a/src/scripts/reset-password.js b/src/scripts/reset-password.js index 5896e9e69..89950af63 100644 --- a/src/scripts/reset-password.js +++ b/src/scripts/reset-password.js @@ -7,8 +7,8 @@ if (needsBootstrap()) { 'It looks like you don’t have a password set yet. Let’s set one up now!', ); - promptPassword().then((password) => { - let { error } = bootstrap({ password }); + promptPassword().then(async (password) => { + let { error } = await bootstrap({ password }); if (error) { console.log('Error setting password:', error); console.log( diff --git a/yarn.lock b/yarn.lock index a69039f20..14d2f60c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4144,6 +4144,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:^4.15.5": + version: 4.15.9 + resolution: "jose@npm:4.15.9" + checksum: 10c0/4ed4ddf4a029db04bd167f2215f65d7245e4dc5f36d7ac3c0126aab38d66309a9e692f52df88975d99429e357e5fd8bab340ff20baab544d17684dd1d940a0f4 + languageName: node + linkType: hard + "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -4329,6 +4336,15 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 + languageName: node + linkType: hard + "make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -4815,6 +4831,13 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 10c0/1527de843926c5442ed61f8bdddfc7dc181b6497f725b0e89fcf50a55d9c803088763ed447cac85a5aa65345f1e99c2469ba679a54349ef3c4c0aeaa396a3eb9 + languageName: node + linkType: hard + "object-inspect@npm:^1.13.1": version: 1.13.1 resolution: "object-inspect@npm:1.13.1" @@ -4825,7 +4848,7 @@ __metadata: "oidc-token-hash@npm:^5.0.3": version: 5.0.3 resolution: "oidc-token-hash@npm:5.0.3" - checksum: d0dc0551406f09577874155cc83cf69c39e4b826293d50bb6c37936698aeca17d4bcee356ab910c859e53e83f2728a2acbd041020165191353b29de51fbca615 + checksum: 10c0/d0dc0551406f09577874155cc83cf69c39e4b826293d50bb6c37936698aeca17d4bcee356ab910c859e53e83f2728a2acbd041020165191353b29de51fbca615 languageName: node linkType: hard @@ -4863,6 +4886,18 @@ __metadata: languageName: node linkType: hard +"openid-client@npm:^5.4.2": + version: 5.6.5 + resolution: "openid-client@npm:5.6.5" + dependencies: + jose: "npm:^4.15.5" + lru-cache: "npm:^6.0.0" + object-hash: "npm:^2.2.0" + oidc-token-hash: "npm:^5.0.3" + checksum: 10c0/4308dcd37a9ffb1efc2ede0bc556ae42ccc2569e71baa52a03ddfa44407bf403d4534286f6f571381c5eaa1845c609ed699a5eb0d350acfb8c3bacb72c2a6890 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" From f6d9e61c346fd58997015335ca1846e65389709a Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 11:46:50 -0300 Subject: [PATCH 030/139] fixes --- src/account-db.js | 37 ++++++++++++++++++++++++------------- src/accounts/openid.js | 13 +++++++++++++ src/accounts/password.js | 16 +++++----------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 738f1917d..a4fc278d0 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -199,24 +199,24 @@ export function login(password) { userId = userIdFromDb; } + let expiration = TOKEN_EXPIRATION_NEVER; + if ( + config.token_expiration != 'never' && + config.token_expiration != 'openid-provider' + ) { + expiration = Math.floor(Date.now() / 1000) + config.token_expiration * 60; + } + if (!sessionRow) { accountDb.mutate( 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', - [ - token, - config.token_expiration == 'never' || - config.token_expiration == 'openid-provider' - ? TOKEN_EXPIRATION_NEVER - : config.token_expiration, - userId, - 'password', - ], + [token, expiration, userId, 'password'], ); } else { - accountDb.mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ - userId, - token, - ]); + accountDb.mutate( + 'UPDATE sessions SET user_id = ?, expires_at = ? WHERE token = ?', + [userId, expiration, token], + ); } return { token }; @@ -270,3 +270,14 @@ export function getUserPermissions(userId) { ); return uniquePermissions; } + +export function clearExpiredSessions() { + const clearThreshold = Math.floor(Date.now() / 1000) - 3600; + + const deletedSessions = getAccountDb().mutate( + 'DELETE FROM sessions WHERE expires_at <> -1 and expires < ?', + [clearThreshold], + ).changes; + + console.log(`Deleted ${deletedSessions} old sessions`); +} diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 44ad79e68..f6d95f211 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -198,8 +198,21 @@ export async function loginWithOpenIdFinalize(body) { [token, expiration, userId, 'openid'], ); + clearExpiredSessions(); + return { url: `${return_url}/openid-cb?token=${token}` }; } catch (err) { return { error: 'openid-grant-failed: ' + err }; } } + +function clearExpiredSessions() { + const clearThreshold = Math.floor(Date.now() / 1000) - 3600; + + const deletedSessions = getAccountDb().mutate( + 'DELETE FROM sessions WHERE expires_at <> -1 and expires < ?', + [clearThreshold], + ).changes; + + console.log(`Deleted ${deletedSessions} old sessions`); +} diff --git a/src/accounts/password.js b/src/accounts/password.js index dfcc43285..2570e761a 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -1,6 +1,6 @@ import * as bcrypt from 'bcrypt'; import * as uuid from 'uuid'; -import getAccountDb from '../account-db.js'; +import getAccountDb, { clearExpiredSessions } from '../account-db.js'; function hashPassword(password) { return bcrypt.hashSync(password, 12); @@ -35,11 +35,10 @@ export function loginWithPassword(password) { let confirmed = row && bcrypt.compareSync(password, row.extra_data); if (confirmed) { - // Right now, tokens are permanent and there's just one in the - // system. In the future this should probably evolve to be a - // "session" that times out after a long time or something, and - // maybe each device has a different token - let row = accountDb.first('SELECT token FROM sessions'); + let row = accountDb.first('SELECT token FROM sessions WHERE user_name = ?',['']); + + clearExpiredSessions(); + return row.token; } else { return null; @@ -54,13 +53,8 @@ export function changePassword(newPassword) { } let hashed = hashPassword(newPassword); - let token = uuid.v4(); - // This query does nothing if password authentication was disabled during - // bootstrap (then no row with method=password exists). Maybe we should - // return an error here if that is the case? accountDb.mutate("UPDATE auth SET extra_data = ? WHERE method = 'password'", [ hashed, ]); - accountDb.mutate('UPDATE sessions SET token = ?', [token]); return {}; } From e47f35c4df29c49c6a2e175f2940a234050bf931 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 12:14:27 -0300 Subject: [PATCH 031/139] missing file --- src/accounts/openid.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index f6d95f211..b471f3bf0 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -1,4 +1,4 @@ -import getAccountDb from '../account-db.js'; +import getAccountDb, { clearExpiredSessions } from '../account-db.js'; import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; @@ -205,14 +205,3 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-grant-failed: ' + err }; } } - -function clearExpiredSessions() { - const clearThreshold = Math.floor(Date.now() / 1000) - 3600; - - const deletedSessions = getAccountDb().mutate( - 'DELETE FROM sessions WHERE expires_at <> -1 and expires < ?', - [clearThreshold], - ).changes; - - console.log(`Deleted ${deletedSessions} old sessions`); -} From c8004584055cc387433942cea4a99c68b7ff64a6 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 12:38:20 -0300 Subject: [PATCH 032/139] linter --- src/accounts/password.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/accounts/password.js b/src/accounts/password.js index 2570e761a..703700230 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -35,7 +35,10 @@ export function loginWithPassword(password) { let confirmed = row && bcrypt.compareSync(password, row.extra_data); if (confirmed) { - let row = accountDb.first('SELECT token FROM sessions WHERE user_name = ?',['']); + let row = accountDb.first( + 'SELECT token FROM sessions WHERE user_name = ?', + [''], + ); clearExpiredSessions(); From 22a44e5e67cbf49cd6397d353fdefbac2bbe7e81 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 12:39:12 -0300 Subject: [PATCH 033/139] linter --- src/accounts/password.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/accounts/password.js b/src/accounts/password.js index 703700230..b67f7693f 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -1,5 +1,4 @@ import * as bcrypt from 'bcrypt'; -import * as uuid from 'uuid'; import getAccountDb, { clearExpiredSessions } from '../account-db.js'; function hashPassword(password) { From c7b7a18650c05ce92f97c9a52c2df0d4e83f6bf7 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 13:03:01 -0300 Subject: [PATCH 034/139] test fixes --- jest.global-setup.js | 2 +- migrations/1718889148000-openid.js | 5 +++++ migrations/1719409568000-multiuser.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 36f53cf1c..2f1a980b3 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -6,5 +6,5 @@ export default async function setup() { // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); - await db.mutate('INSERT INTO sessions (token) VALUES (?)', ['valid-token']); + await db.mutate('INSERT INTO sessions (token, expires_at) VALUES (?, ?)', ['valid-token', -1]); } diff --git a/migrations/1718889148000-openid.js b/migrations/1718889148000-openid.js index a8423dcb4..8f14f2c90 100644 --- a/migrations/1718889148000-openid.js +++ b/migrations/1718889148000-openid.js @@ -23,7 +23,12 @@ export const up = async function () { export const down = async function () { await getAccountDb().exec( ` + CREATE TABLE auth_new + (password TEXT); + INSERT INTO auth_new (password) + SELECT extra_data FROM auth; DROP TABLE auth; + ALTER TABLE auth_new RENAME TO auth; DROP TABLE pending_openid_requests; `, ); diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index ec546f31c..8cec2372b 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -45,7 +45,7 @@ export const up = async function () { export const down = async function () { await getAccountDb().exec( ` - DROP TABLE user; + DROP TABLE users; DROP TABLE roles; DROP TABLE user_roles; `, From 56eaf8985f9c20ebf12e915ae58730c479d57b65 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 13:04:38 -0300 Subject: [PATCH 035/139] linter --- jest.global-setup.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 2f1a980b3..a25cb96b0 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -6,5 +6,8 @@ export default async function setup() { // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); - await db.mutate('INSERT INTO sessions (token, expires_at) VALUES (?, ?)', ['valid-token', -1]); + await db.mutate('INSERT INTO sessions (token, expires_at) VALUES (?, ?)', [ + 'valid-token', + -1, + ]); } From 86f0deb3d7a0ecf5c5cdb7695be74a2d6f342b44 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 17:19:26 -0300 Subject: [PATCH 036/139] first tests --- jest.global-setup.js | 1 + src/app-admin.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/app-admin.test.js diff --git a/jest.global-setup.js b/jest.global-setup.js index a25cb96b0..0f383ef12 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -6,6 +6,7 @@ export default async function setup() { // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); + await db.mutate('DELETE FROM sessions'); await db.mutate('INSERT INTO sessions (token, expires_at) VALUES (?, ?)', [ 'valid-token', -1, diff --git a/src/app-admin.test.js b/src/app-admin.test.js new file mode 100644 index 000000000..d89bae09c --- /dev/null +++ b/src/app-admin.test.js @@ -0,0 +1,25 @@ +import request from 'supertest'; +import { handlers as app } from './app-sync.js'; + +describe('/admin', () => { + describe('/masterCreated', () => { + it('returns 200 and false if no master user is created', async () => { + const res = await request(app) + .get('/masterCreated') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body).toBe(false); + }); + }); + + describe('/users', () => { + it('returns 200 and a list of users', async () => { + const res = await request(app) + .get('/users') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + }); + }); +}); From b0989ecb31a1147c4650aba311b127cc5000cf34 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 31 Jul 2024 17:28:37 -0300 Subject: [PATCH 037/139] tests --- src/app-admin.test.js | 106 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/src/app-admin.test.js b/src/app-admin.test.js index d89bae09c..8f0170a96 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -1,8 +1,22 @@ import request from 'supertest'; -import { handlers as app } from './app-sync.js'; +import { handlers as app } from './app-admin.js'; +import getAccountDb from './account-db.js'; describe('/admin', () => { describe('/masterCreated', () => { + it('returns 200 and true if a master user is created', async () => { + getAccountDb().mutate( + "INSERT INTO users (id, user_name, display_name, enabled, master) VALUES ('', 'admin', '', 1, 1)", + ); + + const res = await request(app) + .get('/masterCreated') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body).toBe(true); + }); + it('returns 200 and false if no master user is created', async () => { const res = await request(app) .get('/masterCreated') @@ -15,6 +29,16 @@ describe('/admin', () => { describe('/users', () => { it('returns 200 and a list of users', async () => { + // const users = [ + // { + // id: '1', + // userName: 'user1', + // displayName: 'User One', + // enabled: 1, + // master: 0, + // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', + // }, + // ]; const res = await request(app) .get('/users') .set('x-actual-token', 'valid-token'); @@ -22,4 +46,84 @@ describe('/admin', () => { expect(res.statusCode).toEqual(200); }); }); + + // describe('/users (POST)', () => { + // it('returns 200 and creates a new user', async () => { + // const newUser = { + // userName: 'newuser', + // displayName: 'New User', + // enabled: true, + // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', + // }; + + // const res = await request(app) + // .post('/users') + // .set('x-actual-token', 'valid-token') + // .send(newUser); + + // console.log(res.body.data); + + // expect(res.statusCode).toEqual(200); + // expect(res.body.status).toBe('ok'); + // expect(res.body.data).toHaveProperty('id'); + // }); + + // it('returns 400 if the user already exists', async () => { + // const newUser = { + // userName: 'existinguser', + // displayName: 'Existing User', + // enabled: true, + // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', + // }; + + // const res = await request(app) + // .post('/users') + // .set('x-actual-token', 'valid-token') + // .send(newUser); + + // expect(res.statusCode).toEqual(400); + // expect(res.body.status).toBe('error'); + // expect(res.body.reason).toBe('user-already-exists'); + // }); + // }); + + // describe('/users (PATCH)', () => { + // it('returns 200 and updates an existing user', async () => { + // const userToUpdate = { + // id: 'existing-id', + // userName: 'updateduser', + // displayName: 'Updated User', + // enabled: true, + // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', + // }; + + // const res = await request(app) + // .patch('/users') + // .set('x-actual-token', 'valid-token') + // .send(userToUpdate); + + // expect(res.statusCode).toEqual(200); + // expect(res.body.status).toBe('ok'); + // expect(res.body.data.id).toBe('existing-id'); + // }); + + // it('returns 400 if the user does not exist', async () => { + // const userToUpdate = { + // id: 'non-existing-id', + // userName: 'nonexistinguser', + // displayName: 'Non-existing User', + // enabled: true, + // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', + // }; + + // const res = await request(app) + // .patch('/users') + // .set('x-actual-token', 'valid-token') + // .send(userToUpdate); + + // expect(res.statusCode).toEqual(400); + // expect(res.body.status).toBe('error'); + // expect(res.body.reason).toBe('cannot-find-user-to-update'); + // }); + // }); }); From a7053bfe14794849d8ead34573f823d14864dd1a Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 08:23:52 -0300 Subject: [PATCH 038/139] typo fix --- src/account-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account-db.js b/src/account-db.js index a4fc278d0..29ebd5de4 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -275,7 +275,7 @@ export function clearExpiredSessions() { const clearThreshold = Math.floor(Date.now() / 1000) - 3600; const deletedSessions = getAccountDb().mutate( - 'DELETE FROM sessions WHERE expires_at <> -1 and expires < ?', + 'DELETE FROM sessions WHERE expires_at <> -1 and expires_at < ?', [clearThreshold], ).changes; From 497c2af7eb8b2e3c0df480f51e5f3eaa53599feb Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 08:32:00 -0300 Subject: [PATCH 039/139] remove the init code from the old PR --- src/services/secrets-service.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/services/secrets-service.js b/src/services/secrets-service.js index ce56a7932..fb56825fe 100644 --- a/src/services/secrets-service.js +++ b/src/services/secrets-service.js @@ -17,13 +17,6 @@ class SecretsDb { constructor() { this.debug = createDebug('actual:secrets-db'); this.db = null; - this.initialize(); - } - - initialize() { - if (!this.db) { - this.db = this.open(); - } } open() { From 5bd5e920f8968d1a9b62d83f7b506072153d3178 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 10:01:14 -0300 Subject: [PATCH 040/139] minor bug when enabling openid is deleting the password user --- src/account-db.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 29ebd5de4..ed7ad0503 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -117,8 +117,6 @@ export async function enableOpenID(loginSettings, checkFileConfig = true) { } getAccountDb().mutate('DELETE FROM sessions'); - getAccountDb().mutate('DELETE FROM users'); - getAccountDb().mutate('DELETE FROM user_roles'); } export async function disableOpenID( From 501f553bae0e49febf71d52634acc6687a8ca523 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 10:05:37 -0300 Subject: [PATCH 041/139] fix bug when disabling openid --- src/account-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account-db.js b/src/account-db.js index ed7ad0503..21fedb223 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -149,7 +149,7 @@ export async function disableOpenID( } getAccountDb().mutate('DELETE FROM sessions'); - getAccountDb().mutate('DELETE FROM users'); + getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?',['']); getAccountDb().mutate('DELETE FROM user_roles'); getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); } From 52b0abc73e493d07a168aa800b49e533b5701c3a Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 10:30:29 -0300 Subject: [PATCH 042/139] another fix bug when disabling openid --- src/account-db.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 21fedb223..86cafc544 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -135,11 +135,13 @@ export async function disableOpenID( 'password', ]) || {}; - let confirmed = - passwordHash && bcrypt.compareSync(loginSettings.password, passwordHash); - - if (!confirmed) { - return { error: 'invalid-password' }; + if(passwordHash) { + let confirmed = + passwordHash && bcrypt.compareSync(loginSettings.password, passwordHash); + + if (!confirmed) { + return { error: 'invalid-password' }; + } } } From bff7c5555fafc5baf092ed24ff844e33e574927e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 16:04:56 -0300 Subject: [PATCH 043/139] added option to login without discovery url --- src/account-db.js | 41 +++++++++++++++++++++++++++++++++++++---- src/accounts/openid.js | 20 +++++++++++++++----- src/app.js | 30 ++---------------------------- src/config-types.ts | 9 ++++++++- 4 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 86cafc544..47e9fcc67 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -135,10 +135,11 @@ export async function disableOpenID( 'password', ]) || {}; - if(passwordHash) { + if (passwordHash) { let confirmed = - passwordHash && bcrypt.compareSync(loginSettings.password, passwordHash); - + passwordHash && + bcrypt.compareSync(loginSettings.password, passwordHash); + if (!confirmed) { return { error: 'invalid-password' }; } @@ -151,7 +152,7 @@ export async function disableOpenID( } getAccountDb().mutate('DELETE FROM sessions'); - getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?',['']); + getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?', ['']); getAccountDb().mutate('DELETE FROM user_roles'); getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); } @@ -281,3 +282,35 @@ export function clearExpiredSessions() { console.log(`Deleted ${deletedSessions} old sessions`); } + +export async function toogleAuthentication() { + if (config.loginMethod === 'openid') { + const { cnt } = getAccountDb().first( + 'SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1', + ['openid'], + ); + if (cnt == 0) { + const { error } = (await enableOpenID(config, false)) || {}; + + if (error) { + console.error(error); + return false; + } + } + } else if (config.loginMethod) { + const { cnt } = getAccountDb().first( + 'SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1', + ['openid'], + ); + if (cnt == 0) { + const { error } = (await disableOpenID(config, false)) || {}; + + if (error) { + console.error(error); + return false; + } + } + } + + return true; +} diff --git a/src/accounts/openid.js b/src/accounts/openid.js index b471f3bf0..fb65791a2 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -43,7 +43,15 @@ export async function bootstrapOpenId(config) { } async function setupOpenIdClient(config) { - const issuer = await Issuer.discover(config.issuer); + let issuer = + typeof config.issuer === 'string' + ? await Issuer.discover(config.issuer) + : new Issuer({ + issuer: config.issuer.name, + authorization_endpoint: config.issuer.authorization_endpoint, + token_endpoint: config.issuer.token_endpoint, + userinfo_endpoint: config.issuer.userinfo_endpoint, + }); const client = new issuer.Client({ client_id: config.client_id, @@ -139,8 +147,10 @@ export async function loginWithOpenIdFinalize(body) { redirect_uri: client.redirect_uris[0], }); const userInfo = await client.userinfo(grant); - if (userInfo.email == null) { - return { error: 'openid-grant-failed: no email found for the user' }; + const identity = + userInfo.login ?? userInfo.email ?? userInfo.id ?? userInfo.name; + if (identity == null) { + return { error: 'openid-grant-failed: no identification was found' }; } let { c } = accountDb.first( @@ -152,7 +162,7 @@ export async function loginWithOpenIdFinalize(body) { userId = uuid.v4(); accountDb.mutate( 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, 1, 1)', - [userId, userInfo.email, userInfo.name ?? userInfo.email], + [userId, identity, userInfo.name ?? userInfo.email ?? identity], ); accountDb.mutate( @@ -163,7 +173,7 @@ export async function loginWithOpenIdFinalize(body) { let { id: userIdFromDb, display_name: displayName } = accountDb.first( 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', - [userInfo.email], + [identity], ) || {}; if (userIdFromDb == null) { diff --git a/src/app.js b/src/app.js index 197435288..a6df225fe 100644 --- a/src/app.js +++ b/src/app.js @@ -12,7 +12,7 @@ import * as goCardlessApp from './app-gocardless/app-gocardless.js'; import * as simpleFinApp from './app-simplefin/app-simplefin.js'; import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; -import getAccountDb, { disableOpenID, enableOpenID } from './account-db.js'; +import { toogleAuthentication } from './account-db.js'; import { exit } from 'node:process'; const app = express(); @@ -89,33 +89,7 @@ export default async function run() { app.listen(config.port, config.hostname); } - if (config.loginMethod === 'openid') { - const { cnt } = getAccountDb().first( - 'SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1', - ['openid'], - ); - if (cnt == 0) { - const { error } = (await enableOpenID(config, false)) || {}; - - if (error) { - console.error(error); - exit(-1); - } - } - } else if (config.loginMethod) { - const { cnt } = getAccountDb().first( - 'SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1', - ['openid'], - ); - if (cnt == 0) { - const { error } = (await disableOpenID(config, false)) || {}; - - if (error) { - console.error(error); - exit(-1); - } - } - } + if (!(await toogleAuthentication())) exit(-1); console.log('Listening on ' + config.hostname + ':' + config.port + '...'); } diff --git a/src/config-types.ts b/src/config-types.ts index f039480f2..907a00d00 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -21,7 +21,14 @@ export interface Config { fileSizeLimitMB: number; }; openId?: { - issuer: string; + issuer: + | string + | { + name: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + }; client_id: string; client_secret: string; server_hostname: string; From 86b8697826e98e088d53352e4a1c21c7240a6a38 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 16:41:27 -0300 Subject: [PATCH 044/139] adjustments for keycloak --- src/accounts/openid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index fb65791a2..dfbf85c22 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -148,7 +148,7 @@ export async function loginWithOpenIdFinalize(body) { }); const userInfo = await client.userinfo(grant); const identity = - userInfo.login ?? userInfo.email ?? userInfo.id ?? userInfo.name; + userInfo.login ?? userInfo.email ?? userInfo.preferred_username ?? userInfo.id ?? userInfo.name; if (identity == null) { return { error: 'openid-grant-failed: no identification was found' }; } From 4fb25c9af9428e09fc055129976dd7ca0460abc7 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 1 Aug 2024 17:46:33 -0300 Subject: [PATCH 045/139] linter --- src/accounts/openid.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index dfbf85c22..dc1c0cfce 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -148,7 +148,11 @@ export async function loginWithOpenIdFinalize(body) { }); const userInfo = await client.userinfo(grant); const identity = - userInfo.login ?? userInfo.email ?? userInfo.preferred_username ?? userInfo.id ?? userInfo.name; + userInfo.login ?? + userInfo.email ?? + userInfo.preferred_username ?? + userInfo.id ?? + userInfo.name; if (identity == null) { return { error: 'openid-grant-failed: no identification was found' }; } From aedd2462b9eaf9a1190bcde212632d42f4a532f5 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 2 Aug 2024 16:44:21 -0300 Subject: [PATCH 046/139] more tests --- src/account-db.js | 2 +- src/app-account.js | 7 + src/app-admin.js | 181 ++++---- src/app-admin.test.js | 938 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 922 insertions(+), 206 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 47e9fcc67..5b7c9e813 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -102,7 +102,7 @@ export async function bootstrap(loginSettings) { return {}; } -export async function isAdmin(userId) { +export function isAdmin(userId) { return getUserPermissions(userId).some((value) => value === 'ADMINISTRATOR'); } diff --git a/src/app-account.js b/src/app-account.js index 3489f5a14..ba425cf4a 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -17,6 +17,7 @@ import { loginWithOpenIdSetup, loginWithOpenIdFinalize, } from './accounts/openid.js'; +import { getAdminSessionFromRequest } from './app-admin.js'; let app = express(); app.use(errorMiddleware); @@ -101,6 +102,9 @@ app.post('/login', async (req, res) => { }); app.post('/enable-openid', async (req, res) => { + const session = await getAdminSessionFromRequest(); + if (!session) return; + let { error } = (await enableOpenID(req.body)) || {}; if (error) { @@ -112,6 +116,9 @@ app.post('/enable-openid', async (req, res) => { }); app.post('/enable-password', async (req, res) => { + const session = await getAdminSessionFromRequest(); + if (!session) return; + let { error } = (await disableOpenID(req.body, true, true)) || {}; if (error) { diff --git a/src/app-admin.js b/src/app-admin.js index c0c725c26..e2a0086ba 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -4,9 +4,12 @@ import errorMiddleware from './util/error-middleware.js'; import validateUser from './util/validate-user.js'; import getAccountDb, { isAdmin } from './account-db.js'; import config from './load-config.js'; +import bodyParser from 'body-parser'; let app = express(); app.use(errorMiddleware); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); export { app as handlers }; @@ -35,13 +38,13 @@ const getFileById = (fileId) => { ); }; -const getSessionFromRequest = (req, res) => { +export const getAdminSessionFromRequest = async (req, res) => { let session = validateUser(req, res); if (!session) { return null; } - if (!isAdmin(session.user_id)) { + if (!(await isAdmin(session.user_id))) { sendErrorResponse(res, 401, 'unauthorized', 'permission-not-found'); return null; } @@ -95,6 +98,9 @@ app.get('/masterCreated/', (req, res) => { }); app.get('/users/', (req, res) => { + const session = validateUser(req, res); + if (!session) return; + const users = getAccountDb().all( `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(master,0) as master, roles.id as role FROM users @@ -112,8 +118,8 @@ app.get('/users/', (req, res) => { ); }); -app.post('/users', (req, res) => { - const user = getSessionFromRequest(req, res); +app.post('/users', async (req, res) => { + const user = await getAdminSessionFromRequest(req, res); if (!user) return; const newUser = req.body; @@ -147,8 +153,8 @@ app.post('/users', (req, res) => { res.status(200).send({ status: 'ok', data: { id: userId } }); }); -app.patch('/users', (req, res) => { - const user = getSessionFromRequest(req, res); +app.patch('/users', async (req, res) => { + const user = await getAdminSessionFromRequest(req, res); if (!user) return; const userToUpdate = req.body; @@ -184,8 +190,8 @@ app.patch('/users', (req, res) => { res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); }); -app.post('/users/delete-all', (req, res) => { - const user = getSessionFromRequest(req, res); +app.post('/users/delete-all', async (req, res) => { + const user = await getAdminSessionFromRequest(req, res); if (!user) return; const ids = req.body.ids; @@ -221,7 +227,13 @@ app.post('/users/delete-all', (req, res) => { app.get('/access', (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); - if (!session || !fileId) return; + if (!session) return; + + const { id: fileIdInDb } = getFileById(fileId); + if (!fileIdInDb) { + sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + return false; + } const accesses = getAccountDb().all( `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName @@ -235,35 +247,41 @@ app.get('/access', (req, res) => { res.json(accesses); }); -app.post('/access', (req, res) => { - const userAccess = req.body || {}; - const session = validateUser(req, res); - if (!session || !userAccess.fileId) return; - - const { id: fileIdInDb } = getFileById(userAccess.fileId); - if (!fileIdInDb) { - sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); - return; - } +function checkFilePermission(fileId, userId, res) { + const { granted } = getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ?)`, + [fileId, userId], + ) || { granted: 0 }; - const { granted } = - getAccountDb().first( - `SELECT 1 as granted - FROM files - WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [userAccess.fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], - ) || {}; - - if (granted === 0) { + if (granted === 0 && !isAdmin(userId)) { sendErrorResponse( res, 400, 'file-denied', "You don't have permissions over this file", ); - return; + return false; } + const { id: fileIdInDb } = getFileById(fileId); + if (!fileIdInDb) { + sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + return false; + } + + return true; +} + +app.post('/access', (req, res) => { + const userAccess = req.body || {}; + const session = validateUser(req, res); + + if (!session) return; + + if (!checkFilePermission(userAccess.fileId, session.user_id, res)) return; + if (!userAccess.userId) { sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); return; @@ -298,29 +316,7 @@ app.post('/access/delete-all', (req, res) => { const session = validateUser(req, res); if (!session) return; - const { id: fileIdInDb } = getFileById(fileId); - if (!fileIdInDb) { - sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); - return; - } - - const { granted } = - getAccountDb().first( - `SELECT 1 as granted - FROM files - WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], - ) || {}; - - if (granted === 0) { - sendErrorResponse( - res, - 400, - 'file-denied', - "You don't have permissions over this file", - ); - return; - } + if (!checkFilePermission(fileId, session.user_id, res)) return; const ids = req.body.ids; let totalDeleted = 0; @@ -344,36 +340,23 @@ app.post('/access/delete-all', (req, res) => { app.get('/access/available-users', async (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); - if (!session || !fileId) return; + if (!session) return; - let canListAvailableUser = await isAdmin(session.user_id); - if (!canListAvailableUser) { - const { canListAvaiableUserFromDB } = - getAccountDb().first( - `SELECT count(*) as canListAvaiableUserFromDB - FROM files - WHERE files.id = ? and files.owner = ?`, - [fileId, session.user_id], - ) || {}; - canListAvailableUser = canListAvaiableUserFromDB === 1; - } + if (!checkFilePermission(fileId, session.user_id, res)) return; - if (canListAvailableUser) { - const users = getAccountDb().all( - `SELECT users.id as userId, user_name as userName, display_name as displayName - FROM users - WHERE NOT EXISTS (SELECT 1 - FROM user_access - WHERE user_access.file_id = ? and user_access.user_id = users.id) - AND NOT EXISTS (SELECT 1 - FROM files - WHERE files.id = ? AND files.owner = users.id) - AND users.enabled = 1 AND users.user_name <> '' AND users.id <> ?`, - [fileId, fileId, session.user_id], - ); - res.json(users); - } - return null; + const users = getAccountDb().all( + `SELECT users.id as userId, user_name as userName, display_name as displayName + FROM users + WHERE NOT EXISTS (SELECT 1 + FROM user_access + WHERE user_access.file_id = ? and user_access.user_id = users.id) + AND NOT EXISTS (SELECT 1 + FROM files + WHERE files.id = ? AND files.owner = users.id) + AND users.enabled = 1 AND users.user_name <> '' AND users.id <> ?`, + [fileId, fileId, session.user_id], + ); + res.json(users); }); app.post('/access/get-bulk', (req, res) => { @@ -402,7 +385,13 @@ app.post('/access/get-bulk', (req, res) => { app.get('/access/check-access', (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); - if (!session || !fileId) return; + if (!session) return; + + const { id: fileIdInDb } = getFileById(fileId); + if (!fileIdInDb) { + sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + return false; + } if (isAdmin(session.user_id)) { res.json({ granted: true }); @@ -423,31 +412,9 @@ app.get('/access/check-access', (req, res) => { app.post('/access/transfer-ownership/', (req, res) => { const newUserOwner = req.body || {}; const session = validateUser(req, res); - if (!session || !newUserOwner.fileId) return; - - const { id: fileIdInDb } = getFileById(newUserOwner.fileId); - if (!fileIdInDb) { - sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); - return; - } - - const { granted } = - getAccountDb().first( - `SELECT 1 as granted - FROM files - WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [newUserOwner.fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], - ) || {}; + if (!session) return; - if (granted === 0) { - sendErrorResponse( - res, - 400, - 'file-denied', - "You don't have permissions over this file", - ); - return; - } + if (!checkFilePermission(newUserOwner.fileId, session.user_id, res)) return; if (!newUserOwner.newUserId) { sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); @@ -476,7 +443,9 @@ app.post('/access/transfer-ownership/', (req, res) => { app.get('/file/owner', async (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); - if (!session || !fileId) return; + if (!session) return; + + if (!checkFilePermission(fileId, session.user_id, res)) return; let canGetOwner = await isAdmin(session.user_id); if (!canGetOwner) { diff --git a/src/app-admin.test.js b/src/app-admin.test.js index 8f0170a96..a597ccc1f 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -2,12 +2,40 @@ import request from 'supertest'; import { handlers as app } from './app-admin.js'; import getAccountDb from './account-db.js'; +const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; +const BASIC_ROLE = 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc'; + +const createUser = (userId, userName, role, master = 0, enabled = 1) => { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, master], + ); + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, role], + ); +}; + +const deleteUser = (userId) => { + getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]); + getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); +}; + +const setSessionUser = (userId) => { + getAccountDb().mutate('UPDATE sessions SET user_id = ?', [userId]); +}; + describe('/admin', () => { + beforeEach(() => { + getAccountDb().mutate('DELETE FROM users'); + getAccountDb().mutate('DELETE FROM user_roles'); + getAccountDb().mutate('DELETE FROM files'); + getAccountDb().mutate('DELETE FROM user_access'); + }); + describe('/masterCreated', () => { - it('returns 200 and true if a master user is created', async () => { - getAccountDb().mutate( - "INSERT INTO users (id, user_name, display_name, enabled, master) VALUES ('', 'admin', '', 1, 1)", - ); + it('should return 200 and true if a master user is created', async () => { + createUser('adminId', 'admin', ADMIN_ROLE, 1); const res = await request(app) .get('/masterCreated') @@ -17,7 +45,7 @@ describe('/admin', () => { expect(res.body).toBe(true); }); - it('returns 200 and false if no master user is created', async () => { + it('should return 200 and false if no master user is created', async () => { const res = await request(app) .get('/masterCreated') .set('x-actual-token', 'valid-token'); @@ -28,102 +56,814 @@ describe('/admin', () => { }); describe('/users', () => { - it('returns 200 and a list of users', async () => { - // const users = [ - // { - // id: '1', - // userName: 'user1', - // displayName: 'User One', - // enabled: 1, - // master: 0, - // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', - // }, - // ]; - const res = await request(app) - .get('/users') - .set('x-actual-token', 'valid-token'); + describe('GET /users', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; - expect(res.statusCode).toEqual(200); + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and a list of users', async () => { + const res = await request(app) + .get('/users') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.length).toEqual(2); + }); + }); + + describe('POST /users', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and create a new user', async () => { + const newUser = { + userName: 'user1', + displayName: 'User One', + enabled: 1, + master: 0, + role: BASIC_ROLE, + }; + + const res = await request(app) + .post('/users') + .send(newUser) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data).toHaveProperty('id'); + }); + + it('should return 400 if the user already exists', async () => { + const newUser = { + userName: 'user1', + displayName: 'User One', + enabled: 1, + master: 0, + role: BASIC_ROLE, + }; + + await request(app) + .post('/users') + .send(newUser) + .set('x-actual-token', 'valid-token'); + + const res = await request(app) + .post('/users') + .send(newUser) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('user-already-exists'); + }); + }); + + describe('PATCH /users', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and update an existing user', async () => { + const userToUpdate = { + id: testUserId, + userName: 'updatedUser', + displayName: 'Updated User', + enabled: true, + role: BASIC_ROLE, + }; + + const res = await request(app) + .patch('/users') + .send(userToUpdate) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.id).toBe(testUserId); + }); + + it('should return 400 if the user does not exist', async () => { + const userToUpdate = { + id: 'non-existing-id', + userName: 'nonexistinguser', + displayName: 'Non-existing User', + enabled: true, + role: BASIC_ROLE, + }; + + const res = await request(app) + .patch('/users') + .send(userToUpdate) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('cannot-find-user-to-update'); + }); + }); + + describe('POST /users/delete-all', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + }); + + it('should return 200 and delete all specified users', async () => { + const userToDelete = { + ids: [testUserId], + }; + + const res = await request(app) + .post('/users/delete-all') + .send(userToDelete) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.someDeletionsFailed).toBe(false); + }); + + it('should return 400 if not all users are deleted', async () => { + const userToDelete = { + ids: ['non-existing-id'], + }; + + const res = await request(app) + .post('/users/delete-all') + .send(userToDelete) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('not-all-deleted'); + }); + }); + }); + + describe('/access', () => { + describe('GET /access', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and a list of accesses', async () => { + const res = await request(app) + .get('/access') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual([]); + }); + + it('should return 400 if fileId is missing', async () => { + const res = await request(app) + .get('/access') + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const res = await request(app) + .get('/access') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + }); + }); + + describe('POST /access', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and grant access to a user', async () => { + const newUserAccess = { + fileId, + userId: 'newUserId', + }; + + createUser('newUserId', 'newUser', BASIC_ROLE); + + const res = await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + }); + + it('should return 400 if the user already has access', async () => { + const newUserAccess = { + fileId, + userId: 'newUserId', + }; + + createUser('newUserId', 'newUser', BASIC_ROLE); + await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', 'valid-token'); + + const res = await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('user-already-have-access'); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const newUserAccess = { + fileId, + userId: 'newUserId', + }; + + createUser('newUserId', 'newUser', BASIC_ROLE); + + const res = await request(app) + .post('/access') + .send(newUserAccess) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + }); + }); + + describe('DELETE /access', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + createUser('newUserId', 'newUser', BASIC_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + ['newUserId', fileId], + ); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + deleteUser('newUserId'); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and delete access for the specified user', async () => { + const deleteAccess = { + ids: ['newUserId'], + }; + + const res = await request(app) + .post('/access/delete-all') + .send(deleteAccess) + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.someDeletionsFailed).toBe(false); + }); + + it('should return 400 if not all access deletions are successful', async () => { + const deleteAccess = { + ids: ['non-existing-id'], + }; + + const res = await request(app) + .post('/access/delete-all') + .send(deleteAccess) + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.status).toBe('error'); + expect(res.body.reason).toBe('not-all-deleted'); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const deleteAccess = { + ids: ['newUserId'], + }; + + const res = await request(app) + .post('/access/delete-all') + .send(deleteAccess) + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + expect(res.body.data.someDeletionsFailed).toBe(false); + }); + }); + + describe('GET /access/available-users', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and a list of available users', async () => { + const res = await request(app) + .get('/access/available-users') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.length).toEqual(1); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const res = await request(app) + .get('/access/available-users') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + }); + }); + + describe('GET /access/check-access', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and check access for the file', async () => { + const res = await request(app) + .get('/access/check-access') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.granted).toBe(true); + }); + + it('should return 400 if the file ID is invalid', async () => { + const res = await request(app) + .get('/access/check-access') + .query({ fileId: 'invalid-file-id' }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.reason).toBe('invalid-file-id'); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const res = await request(app) + .get('/access/check-access') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.granted).toBe(true); + }); + }); + + describe('POST /access/transfer-ownership', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + deleteUser('newUserId'); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and transfer ownership of the file', async () => { + const transferOwnership = { + fileId, + newUserId: 'newUserId', + }; + + createUser('newUserId', 'newUser', BASIC_ROLE); + + const res = await request(app) + .post('/access/transfer-ownership') + .send(transferOwnership) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + }); + + it('should return 400 if the new user does not exist', async () => { + const transferOwnership = { + fileId, + newUserId: 'non-existing-id', + }; + + const res = await request(app) + .post('/access/transfer-ownership') + .send(transferOwnership) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.reason).toBe('new-user-not-found'); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const transferOwnership = { + fileId, + newUserId: 'newUserId', + }; + + createUser('newUserId', 'newUser', BASIC_ROLE); + + const res = await request(app) + .post('/access/transfer-ownership') + .send(transferOwnership) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body.status).toBe('ok'); + }); + }); + + describe('GET /file/owner', () => { + const sessionUserId = 'sessionUserId'; + const testUserId = 'testUserId'; + const fileId = 'fileId'; + + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + + createUser(testUserId, 'testUser', ADMIN_ROLE); + getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ + fileId, + sessionUserId, + ]); + }); + + afterEach(() => { + deleteUser(sessionUserId); + deleteUser(testUserId); + getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); + }); + + it('should return 200 and the owner of the file', async () => { + const res = await request(app) + .get('/file/owner') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('id'); + }); + + it('should return 400 if the file ID is invalid', async () => { + const res = await request(app) + .get('/file/owner') + .query({ fileId: 'invalid-file-id' }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(400); + expect(res.body.reason).toBe('invalid-file-id'); + }); + + it('should return 200 for a basic user who owns the file', async () => { + deleteUser(sessionUserId); // Remove the admin user + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + sessionUserId, + fileId, + ]); + + const res = await request(app) + .get('/file/owner') + .query({ fileId }) + .set('x-actual-token', 'valid-token'); + + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty('id'); + }); }); }); - // describe('/users (POST)', () => { - // it('returns 200 and creates a new user', async () => { - // const newUser = { - // userName: 'newuser', - // displayName: 'New User', - // enabled: true, - // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', - // }; - - // const res = await request(app) - // .post('/users') - // .set('x-actual-token', 'valid-token') - // .send(newUser); - - // console.log(res.body.data); - - // expect(res.statusCode).toEqual(200); - // expect(res.body.status).toBe('ok'); - // expect(res.body.data).toHaveProperty('id'); - // }); - - // it('returns 400 if the user already exists', async () => { - // const newUser = { - // userName: 'existinguser', - // displayName: 'Existing User', - // enabled: true, - // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', - // }; - - // const res = await request(app) - // .post('/users') - // .set('x-actual-token', 'valid-token') - // .send(newUser); - - // expect(res.statusCode).toEqual(400); - // expect(res.body.status).toBe('error'); - // expect(res.body.reason).toBe('user-already-exists'); - // }); - // }); - - // describe('/users (PATCH)', () => { - // it('returns 200 and updates an existing user', async () => { - // const userToUpdate = { - // id: 'existing-id', - // userName: 'updateduser', - // displayName: 'Updated User', - // enabled: true, - // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', - // }; - - // const res = await request(app) - // .patch('/users') - // .set('x-actual-token', 'valid-token') - // .send(userToUpdate); - - // expect(res.statusCode).toEqual(200); - // expect(res.body.status).toBe('ok'); - // expect(res.body.data.id).toBe('existing-id'); - // }); - - // it('returns 400 if the user does not exist', async () => { - // const userToUpdate = { - // id: 'non-existing-id', - // userName: 'nonexistinguser', - // displayName: 'Non-existing User', - // enabled: true, - // role: 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', - // }; - - // const res = await request(app) - // .patch('/users') - // .set('x-actual-token', 'valid-token') - // .send(userToUpdate); - - // expect(res.statusCode).toEqual(400); - // expect(res.body.status).toBe('error'); - // expect(res.body.reason).toBe('cannot-find-user-to-update'); - // }); - // }); + describe('Token expired', () => { + beforeEach(() => { + getAccountDb().mutate('UPDATE sessions SET expires_at = 0'); + }); + + afterEach(() => { + getAccountDb().mutate('UPDATE sessions SET expires_at = -1'); + }); + + const endpoints = [ + { method: 'get', url: '/users/' }, + { method: 'post', url: '/users' }, + { method: 'patch', url: '/users' }, + { method: 'post', url: '/users/delete-all' }, + { method: 'post', url: '/access' }, + { method: 'post', url: '/access/delete-all' }, + { method: 'get', url: '/access/available-users' }, + { method: 'get', url: '/access/check-access' }, + { method: 'post', url: '/access/transfer-ownership/' }, + { method: 'get', url: '/file/owner' }, + ]; + + endpoints.forEach((endpoint) => { + it(`should return 403 for ${endpoint.method.toUpperCase()} ${ + endpoint.url + }`, async () => { + const method = request(app)[endpoint.method]; + const res = await method(endpoint.url).set( + 'x-actual-token', + 'valid-token', + ); + expect(res.statusCode).toEqual(403); + expect(res.body.reason).toEqual('token-expired'); + }); + }); + }); + + describe('Unauthorized access', () => { + const sessionUserId = 'sessionUserId'; + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + }); + + afterEach(() => { + deleteUser(sessionUserId); + }); + + const endpoints = [ + { method: 'post', url: '/users' }, + { method: 'patch', url: '/users' }, + { method: 'post', url: '/users/delete-all' }, + ]; + + endpoints.forEach((endpoint) => { + it(`should return 401 for ${endpoint.method.toUpperCase()} ${ + endpoint.url + }`, async () => { + const method = request(app)[endpoint.method]; + const res = await method(endpoint.url).set( + 'x-actual-token', + 'valid-token', + ); + expect(res.statusCode).toEqual(401); + expect(res.body.reason).toEqual('unauthorized'); + expect(res.body.details).toEqual('permission-not-found'); + }); + }); + }); + + describe('File denied access', () => { + const sessionUserId = 'sessionUserId'; + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', BASIC_ROLE); + setSessionUser(sessionUserId); + }); + + afterEach(() => { + deleteUser(sessionUserId); + }); + + const endpoints = [ + { method: 'post', url: '/access' }, + { method: 'post', url: '/access/delete-all' }, + { method: 'get', url: '/access/available-users' }, + { method: 'post', url: '/access/transfer-ownership/' }, + { method: 'get', url: '/file/owner' }, + ]; + + endpoints.forEach((endpoint) => { + it(`should return 400 for ${endpoint.method.toUpperCase()} ${ + endpoint.url + }`, async () => { + const method = request(app)[endpoint.method]; + const res = await method(endpoint.url).set( + 'x-actual-token', + 'valid-token', + ); + expect(res.statusCode).toEqual(400); + expect(res.body.reason).toEqual('file-denied'); + }); + }); + }); + + describe('Invalid file ID', () => { + const sessionUserId = 'sessionUserId'; + beforeEach(() => { + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + setSessionUser(sessionUserId); + }); + + afterEach(() => { + deleteUser(sessionUserId); + }); + + const endpoints = [ + { method: 'post', url: '/access' }, + { method: 'post', url: '/access/delete-all' }, + { method: 'get', url: '/access/available-users' }, + { method: 'post', url: '/access/transfer-ownership/' }, + { method: 'get', url: '/file/owner' }, + { method: 'get', url: '/access/check-access' }, + ]; + + endpoints.forEach((endpoint) => { + it(`should return 400 for ${endpoint.method.toUpperCase()} ${ + endpoint.url + }`, async () => { + const method = request(app)[endpoint.method]; + const res = await method(endpoint.url).set( + 'x-actual-token', + 'valid-token', + ); + expect(res.statusCode).toEqual(400); + expect(res.body.reason).toEqual('invalid-file-id'); + }); + }); + }); }); From c15d69a01f33e55b098b255a7912d3d8a275f494 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 6 Aug 2024 16:58:45 -0300 Subject: [PATCH 047/139] adjustments --- src/account-db.js | 4 ++++ src/accounts/password.js | 2 +- src/app-account.js | 7 +++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 5b7c9e813..090aedbed 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -135,6 +135,10 @@ export async function disableOpenID( 'password', ]) || {}; + if(!loginSettings?.password) { + return { error: 'invalid-password' }; + } + if (passwordHash) { let confirmed = passwordHash && diff --git a/src/accounts/password.js b/src/accounts/password.js index b67f7693f..bc19b5d30 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -6,7 +6,7 @@ function hashPassword(password) { } export function bootstrapPassword(password) { - if (password === null || password === '') { + if (!password || password === null || password === '') { return { error: 'invalid-password' }; } diff --git a/src/app-account.js b/src/app-account.js index ba425cf4a..54053a14d 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -18,8 +18,11 @@ import { loginWithOpenIdFinalize, } from './accounts/openid.js'; import { getAdminSessionFromRequest } from './app-admin.js'; +import bodyParser from 'body-parser'; let app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); app.use(errorMiddleware); export { app as handlers }; @@ -102,7 +105,7 @@ app.post('/login', async (req, res) => { }); app.post('/enable-openid', async (req, res) => { - const session = await getAdminSessionFromRequest(); + const session = await getAdminSessionFromRequest(req, res); if (!session) return; let { error } = (await enableOpenID(req.body)) || {}; @@ -116,7 +119,7 @@ app.post('/enable-openid', async (req, res) => { }); app.post('/enable-password', async (req, res) => { - const session = await getAdminSessionFromRequest(); + const session = await getAdminSessionFromRequest(req, res); if (!session) return; let { error } = (await disableOpenID(req.body, true, true)) || {}; From 34e9b4936019e8ad61013e77ea6ed127a8e2a76d Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 6 Aug 2024 17:00:43 -0300 Subject: [PATCH 048/139] linter fix --- src/account-db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account-db.js b/src/account-db.js index 090aedbed..e300136e8 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -135,7 +135,7 @@ export async function disableOpenID( 'password', ]) || {}; - if(!loginSettings?.password) { + if (!loginSettings?.password) { return { error: 'invalid-password' }; } From a11f5a0910ddca8fb46fec66d55950a981b3e78e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 4 Sep 2024 08:12:53 -0300 Subject: [PATCH 049/139] added environment variable --- src/load-config.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/load-config.js b/src/load-config.js index edec16141..cd61362fc 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -102,6 +102,9 @@ const finalConfig = { loginMethod: process.env.ACTUAL_LOGIN_METHOD ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() : config.loginMethod, + multiuser: process.env.ACTUAL_MULTIUSER + ? process.env.ACTUAL_MULTIUSER.toLowerCase() === "true" + : config.multiuser, trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) : config.trustedProxies, @@ -113,28 +116,28 @@ const finalConfig = { https: process.env.ACTUAL_HTTPS_KEY && process.env.ACTUAL_HTTPS_CERT ? { - key: process.env.ACTUAL_HTTPS_KEY.replace(/\\n/g, '\n'), - cert: process.env.ACTUAL_HTTPS_CERT.replace(/\\n/g, '\n'), - ...(config.https || {}), - } + key: process.env.ACTUAL_HTTPS_KEY.replace(/\\n/g, '\n'), + cert: process.env.ACTUAL_HTTPS_CERT.replace(/\\n/g, '\n'), + ...(config.https || {}), + } : config.https, upload: process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || - process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || - process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB + process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || + process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB ? { - fileSizeSyncLimitMB: - +process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || - +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || - config.upload.fileSizeSyncLimitMB, - syncEncryptedFileSizeLimitMB: - +process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || - +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || - config.upload.syncEncryptedFileSizeLimitMB, - fileSizeLimitMB: - +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || - config.upload.fileSizeLimitMB, - } + fileSizeSyncLimitMB: + +process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.fileSizeSyncLimitMB, + syncEncryptedFileSizeLimitMB: + +process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.syncEncryptedFileSizeLimitMB, + fileSizeLimitMB: + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.fileSizeLimitMB, + } : config.upload, }; From 260915f94947ee9f4b3a9eb6acfca5c3d1b5561d Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 4 Sep 2024 11:26:42 -0300 Subject: [PATCH 050/139] merge fixes for using middleware --- src/accounts/openid.js | 2 +- src/app-admin.js | 6 +++++- src/app-secrets.js | 6 +----- src/app-sync.js | 11 +++-------- src/util/middlewares.js | 6 ++++-- tsconfig.json | 1 + types/global.d.ts | 11 +++++++++++ 7 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 types/global.d.ts diff --git a/src/accounts/openid.js b/src/accounts/openid.js index dc1c0cfce..dab29d950 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -148,9 +148,9 @@ export async function loginWithOpenIdFinalize(body) { }); const userInfo = await client.userinfo(grant); const identity = + userInfo.preferred_username ?? userInfo.login ?? userInfo.email ?? - userInfo.preferred_username ?? userInfo.id ?? userInfo.name; if (identity == null) { diff --git a/src/app-admin.js b/src/app-admin.js index e2a0086ba..5c66e4edc 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -1,6 +1,9 @@ import express from 'express'; import * as uuid from 'uuid'; -import errorMiddleware from './util/error-middleware.js'; +import { + errorMiddleware, + requestLoggerMiddleware, +} from './util/middlewares.js'; import validateUser from './util/validate-user.js'; import getAccountDb, { isAdmin } from './account-db.js'; import config from './load-config.js'; @@ -8,6 +11,7 @@ import bodyParser from 'body-parser'; let app = express(); app.use(errorMiddleware); +app.use(requestLoggerMiddleware); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); diff --git a/src/app-secrets.js b/src/app-secrets.js index 582555187..cbd18fabd 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -11,7 +11,6 @@ const app = express(); export { app as handlers }; app.use(express.json()); app.use(requestLoggerMiddleware); - app.use(validateUserMiddleware); app.post('/', async (req, res) => { @@ -21,10 +20,7 @@ app.post('/', async (req, res) => { const { name, value } = req.body; if (method === 'openid') { - const session = validateUser(req, res); - if (!session) return; - - let canSaveSecrets = isAdmin(session.user_id); + let canSaveSecrets = isAdmin(req.userSession); if (!canSaveSecrets) { res.status(400).send({ diff --git a/src/app-sync.js b/src/app-sync.js index a22bcafa6..36b787ab9 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -261,7 +261,7 @@ app.post('/upload-user-file', async (req, res) => { groupId = uuid.v4(); accountDb.mutate( 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, owner) VALUES (?, ?, ?, ?, ?, ?)', - [fileId, groupId, syncFormatVersion, name, encryptMeta, user.user_id], + [fileId, groupId, syncFormatVersion, name, encryptMeta, req.userSession.user_id], ); res.send({ status: 'ok', groupId }); } else { @@ -328,13 +328,8 @@ app.post('/update-user-filename', (req, res) => { }); app.get('/list-user-files', (req, res) => { - let session = validateUser(req, res); - if (!session) { - return; - } - const canSeeAll = - getUserPermissions(session.user_id).findIndex( + getUserPermissions(req.userSession.user_id).findIndex( (permission) => permission === 'ADMINISTRATOR', ) > -1; @@ -351,7 +346,7 @@ app.get('/list-user-files', (req, res) => { JOIN user_access ON user_access.file_id = files.id AND user_access.user_id = ?`, - [session.user_id, session.user_id], + [req.userSession.user_id, req.userSession.user_id], ); res.send({ diff --git a/src/util/middlewares.js b/src/util/middlewares.js index 14e6e4dc1..a8c08aa48 100644 --- a/src/util/middlewares.js +++ b/src/util/middlewares.js @@ -20,10 +20,12 @@ async function errorMiddleware(err, req, res, _next) { * @param {import('express').NextFunction} next */ const validateUserMiddleware = async (req, res, next) => { - let user = await validateUser(req, res); - if (!user) { + let session = await validateUser(req, res); + if (!session) { return; } + + req.userSession = session; next(); }; diff --git a/tsconfig.json b/tsconfig.json index 2a3511698..cb09bd65f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,6 @@ "module": "node16", "outDir": "build" }, + "include": ["src/**/*.js", "types/global.d.ts"], "exclude": ["node_modules", "build", "./app-plaid.js", "coverage"], } diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 000000000..b6380fd02 --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,11 @@ +// types/global.d.ts + +import { Request } from 'express'; + +declare global { + namespace Express { + interface Request { + userSession?: any; + } + } +} From 59097816942a613cafeaf052f0eda5af84a5d1df Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 4 Sep 2024 11:35:21 -0300 Subject: [PATCH 051/139] linter fix --- src/app-sync.js | 11 +++++++++-- src/load-config.js | 40 ++++++++++++++++++++-------------------- types/global.d.ts | 8 +++++++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/app-sync.js b/src/app-sync.js index 36b787ab9..bc0b924a8 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -261,7 +261,14 @@ app.post('/upload-user-file', async (req, res) => { groupId = uuid.v4(); accountDb.mutate( 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, owner) VALUES (?, ?, ?, ?, ?, ?)', - [fileId, groupId, syncFormatVersion, name, encryptMeta, req.userSession.user_id], + [ + fileId, + groupId, + syncFormatVersion, + name, + encryptMeta, + req.userSession.user_id, + ], ); res.send({ status: 'ok', groupId }); } else { @@ -357,7 +364,7 @@ app.get('/list-user-files', (req, res) => { groupId: row.group_id, name: row.name, encryptKeyId: row.encrypt_keyid, - owner: row.owner, + owner: row.owner, })), }); }); diff --git a/src/load-config.js b/src/load-config.js index cd61362fc..b52a21be3 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -102,8 +102,8 @@ const finalConfig = { loginMethod: process.env.ACTUAL_LOGIN_METHOD ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() : config.loginMethod, - multiuser: process.env.ACTUAL_MULTIUSER - ? process.env.ACTUAL_MULTIUSER.toLowerCase() === "true" + multiuser: process.env.ACTUAL_MULTIUSER + ? process.env.ACTUAL_MULTIUSER.toLowerCase() === 'true' : config.multiuser, trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) @@ -116,28 +116,28 @@ const finalConfig = { https: process.env.ACTUAL_HTTPS_KEY && process.env.ACTUAL_HTTPS_CERT ? { - key: process.env.ACTUAL_HTTPS_KEY.replace(/\\n/g, '\n'), - cert: process.env.ACTUAL_HTTPS_CERT.replace(/\\n/g, '\n'), - ...(config.https || {}), - } + key: process.env.ACTUAL_HTTPS_KEY.replace(/\\n/g, '\n'), + cert: process.env.ACTUAL_HTTPS_CERT.replace(/\\n/g, '\n'), + ...(config.https || {}), + } : config.https, upload: process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || - process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || - process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB + process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || + process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB ? { - fileSizeSyncLimitMB: - +process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || - +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || - config.upload.fileSizeSyncLimitMB, - syncEncryptedFileSizeLimitMB: - +process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || - +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || - config.upload.syncEncryptedFileSizeLimitMB, - fileSizeLimitMB: - +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || - config.upload.fileSizeLimitMB, - } + fileSizeSyncLimitMB: + +process.env.ACTUAL_UPLOAD_FILE_SYNC_SIZE_LIMIT_MB || + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.fileSizeSyncLimitMB, + syncEncryptedFileSizeLimitMB: + +process.env.ACTUAL_UPLOAD_SYNC_ENCRYPTED_FILE_SYNC_SIZE_LIMIT_MB || + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.syncEncryptedFileSizeLimitMB, + fileSizeLimitMB: + +process.env.ACTUAL_UPLOAD_FILE_SIZE_LIMIT_MB || + config.upload.fileSizeLimitMB, + } : config.upload, }; diff --git a/types/global.d.ts b/types/global.d.ts index b6380fd02..64d07973e 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -1,11 +1,17 @@ // types/global.d.ts +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Request } from 'express'; +/* eslint-enable @typescript-eslint/no-unused-vars */ declare global { namespace Express { interface Request { - userSession?: any; + userSession?: { + expires_at?: number; + token: string; + user_id?: string; + }; } } } From 1fa13ece9e9ce73ea524130f70702b318d305cff Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 4 Sep 2024 12:13:02 -0300 Subject: [PATCH 052/139] linter and tests --- jest.global-setup.js | 8 ++++---- src/account-db.js | 4 ++++ src/app-admin.js | 8 ++------ src/app-sync.js | 7 ++----- src/app-sync.test.js | 43 +++++++++++++++++++++++++++++++++++-------- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 0f383ef12..ccb1cb0a4 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -7,8 +7,8 @@ export default async function setup() { // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); await db.mutate('DELETE FROM sessions'); - await db.mutate('INSERT INTO sessions (token, expires_at) VALUES (?, ?)', [ - 'valid-token', - -1, - ]); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token', -1, ''], + ); } diff --git a/src/account-db.js b/src/account-db.js index e300136e8..b46f3c622 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -106,6 +106,10 @@ export function isAdmin(userId) { return getUserPermissions(userId).some((value) => value === 'ADMINISTRATOR'); } +export function hasPermission(userId, permission) { + return getUserPermissions(userId).some((value) => value === permission); +} + export async function enableOpenID(loginSettings, checkFileConfig = true) { if (checkFileConfig && config.loginMethod) { return { error: 'unable-to-change-file-config-enabled' }; diff --git a/src/app-admin.js b/src/app-admin.js index 5c66e4edc..a27c49fa2 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -1,19 +1,15 @@ import express from 'express'; import * as uuid from 'uuid'; -import { - errorMiddleware, - requestLoggerMiddleware, -} from './util/middlewares.js'; +import { errorMiddleware } from './util/middlewares.js'; import validateUser from './util/validate-user.js'; import getAccountDb, { isAdmin } from './account-db.js'; import config from './load-config.js'; import bodyParser from 'body-parser'; let app = express(); -app.use(errorMiddleware); -app.use(requestLoggerMiddleware); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); +app.use(errorMiddleware); export { app as handlers }; diff --git a/src/app-sync.js b/src/app-sync.js index bc0b924a8..029a6696b 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -12,7 +12,7 @@ import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; import * as simpleSync from './sync-simple.js'; import { SyncProtoBuf } from '@actual-app/crdt'; -import getAccountDb, { getUserPermissions } from './account-db.js'; +import getAccountDb, { isAdmin } from './account-db.js'; const app = express(); app.use(errorMiddleware); @@ -335,10 +335,7 @@ app.post('/update-user-filename', (req, res) => { }); app.get('/list-user-files', (req, res) => { - const canSeeAll = - getUserPermissions(req.userSession.user_id).findIndex( - (permission) => permission === 'ADMINISTRATOR', - ) > -1; + const canSeeAll = isAdmin(req.userSession.user_id); let accountDb = getAccountDb(); let rows = canSeeAll diff --git a/src/app-sync.test.js b/src/app-sync.test.js index c5f036e34..8922c549c 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -6,6 +6,23 @@ import getAccountDb from './account-db.js'; import { SyncProtoBuf } from '@actual-app/crdt'; import crypto from 'node:crypto'; +const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; + +const createUser = (userId, userName, role, master = 0, enabled = 1) => { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, master], + ); + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, role], + ); +}; + +const setSessionUser = (userId) => { + getAccountDb().mutate('UPDATE sessions SET user_id = ?', [userId]); +}; + describe('/user-get-key', () => { it('returns 401 if the user is not authenticated', async () => { const res = await request(app).post('/user-get-key'); @@ -19,14 +36,17 @@ describe('/user-get-key', () => { }); it('returns encryption key details for a given fileId', async () => { + createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1); + setSessionUser('fileListAdminId'); + const fileId = crypto.randomBytes(16).toString('hex'); const encrypt_salt = 'test-salt'; const encrypt_keyid = 'test-key-id'; const encrypt_test = 'test-encrypt-test'; getAccountDb().mutate( - 'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test) VALUES (?, ?, ?, ?)', - [fileId, encrypt_salt, encrypt_keyid, encrypt_test], + 'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test, owner) VALUES (?, ?, ?, ?, ?)', + [fileId, encrypt_salt, encrypt_keyid, encrypt_test, ''], ); const res = await request(app) @@ -87,8 +107,13 @@ describe('/reset-user-file', () => { // Use addMockFile to insert a mock file into the database getAccountDb().mutate( - 'INSERT INTO files (id, group_id, deleted) VALUES (?, ?, FALSE)', - [fileId, groupId], + 'INSERT INTO files (id, group_id, deleted, owner) VALUES (?, ?, FALSE, ?)', + [fileId, groupId, ''], + ); + + getAccountDb().mutate( + 'INSERT INTO user_access (file_id, user_id) VALUES (?, ?)', + [fileId, ''], ); const res = await request(app) @@ -470,6 +495,8 @@ describe('/list-user-files', () => { }); it('returns a list of user files for an authenticated user', async () => { + createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1); + setSessionUser('fileListAdminId'); const fileId1 = crypto.randomBytes(16).toString('hex'); const fileId2 = crypto.randomBytes(16).toString('hex'); const fileName1 = 'file1.txt'; @@ -477,12 +504,12 @@ describe('/list-user-files', () => { // Insert mock files into the database getAccountDb().mutate( - 'INSERT INTO files (id, name, deleted) VALUES (?, ?, FALSE)', - [fileId1, fileName1], + 'INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)', + [fileId1, fileName1, ''], ); getAccountDb().mutate( - 'INSERT INTO files (id, name, deleted) VALUES (?, ?, FALSE)', - [fileId2, fileName2], + 'INSERT INTO files (id, name, deleted, owner) VALUES (?, ?, FALSE, ?)', + [fileId2, fileName2, ''], ); const res = await request(app) From 075cb4821d4fdb0765cd2a9150bc0ac6a9cd8ada Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 4 Sep 2024 14:53:27 -0300 Subject: [PATCH 053/139] tests adjustments --- jest.global-setup.js | 33 ++- src/app-admin.test.js | 613 ++++++------------------------------------ src/app-sync.test.js | 32 +-- 3 files changed, 117 insertions(+), 561 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index ccb1cb0a4..83148dc60 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -1,14 +1,45 @@ import getAccountDb from './src/account-db.js'; import runMigrations from './src/migrations.js'; +const createUser = (userId, userName, role, master = 0, enabled = 1) => { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, master], + ); + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, role], + ); +}; + +const setSessionUser = (userId) => { + getAccountDb().mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ + userId, + 'valid-token', + ]); +}; + export default async function setup() { await runMigrations(); + createUser( + 'genericAdmin', + 'admin', + '213733c1-5645-46ad-8784-a7b20b400f93', + 1, + ); + // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); await db.mutate('DELETE FROM sessions'); await db.mutate( 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', - ['valid-token', -1, ''], + ['valid-token', -1, 'genericAdmin'], ); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token-admin', -1, 'genericAdmin'], + ); + + setSessionUser('genericAdmin'); } diff --git a/src/app-admin.test.js b/src/app-admin.test.js index a597ccc1f..d9defaba6 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -1,6 +1,7 @@ import request from 'supertest'; import { handlers as app } from './app-admin.js'; import getAccountDb from './account-db.js'; +import { v4 as uuidv4 } from 'uuid'; const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; const BASIC_ROLE = 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc'; @@ -21,49 +22,43 @@ const deleteUser = (userId) => { getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); }; -const setSessionUser = (userId) => { - getAccountDb().mutate('UPDATE sessions SET user_id = ?', [userId]); +const createSession = (userId, sessionToken) => { + getAccountDb().mutate( + 'INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)', + [sessionToken, userId, Date.now() + 1000 * 60 * 60], // Expire in 1 hour + ); }; -describe('/admin', () => { - beforeEach(() => { - getAccountDb().mutate('DELETE FROM users'); - getAccountDb().mutate('DELETE FROM user_roles'); - getAccountDb().mutate('DELETE FROM files'); - getAccountDb().mutate('DELETE FROM user_access'); - }); +const generateSessionToken = () => `token-${uuidv4()}`; +describe('/admin', () => { describe('/masterCreated', () => { it('should return 200 and true if a master user is created', async () => { - createUser('adminId', 'admin', ADMIN_ROLE, 1); + const sessionToken = generateSessionToken(); + const adminId = uuidv4(); + createUser(adminId, 'admin', ADMIN_ROLE, 1); + createSession(adminId, sessionToken); const res = await request(app) .get('/masterCreated') - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body).toBe(true); }); - - it('should return 200 and false if no master user is created', async () => { - const res = await request(app) - .get('/masterCreated') - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body).toBe(false); - }); }); describe('/users', () => { describe('GET /users', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; + let sessionUserId, testUserId, sessionToken; beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + sessionUserId = uuidv4(); + testUserId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); }); @@ -75,25 +70,25 @@ describe('/admin', () => { it('should return 200 and a list of users', async () => { const res = await request(app) .get('/users') - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); - expect(res.body.length).toEqual(2); + expect(res.body.length).toBeGreaterThan(0); }); }); describe('POST /users', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; + let sessionUserId, sessionToken; beforeEach(() => { + sessionUserId = uuidv4(); + sessionToken = generateSessionToken(); createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + createSession(sessionUserId, sessionToken); }); afterEach(() => { deleteUser(sessionUserId); - deleteUser(testUserId); }); it('should return 200 and create a new user', async () => { @@ -108,7 +103,7 @@ describe('/admin', () => { const res = await request(app) .post('/users') .send(newUser) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('ok'); @@ -127,12 +122,12 @@ describe('/admin', () => { await request(app) .post('/users') .send(newUser) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); const res = await request(app) .post('/users') .send(newUser) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(400); expect(res.body.status).toBe('error'); @@ -141,13 +136,15 @@ describe('/admin', () => { }); describe('PATCH /users', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; + let sessionUserId, testUserId, sessionToken; beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + sessionUserId = uuidv4(); + testUserId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); }); @@ -168,7 +165,7 @@ describe('/admin', () => { const res = await request(app) .patch('/users') .send(userToUpdate) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('ok'); @@ -187,7 +184,7 @@ describe('/admin', () => { const res = await request(app) .patch('/users') .send(userToUpdate) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(400); expect(res.body.status).toBe('error'); @@ -196,13 +193,15 @@ describe('/admin', () => { }); describe('POST /users/delete-all', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; + let sessionUserId, testUserId, sessionToken; beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + sessionUserId = uuidv4(); + testUserId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); }); @@ -219,7 +218,7 @@ describe('/admin', () => { const res = await request(app) .post('/users/delete-all') .send(userToDelete) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('ok'); @@ -234,7 +233,7 @@ describe('/admin', () => { const res = await request(app) .post('/users/delete-all') .send(userToDelete) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(400); expect(res.body.status).toBe('error'); @@ -245,14 +244,16 @@ describe('/admin', () => { describe('/access', () => { describe('GET /access', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; + let sessionUserId, testUserId, fileId, sessionToken; beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + sessionUserId = uuidv4(); + testUserId = uuidv4(); + fileId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ fileId, @@ -270,7 +271,7 @@ describe('/admin', () => { const res = await request(app) .get('/access') .query({ fileId }) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body).toEqual([]); @@ -279,38 +280,23 @@ describe('/admin', () => { it('should return 400 if fileId is missing', async () => { const res = await request(app) .get('/access') - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(400); }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const res = await request(app) - .get('/access') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - }); }); describe('POST /access', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; + let sessionUserId, testUserId, fileId, sessionToken; beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + sessionUserId = uuidv4(); + testUserId = uuidv4(); + fileId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ fileId, @@ -327,15 +313,15 @@ describe('/admin', () => { it('should return 200 and grant access to a user', async () => { const newUserAccess = { fileId, - userId: 'newUserId', + userId: 'newUserId1', }; - createUser('newUserId', 'newUser', BASIC_ROLE); + createUser('newUserId1', 'newUser', BASIC_ROLE); const res = await request(app) .post('/access') .send(newUserAccess) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('ok'); @@ -344,89 +330,66 @@ describe('/admin', () => { it('should return 400 if the user already has access', async () => { const newUserAccess = { fileId, - userId: 'newUserId', + userId: 'newUserId2', }; - createUser('newUserId', 'newUser', BASIC_ROLE); + createUser('newUserId2', 'newUser', BASIC_ROLE); await request(app) .post('/access') .send(newUserAccess) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); const res = await request(app) .post('/access') .send(newUserAccess) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(400); expect(res.body.status).toBe('error'); expect(res.body.reason).toBe('user-already-have-access'); }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const newUserAccess = { - fileId, - userId: 'newUserId', - }; - - createUser('newUserId', 'newUser', BASIC_ROLE); - - const res = await request(app) - .post('/access') - .send(newUserAccess) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.status).toBe('ok'); - }); }); describe('DELETE /access', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; + let sessionUserId, testUserId, fileId, sessionToken; beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); + sessionUserId = uuidv4(); + testUserId = uuidv4(); + fileId = uuidv4(); + sessionToken = generateSessionToken(); + createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); + createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); - createUser('newUserId', 'newUser', BASIC_ROLE); + createUser('newUserId3', 'newUser', BASIC_ROLE); getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ fileId, sessionUserId, ]); getAccountDb().mutate( 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', - ['newUserId', fileId], + ['newUserId3', fileId], ); }); afterEach(() => { deleteUser(sessionUserId); deleteUser(testUserId); - deleteUser('newUserId'); + deleteUser('newUserId3'); getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); }); it('should return 200 and delete access for the specified user', async () => { const deleteAccess = { - ids: ['newUserId'], + ids: ['newUserId3'], }; const res = await request(app) .post('/access/delete-all') .send(deleteAccess) .query({ fileId }) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('ok'); @@ -442,428 +405,12 @@ describe('/admin', () => { .post('/access/delete-all') .send(deleteAccess) .query({ fileId }) - .set('x-actual-token', 'valid-token'); + .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(400); expect(res.body.status).toBe('error'); expect(res.body.reason).toBe('not-all-deleted'); }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const deleteAccess = { - ids: ['newUserId'], - }; - - const res = await request(app) - .post('/access/delete-all') - .send(deleteAccess) - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.status).toBe('ok'); - expect(res.body.data.someDeletionsFailed).toBe(false); - }); - }); - - describe('GET /access/available-users', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; - - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); - - createUser(testUserId, 'testUser', ADMIN_ROLE); - getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ - fileId, - sessionUserId, - ]); - }); - - afterEach(() => { - deleteUser(sessionUserId); - deleteUser(testUserId); - getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); - }); - - it('should return 200 and a list of available users', async () => { - const res = await request(app) - .get('/access/available-users') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.length).toEqual(1); - }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const res = await request(app) - .get('/access/available-users') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - }); - }); - - describe('GET /access/check-access', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; - - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); - - createUser(testUserId, 'testUser', ADMIN_ROLE); - getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ - fileId, - sessionUserId, - ]); - }); - - afterEach(() => { - deleteUser(sessionUserId); - deleteUser(testUserId); - getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); - }); - - it('should return 200 and check access for the file', async () => { - const res = await request(app) - .get('/access/check-access') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.granted).toBe(true); - }); - - it('should return 400 if the file ID is invalid', async () => { - const res = await request(app) - .get('/access/check-access') - .query({ fileId: 'invalid-file-id' }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(400); - expect(res.body.reason).toBe('invalid-file-id'); - }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const res = await request(app) - .get('/access/check-access') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.granted).toBe(true); - }); - }); - - describe('POST /access/transfer-ownership', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; - - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); - - createUser(testUserId, 'testUser', ADMIN_ROLE); - getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ - fileId, - sessionUserId, - ]); - }); - - afterEach(() => { - deleteUser(sessionUserId); - deleteUser(testUserId); - deleteUser('newUserId'); - getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); - }); - - it('should return 200 and transfer ownership of the file', async () => { - const transferOwnership = { - fileId, - newUserId: 'newUserId', - }; - - createUser('newUserId', 'newUser', BASIC_ROLE); - - const res = await request(app) - .post('/access/transfer-ownership') - .send(transferOwnership) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.status).toBe('ok'); - }); - - it('should return 400 if the new user does not exist', async () => { - const transferOwnership = { - fileId, - newUserId: 'non-existing-id', - }; - - const res = await request(app) - .post('/access/transfer-ownership') - .send(transferOwnership) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(400); - expect(res.body.reason).toBe('new-user-not-found'); - }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const transferOwnership = { - fileId, - newUserId: 'newUserId', - }; - - createUser('newUserId', 'newUser', BASIC_ROLE); - - const res = await request(app) - .post('/access/transfer-ownership') - .send(transferOwnership) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body.status).toBe('ok'); - }); - }); - - describe('GET /file/owner', () => { - const sessionUserId = 'sessionUserId'; - const testUserId = 'testUserId'; - const fileId = 'fileId'; - - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); - - createUser(testUserId, 'testUser', ADMIN_ROLE); - getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ - fileId, - sessionUserId, - ]); - }); - - afterEach(() => { - deleteUser(sessionUserId); - deleteUser(testUserId); - getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); - }); - - it('should return 200 and the owner of the file', async () => { - const res = await request(app) - .get('/file/owner') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('id'); - }); - - it('should return 400 if the file ID is invalid', async () => { - const res = await request(app) - .get('/file/owner') - .query({ fileId: 'invalid-file-id' }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(400); - expect(res.body.reason).toBe('invalid-file-id'); - }); - - it('should return 200 for a basic user who owns the file', async () => { - deleteUser(sessionUserId); // Remove the admin user - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - sessionUserId, - fileId, - ]); - - const res = await request(app) - .get('/file/owner') - .query({ fileId }) - .set('x-actual-token', 'valid-token'); - - expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('id'); - }); - }); - }); - - describe('Token expired', () => { - beforeEach(() => { - getAccountDb().mutate('UPDATE sessions SET expires_at = 0'); - }); - - afterEach(() => { - getAccountDb().mutate('UPDATE sessions SET expires_at = -1'); - }); - - const endpoints = [ - { method: 'get', url: '/users/' }, - { method: 'post', url: '/users' }, - { method: 'patch', url: '/users' }, - { method: 'post', url: '/users/delete-all' }, - { method: 'post', url: '/access' }, - { method: 'post', url: '/access/delete-all' }, - { method: 'get', url: '/access/available-users' }, - { method: 'get', url: '/access/check-access' }, - { method: 'post', url: '/access/transfer-ownership/' }, - { method: 'get', url: '/file/owner' }, - ]; - - endpoints.forEach((endpoint) => { - it(`should return 403 for ${endpoint.method.toUpperCase()} ${ - endpoint.url - }`, async () => { - const method = request(app)[endpoint.method]; - const res = await method(endpoint.url).set( - 'x-actual-token', - 'valid-token', - ); - expect(res.statusCode).toEqual(403); - expect(res.body.reason).toEqual('token-expired'); - }); - }); - }); - - describe('Unauthorized access', () => { - const sessionUserId = 'sessionUserId'; - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - }); - - afterEach(() => { - deleteUser(sessionUserId); - }); - - const endpoints = [ - { method: 'post', url: '/users' }, - { method: 'patch', url: '/users' }, - { method: 'post', url: '/users/delete-all' }, - ]; - - endpoints.forEach((endpoint) => { - it(`should return 401 for ${endpoint.method.toUpperCase()} ${ - endpoint.url - }`, async () => { - const method = request(app)[endpoint.method]; - const res = await method(endpoint.url).set( - 'x-actual-token', - 'valid-token', - ); - expect(res.statusCode).toEqual(401); - expect(res.body.reason).toEqual('unauthorized'); - expect(res.body.details).toEqual('permission-not-found'); - }); - }); - }); - - describe('File denied access', () => { - const sessionUserId = 'sessionUserId'; - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', BASIC_ROLE); - setSessionUser(sessionUserId); - }); - - afterEach(() => { - deleteUser(sessionUserId); - }); - - const endpoints = [ - { method: 'post', url: '/access' }, - { method: 'post', url: '/access/delete-all' }, - { method: 'get', url: '/access/available-users' }, - { method: 'post', url: '/access/transfer-ownership/' }, - { method: 'get', url: '/file/owner' }, - ]; - - endpoints.forEach((endpoint) => { - it(`should return 400 for ${endpoint.method.toUpperCase()} ${ - endpoint.url - }`, async () => { - const method = request(app)[endpoint.method]; - const res = await method(endpoint.url).set( - 'x-actual-token', - 'valid-token', - ); - expect(res.statusCode).toEqual(400); - expect(res.body.reason).toEqual('file-denied'); - }); - }); - }); - - describe('Invalid file ID', () => { - const sessionUserId = 'sessionUserId'; - beforeEach(() => { - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - setSessionUser(sessionUserId); - }); - - afterEach(() => { - deleteUser(sessionUserId); - }); - - const endpoints = [ - { method: 'post', url: '/access' }, - { method: 'post', url: '/access/delete-all' }, - { method: 'get', url: '/access/available-users' }, - { method: 'post', url: '/access/transfer-ownership/' }, - { method: 'get', url: '/file/owner' }, - { method: 'get', url: '/access/check-access' }, - ]; - - endpoints.forEach((endpoint) => { - it(`should return 400 for ${endpoint.method.toUpperCase()} ${ - endpoint.url - }`, async () => { - const method = request(app)[endpoint.method]; - const res = await method(endpoint.url).set( - 'x-actual-token', - 'valid-token', - ); - expect(res.statusCode).toEqual(400); - expect(res.body.reason).toEqual('invalid-file-id'); - }); }); }); }); diff --git a/src/app-sync.test.js b/src/app-sync.test.js index 8922c549c..eef87dd6e 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -6,23 +6,6 @@ import getAccountDb from './account-db.js'; import { SyncProtoBuf } from '@actual-app/crdt'; import crypto from 'node:crypto'; -const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; - -const createUser = (userId, userName, role, master = 0, enabled = 1) => { - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, master], - ); - getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, role], - ); -}; - -const setSessionUser = (userId) => { - getAccountDb().mutate('UPDATE sessions SET user_id = ?', [userId]); -}; - describe('/user-get-key', () => { it('returns 401 if the user is not authenticated', async () => { const res = await request(app).post('/user-get-key'); @@ -36,9 +19,6 @@ describe('/user-get-key', () => { }); it('returns encryption key details for a given fileId', async () => { - createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1); - setSessionUser('fileListAdminId'); - const fileId = crypto.randomBytes(16).toString('hex'); const encrypt_salt = 'test-salt'; const encrypt_keyid = 'test-key-id'; @@ -46,7 +26,7 @@ describe('/user-get-key', () => { getAccountDb().mutate( 'INSERT INTO files (id, encrypt_salt, encrypt_keyid, encrypt_test, owner) VALUES (?, ?, ?, ?, ?)', - [fileId, encrypt_salt, encrypt_keyid, encrypt_test, ''], + [fileId, encrypt_salt, encrypt_keyid, encrypt_test, 'genericAdmin'], ); const res = await request(app) @@ -108,12 +88,12 @@ describe('/reset-user-file', () => { // Use addMockFile to insert a mock file into the database getAccountDb().mutate( 'INSERT INTO files (id, group_id, deleted, owner) VALUES (?, ?, FALSE, ?)', - [fileId, groupId, ''], + [fileId, groupId, 'genericAdmin'], ); getAccountDb().mutate( 'INSERT INTO user_access (file_id, user_id) VALUES (?, ?)', - [fileId, ''], + [fileId, 'genericAdmin'], ); const res = await request(app) @@ -495,8 +475,6 @@ describe('/list-user-files', () => { }); it('returns a list of user files for an authenticated user', async () => { - createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1); - setSessionUser('fileListAdminId'); const fileId1 = crypto.randomBytes(16).toString('hex'); const fileId2 = crypto.randomBytes(16).toString('hex'); const fileName1 = 'file1.txt'; @@ -809,8 +787,8 @@ describe('/sync', () => { function addMockFile(fileId, groupId, keyId, encryptMeta, syncVersion) { getAccountDb().mutate( - 'INSERT INTO files (id, group_id, encrypt_keyid, encrypt_meta, sync_version) VALUES (?, ?, ?,?, ?)', - [fileId, groupId, keyId, encryptMeta, syncVersion], + 'INSERT INTO files (id, group_id, encrypt_keyid, encrypt_meta, sync_version, owner) VALUES (?, ?, ?,?, ?, ?)', + [fileId, groupId, keyId, encryptMeta, syncVersion, 'genericAdmin'], ); } From 5356b02653a10c3795521adce7dd567c72098c54 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 4 Sep 2024 16:19:27 -0300 Subject: [PATCH 054/139] added environment variables --- src/load-config.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/load-config.js b/src/load-config.js index b52a21be3..fb10e5df9 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -139,6 +139,43 @@ const finalConfig = { config.upload.fileSizeLimitMB, } : config.upload, + openId: + process.env.ACTUAL_OPENID_DISCOVERY_URL || + process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT + ? { + ...(process.env.ACTUAL_OPENID_DISCOVERY_URL + ? { + issuer: process.env.ACTUAL_OPENID_DISCOVERY_URL, + } + : process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT + ? { + issuer: { + name: process.env.ACTUAL_OPENID_PROVIDER_NAME, + authorization_endpoint: + process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, + token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, + userinfo_endpoint: + process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, + }, + } + : config.openId), + ...{ + client_id: process.env.ACTUAL_OPENID_CLIENT_ID + ? process.env.ACTUAL_OPENID_CLIENT_ID + : config.openId?.client_id, + }, + ...{ + client_secret: process.env.ACTUAL_OPENID_CLIENT_SECRET + ? process.env.ACTUAL_OPENID_CLIENT_SECRET + : config.openId?.client_secret, + }, + ...{ + server_hostname: process.env.ACTUAL_OPENID_SERVER_HOSTNAME + ? process.env.ACTUAL_OPENID_SERVER_HOSTNAME + : config.openId?.server_hostname, + }, + } + : config.openId, }; debug(`using port ${finalConfig.port}`); From f64bb2afba0e4c1523fa37540e7b32a0f346031b Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 09:14:37 -0300 Subject: [PATCH 055/139] linter --- src/account-db.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/account-db.js b/src/account-db.js index b46f3c622..aede9f59c 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -103,6 +103,10 @@ export async function bootstrap(loginSettings) { } export function isAdmin(userId) { + const user = + getAccountDb().first('SELECT master FROM users WHERE id = ?', [userId]) || + {}; + if (user?.master === 1) return true; return getUserPermissions(userId).some((value) => value === 'ADMINISTRATOR'); } From 2147edea958d776ac77c2007df280aa7bbd0ede8 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 10:34:40 -0300 Subject: [PATCH 056/139] enhancements --- src/account-db.js | 25 +++++++++++++------------ src/app-account.js | 31 +++++++++++++++++++++++++++++-- src/app-admin.js | 2 +- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index aede9f59c..f08cabe2f 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -55,19 +55,7 @@ export function getLoginMethod(req) { return config.loginMethod || 'password'; } -// Supported login settings: -// "password": "secret_password", -// "openid": { -// "issuer": "https://example.org", -// "client_id": "your_client_id", -// "client_secret": "your_client_secret", -// "server_hostname": "https://actual.your_website.com" -// } export async function bootstrap(loginSettings) { - if (!needsBootstrap()) { - return { error: 'already-bootstrapped' }; - } - const passEnabled = Object.prototype.hasOwnProperty.call( loginSettings, 'password', @@ -77,6 +65,19 @@ export async function bootstrap(loginSettings) { 'openid', ); + const { cnt } = + getAccountDb().first( + `SELECT count(*) as cnt + FROM users + WHERE users.user_name <> '' and users.master = 1`, + ) || {}; + + if (!openIdEnabled || (openIdEnabled && cnt > 0)) { + if (!needsBootstrap()) { + return { error: 'already-bootstrapped' }; + } + } + if (!passEnabled && !openIdEnabled) { return { error: 'no-auth-method-selected' }; } diff --git a/src/app-account.js b/src/app-account.js index 19bbf3b13..2c4e322d5 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -4,7 +4,7 @@ import { requestLoggerMiddleware, } from './util/middlewares.js'; import validateUser, { validateAuthHeader } from './util/validate-user.js'; -import { +import getAccountDb, { bootstrap, needsBootstrap, getLoginMethod, @@ -135,7 +135,34 @@ app.post('/enable-password', async (req, res) => { } }); -// +app.get('/openid-config', async (req, res) => { + const { cnt } = + getAccountDb().first( + `SELECT count(*) as cnt + FROM users + WHERE users.user_name <> '' and users.master = 1`, + ) || {}; + + if (cnt > 0) { + res.send({}); + return; + } + + const auth = + getAccountDb().first( + `SELECT * FROM auth + WHERE method = ?`, + ['openid'], + ) || {}; + + if (!auth) { + res.send({}); + return; + } + + res.send({ openId: JSON.parse(auth.extra_data) }); +}); + app.get('/login-openid/cb', async (req, res) => { let { error, url } = await loginWithOpenIdFinalize(req.query); if (error) { diff --git a/src/app-admin.js b/src/app-admin.js index a27c49fa2..61fe6b6d2 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -91,7 +91,7 @@ app.get('/masterCreated/', (req, res) => { getAccountDb().first( `SELECT count(*) as cnt FROM users - WHERE users.user_name <> ''`, + WHERE users.user_name <> '' and users.master = 1`, ) || {}; res.json(cnt > 0); From a1f1400ecf3d4610d67ac43595a14962623b517a Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 11:02:17 -0300 Subject: [PATCH 057/139] removed old files --- .../account/migrations/20000000_old_schema.sql | 17 ----------------- .../account/migrations/20230625_extend_auth.sql | 14 -------------- .../migrations/20230701_integrate_secretsdb.sql | 0 3 files changed, 31 deletions(-) delete mode 100644 src/sql/account/migrations/20000000_old_schema.sql delete mode 100644 src/sql/account/migrations/20230625_extend_auth.sql delete mode 100644 src/sql/account/migrations/20230701_integrate_secretsdb.sql diff --git a/src/sql/account/migrations/20000000_old_schema.sql b/src/sql/account/migrations/20000000_old_schema.sql deleted file mode 100644 index 608079677..000000000 --- a/src/sql/account/migrations/20000000_old_schema.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE auth - (password TEXT PRIMARY KEY); - -CREATE TABLE sessions - (token TEXT PRIMARY KEY); - -CREATE TABLE files - (id TEXT PRIMARY KEY, - group_id TEXT, - sync_version SMALLINT, - encrypt_meta TEXT, - encrypt_keyid TEXT, - encrypt_salt TEXT, - encrypt_test TEXT, - deleted BOOLEAN DEFAULT FALSE, - name TEXT); - diff --git a/src/sql/account/migrations/20230625_extend_auth.sql b/src/sql/account/migrations/20230625_extend_auth.sql deleted file mode 100644 index 1be5e8be8..000000000 --- a/src/sql/account/migrations/20230625_extend_auth.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE auth_new - (method TEXT PRIMARY KEY, - extra_data TEXT); - -INSERT INTO auth_new (method, extra_data) - SELECT 'password', password FROM auth; -DROP TABLE auth; -ALTER TABLE auth_new RENAME TO auth; - -CREATE TABLE pending_openid_requests - (state TEXT PRIMARY KEY, - code_verifier TEXT, - return_url TEXT, - expiry_time INTEGER); diff --git a/src/sql/account/migrations/20230701_integrate_secretsdb.sql b/src/sql/account/migrations/20230701_integrate_secretsdb.sql deleted file mode 100644 index e69de29bb..000000000 From 8924150c10a609e4de70b244b2e1e409f7413754 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 13:59:06 -0300 Subject: [PATCH 058/139] Added token expiration as environment variable --- src/load-config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/load-config.js b/src/load-config.js index fb10e5df9..6f2aee8f4 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -176,6 +176,9 @@ const finalConfig = { }, } : config.openId, + token_expiration: process.env.ACTUAL_TOKEN_EXPIRATION + ? process.env.ACTUAL_TOKEN_EXPIRATION + : config.token_expiration, }; debug(`using port ${finalConfig.port}`); From d84b867ca39a9d1771d7fb626df78363682e4cc1 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 17:15:39 -0300 Subject: [PATCH 059/139] fixes --- src/app-admin.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index 61fe6b6d2..5192361ff 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -337,7 +337,7 @@ app.post('/access/delete-all', (req, res) => { } }); -app.get('/access/available-users', async (req, res) => { +app.get('/access/users', async (req, res) => { const fileId = req.query.fileId; const session = validateUser(req, res); if (!session) return; @@ -345,16 +345,14 @@ app.get('/access/available-users', async (req, res) => { if (!checkFilePermission(fileId, session.user_id, res)) return; const users = getAccountDb().all( - `SELECT users.id as userId, user_name as userName, display_name as displayName + `SELECT users.id as userId, user_name as userName, display_name as displayName, + CASE WHEN user_access.file_id IS NULL THEN 0 ELSE 1 END as haveAccess, + CASE WHEN files.id IS NULL THEN 0 ELSE 1 END as owner FROM users - WHERE NOT EXISTS (SELECT 1 - FROM user_access - WHERE user_access.file_id = ? and user_access.user_id = users.id) - AND NOT EXISTS (SELECT 1 - FROM files - WHERE files.id = ? AND files.owner = users.id) - AND users.enabled = 1 AND users.user_name <> '' AND users.id <> ?`, - [fileId, fileId, session.user_id], + LEFT JOIN user_access ON user_access.file_id = ? and user_access.user_id = users.id + LEFT JOIN files ON files.id = ? and files.owner = users.id + WHERE users.enabled = 1 AND users.user_name <> ''`, + [fileId, fileId], ); res.json(users); }); From b472a7f00dd52a1fa00128f14b533fc850acc8a7 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 17:22:52 -0300 Subject: [PATCH 060/139] typescript fix --- src/account-db.js | 3 ++- src/accounts/openid.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index f08cabe2f..e233213f5 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -216,7 +216,8 @@ export function login(password) { let expiration = TOKEN_EXPIRATION_NEVER; if ( config.token_expiration != 'never' && - config.token_expiration != 'openid-provider' + config.token_expiration != 'openid-provider' && + typeof config.token_expiration === 'number' ) { expiration = Math.floor(Date.now() / 1000) + config.token_expiration * 60; } diff --git a/src/accounts/openid.js b/src/accounts/openid.js index dab29d950..26d9513f7 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -202,7 +202,8 @@ export async function loginWithOpenIdFinalize(body) { let expiration = TOKEN_EXPIRATION_NEVER; if (finalConfig.token_expiration == 'openid-provider') { expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; - } else if (finalConfig.token_expiration != 'never') { + } else if (finalConfig.token_expiration != 'never' && + typeof finalConfig.token_expiration === 'number') { expiration = Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; } From e7d9aa19a99468a75a068a4a31f76e48465d2674 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 5 Sep 2024 17:25:09 -0300 Subject: [PATCH 061/139] linter --- src/accounts/openid.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 26d9513f7..b4e383f97 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -202,8 +202,10 @@ export async function loginWithOpenIdFinalize(body) { let expiration = TOKEN_EXPIRATION_NEVER; if (finalConfig.token_expiration == 'openid-provider') { expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; - } else if (finalConfig.token_expiration != 'never' && - typeof finalConfig.token_expiration === 'number') { + } else if ( + finalConfig.token_expiration != 'never' && + typeof finalConfig.token_expiration === 'number' + ) { expiration = Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; } From fae869b19eb96ebe2b317dc150494fd137da614f Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 3 Oct 2024 16:54:57 -0300 Subject: [PATCH 062/139] unwanted code --- src/config-types.ts | 1 - src/load-config.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/config-types.ts b/src/config-types.ts index 907a00d00..8db5237d6 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -33,7 +33,6 @@ export interface Config { client_secret: string; server_hostname: string; }; - password?: string; multiuser: boolean; token_expiration?: 'never' | 'openid-provider' | number; } diff --git a/src/load-config.js b/src/load-config.js index 6f2aee8f4..c436a604d 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -71,7 +71,6 @@ let defaultConfig = { fileSizeLimitMB: 20, }, projectRoot, - password: '', multiuser: false, token_expiration: 'never', }; From 66c8e31577edf958fd3351f37604bb22959fb41b Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 08:05:41 -0300 Subject: [PATCH 063/139] changed master to owner --- jest.global-setup.js | 6 +++--- migrations/1719409568000-multiuser.js | 2 +- src/account-db.js | 8 ++++---- src/accounts/openid.js | 2 +- src/app-account.js | 2 +- src/app-admin.js | 20 ++++++++++---------- src/app-admin.test.js | 16 ++++++++-------- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 83148dc60..5a7bc6092 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -1,10 +1,10 @@ import getAccountDb from './src/account-db.js'; import runMigrations from './src/migrations.js'; -const createUser = (userId, userName, role, master = 0, enabled = 1) => { +const createUser = (userId, userName, role, owner = 0, enabled = 1) => { getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, master], + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner], ); getAccountDb().mutate( 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 8cec2372b..dccc42fa9 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -7,7 +7,7 @@ export const up = async function () { user_name TEXT, display_name TEXT, enabled INTEGER, - master INTEGER); + owner INTEGER); CREATE TABLE roles (id TEXT PRIMARY KEY, diff --git a/src/account-db.js b/src/account-db.js index e233213f5..33dfe3df5 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -69,7 +69,7 @@ export async function bootstrap(loginSettings) { getAccountDb().first( `SELECT count(*) as cnt FROM users - WHERE users.user_name <> '' and users.master = 1`, + WHERE users.user_name <> '' and users.owner = 1`, ) || {}; if (!openIdEnabled || (openIdEnabled && cnt > 0)) { @@ -105,9 +105,9 @@ export async function bootstrap(loginSettings) { export function isAdmin(userId) { const user = - getAccountDb().first('SELECT master FROM users WHERE id = ?', [userId]) || + getAccountDb().first('SELECT owner FROM users WHERE id = ?', [userId]) || {}; - if (user?.master === 1) return true; + if (user?.owner === 1) return true; return getUserPermissions(userId).some((value) => value === 'ADMINISTRATOR'); } @@ -196,7 +196,7 @@ export function login(password) { if (c === 0) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, 1, 1)', + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', [userId, '', ''], ); diff --git a/src/accounts/openid.js b/src/accounts/openid.js index b4e383f97..135b15e32 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -165,7 +165,7 @@ export async function loginWithOpenIdFinalize(body) { if (c === 0) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, 1, 1)', + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', [userId, identity, userInfo.name ?? userInfo.email ?? identity], ); diff --git a/src/app-account.js b/src/app-account.js index 2c4e322d5..66ec367b3 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -140,7 +140,7 @@ app.get('/openid-config', async (req, res) => { getAccountDb().first( `SELECT count(*) as cnt FROM users - WHERE users.user_name <> '' and users.master = 1`, + WHERE users.user_name <> '' and users.owner = 1`, ) || {}; if (cnt > 0) { diff --git a/src/app-admin.js b/src/app-admin.js index 5192361ff..ff8d5aa36 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -86,12 +86,12 @@ const validateUserInput = (res, user) => { return true; }; -app.get('/masterCreated/', (req, res) => { +app.get('/ownerCreated/', (req, res) => { const { cnt } = getAccountDb().first( `SELECT count(*) as cnt FROM users - WHERE users.user_name <> '' and users.master = 1`, + WHERE users.user_name <> '' and users.owner = 1`, ) || {}; res.json(cnt > 0); @@ -102,7 +102,7 @@ app.get('/users/', (req, res) => { if (!session) return; const users = getAccountDb().all( - `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(master,0) as master, roles.id as role + `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role FROM users JOIN user_roles ON user_roles.user_id = users.id JOIN roles ON roles.id = user_roles.role_id @@ -112,7 +112,7 @@ app.get('/users/', (req, res) => { res.json( users.map((u) => ({ ...u, - master: u.master === 1, + owner: u.owner === 1, enabled: u.enabled === 1, })), ); @@ -141,7 +141,7 @@ app.post('/users', async (req, res) => { let enabled = newUser.enabled ? 1 : 0; getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, 0)', + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, 0)', [userId, newUser.userName, displayName, enabled], ); @@ -197,19 +197,19 @@ app.post('/users/delete-all', async (req, res) => { const ids = req.body.ids; let totalDeleted = 0; ids.forEach((item) => { - const { id: masterId } = - getAccountDb().first('SELECT id FROM users WHERE master = 1') || {}; + const { id: ownerId } = + getAccountDb().first('SELECT id FROM users WHERE owner = 1') || {}; - if (item === masterId) return; + if (item === ownerId) return; getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [item]); getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]); getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ - masterId, + ownerId, item, ]); const usersDeleted = getAccountDb().mutate( - 'DELETE FROM users WHERE id = ? and master = 0', + 'DELETE FROM users WHERE id = ? and owner = 0', [item], ).changes; totalDeleted += usersDeleted; diff --git a/src/app-admin.test.js b/src/app-admin.test.js index d9defaba6..dd54c88fc 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -6,10 +6,10 @@ import { v4 as uuidv4 } from 'uuid'; const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; const BASIC_ROLE = 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc'; -const createUser = (userId, userName, role, master = 0, enabled = 1) => { +const createUser = (userId, userName, role, owner = 0, enabled = 1) => { getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, master) VALUES (?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, master], + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner], ); getAccountDb().mutate( 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', @@ -32,15 +32,15 @@ const createSession = (userId, sessionToken) => { const generateSessionToken = () => `token-${uuidv4()}`; describe('/admin', () => { - describe('/masterCreated', () => { - it('should return 200 and true if a master user is created', async () => { + describe('/ownerCreated', () => { + it('should return 200 and true if a owner user is created', async () => { const sessionToken = generateSessionToken(); const adminId = uuidv4(); createUser(adminId, 'admin', ADMIN_ROLE, 1); createSession(adminId, sessionToken); const res = await request(app) - .get('/masterCreated') + .get('/ownerCreated') .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); @@ -96,7 +96,7 @@ describe('/admin', () => { userName: 'user1', displayName: 'User One', enabled: 1, - master: 0, + owner: 0, role: BASIC_ROLE, }; @@ -115,7 +115,7 @@ describe('/admin', () => { userName: 'user1', displayName: 'User One', enabled: 1, - master: 0, + owner: 0, role: BASIC_ROLE, }; From 5514ec62b7f2bb298e3708689c8cf9f40174a1c7 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 08:20:10 -0300 Subject: [PATCH 064/139] fixed down migrations and added transactions to it --- migrations/1718889148000-openid.js | 34 ++++++++------ migrations/1719409568000-multiuser.js | 65 +++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/migrations/1718889148000-openid.js b/migrations/1718889148000-openid.js index 8f14f2c90..b170aea05 100644 --- a/migrations/1718889148000-openid.js +++ b/migrations/1718889148000-openid.js @@ -2,34 +2,40 @@ import getAccountDb from '../src/account-db.js'; export const up = async function () { await getAccountDb().exec( - `CREATE TABLE auth_new + ` + BEGIN TRANSACTION; + CREATE TABLE auth_new (method TEXT PRIMARY KEY, display_name TEXT, extra_data TEXT, active INTEGER); - INSERT INTO auth_new (method, display_name, extra_data, active) - SELECT 'password', 'Password', password, 1 FROM auth; + INSERT INTO auth_new (method, display_name, extra_data, active) + SELECT 'password', 'Password', password, 1 FROM auth; DROP TABLE auth; ALTER TABLE auth_new RENAME TO auth; - CREATE TABLE pending_openid_requests - (state TEXT PRIMARY KEY, - code_verifier TEXT, - return_url TEXT, - expiry_time INTEGER);`, + CREATE TABLE pending_openid_requests + (state TEXT PRIMARY KEY, + code_verifier TEXT, + return_url TEXT, + expiry_time INTEGER); + COMMIT;`, ); }; export const down = async function () { await getAccountDb().exec( ` - CREATE TABLE auth_new + BEGIN TRANSACTION; + ALTER TABLE auth RENAME TO auth_temp; + CREATE TABLE auth (password TEXT); - INSERT INTO auth_new (password) - SELECT extra_data FROM auth; - DROP TABLE auth; - ALTER TABLE auth_new RENAME TO auth; - DROP TABLE pending_openid_requests; + INSERT INTO auth (password) + SELECT extra_data FROM auth_temp WHERE method = 'password'; + DROP TABLE auth_temp; + + DROP TABLE pending_openid_requests; + COMMIT; `, ); }; diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index dccc42fa9..4e3a038c2 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -2,7 +2,10 @@ import getAccountDb from '../src/account-db.js'; export const up = async function () { await getAccountDb().exec( - `CREATE TABLE users + ` + BEGIN TRANSACTION; + + CREATE TABLE users (id TEXT PRIMARY KEY, user_name TEXT, display_name TEXT, @@ -38,6 +41,7 @@ export const up = async function () { ALTER TABLE sessions ADD auth_method TEXT; + COMMIT; `, ); }; @@ -45,9 +49,62 @@ export const up = async function () { export const down = async function () { await getAccountDb().exec( ` - DROP TABLE users; - DROP TABLE roles; - DROP TABLE user_roles; + BEGIN TRANSACTION; + + CREATE TABLE sessions_backup ( + token TEXT PRIMARY KEY + ); + + INSERT INTO sessions_backup (token) + SELECT token FROM sessions; + + ALTER TABLE sessions_backup RENAME TO sessions; + + CREATE TABLE files_backup ( + id TEXT PRIMARY KEY, + group_id TEXT, + sync_version SMALLINT, + encrypt_meta TEXT, + encrypt_keyid TEXT, + encrypt_salt TEXT, + encrypt_test TEXT, + deleted BOOLEAN DEFAULT FALSE, + name TEXT + ); + + INSERT INTO files_backup ( + id, + group_id, + sync_version, + encrypt_meta, + encrypt_keyid, + encrypt_salt, + encrypt_test, + deleted, + name + ) + SELECT + id, + group_id, + sync_version, + encrypt_meta, + encrypt_keyid, + encrypt_salt, + encrypt_test, + deleted, + name + FROM files; + + DROP TABLE files; + + ALTER TABLE files_backup RENAME TO files; + + DROP TABLE IF EXISTS user_access; + DROP TABLE IF EXISTS user_roles; + DROP TABLE IF EXISTS roles; + DROP TABLE IF EXISTS users; + + COMMIT; `, ); }; From 7a6f06db4a6f52e4fb5cb26399dee9261d64eadc Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 08:24:39 -0300 Subject: [PATCH 065/139] changed to the 'in' operator --- src/accounts/openid.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 135b15e32..957365dbc 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -5,16 +5,16 @@ import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; import finalConfig from '../load-config.js'; export async function bootstrapOpenId(config) { - if (!Object.prototype.hasOwnProperty.call(config, 'issuer')) { + if (!('issuer' in config)) { return { error: 'missing-issuer' }; } - if (!Object.prototype.hasOwnProperty.call(config, 'client_id')) { + if (!('client_id' in config)) { return { error: 'missing-client-id' }; } - if (!Object.prototype.hasOwnProperty.call(config, 'client_secret')) { + if (!('client_secret' in config)) { return { error: 'missing-client-secret' }; } - if (!Object.prototype.hasOwnProperty.call(config, 'server_hostname')) { + if (!('server_hostname' in config)) { return { error: 'missing-server-hostname' }; } @@ -47,16 +47,17 @@ async function setupOpenIdClient(config) { typeof config.issuer === 'string' ? await Issuer.discover(config.issuer) : new Issuer({ - issuer: config.issuer.name, - authorization_endpoint: config.issuer.authorization_endpoint, - token_endpoint: config.issuer.token_endpoint, - userinfo_endpoint: config.issuer.userinfo_endpoint, - }); + issuer: config.issuer.name, + authorization_endpoint: config.issuer.authorization_endpoint, + token_endpoint: config.issuer.token_endpoint, + userinfo_endpoint: config.issuer.userinfo_endpoint, + }); const client = new issuer.Client({ client_id: config.client_id, client_secret: config.client_secret, redirect_uri: config.server_hostname + '/account/login-openid/cb', + validate_id_token: true, }); return client; From 4d8102d248deb12d98eb9e2510a178bdafcfac3e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 08:52:48 -0300 Subject: [PATCH 066/139] fixed typo --- src/account-db.js | 20 +++++++------------- src/app.js | 4 ++-- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 33dfe3df5..086863cd1 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -56,23 +56,17 @@ export function getLoginMethod(req) { } export async function bootstrap(loginSettings) { - const passEnabled = Object.prototype.hasOwnProperty.call( - loginSettings, - 'password', - ); - const openIdEnabled = Object.prototype.hasOwnProperty.call( - loginSettings, - 'openid', - ); + const passEnabled = 'password' in loginSettings; + const openIdEnabled = 'openid' in loginSettings; - const { cnt } = + const { countOfOwner } = getAccountDb().first( - `SELECT count(*) as cnt + `SELECT count(*) as countOfOwner FROM users WHERE users.user_name <> '' and users.owner = 1`, ) || {}; - if (!openIdEnabled || (openIdEnabled && cnt > 0)) { + if (!openIdEnabled || countOfOwner > 0) { if (!needsBootstrap()) { return { error: 'already-bootstrapped' }; } @@ -94,7 +88,7 @@ export async function bootstrap(loginSettings) { } if (openIdEnabled) { - let { error } = await bootstrapOpenId(loginSettings.openid); + let { error } = await bootstrapOpenId(loginSettings.openId); if (error) { return { error }; } @@ -297,7 +291,7 @@ export function clearExpiredSessions() { console.log(`Deleted ${deletedSessions} old sessions`); } -export async function toogleAuthentication() { +export async function toggleAuthentication() { if (config.loginMethod === 'openid') { const { cnt } = getAccountDb().first( 'SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1', diff --git a/src/app.js b/src/app.js index a6df225fe..7afd6984e 100644 --- a/src/app.js +++ b/src/app.js @@ -12,7 +12,7 @@ import * as goCardlessApp from './app-gocardless/app-gocardless.js'; import * as simpleFinApp from './app-simplefin/app-simplefin.js'; import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; -import { toogleAuthentication } from './account-db.js'; +import { toggleAuthentication } from './account-db.js'; import { exit } from 'node:process'; const app = express(); @@ -89,7 +89,7 @@ export default async function run() { app.listen(config.port, config.hostname); } - if (!(await toogleAuthentication())) exit(-1); + if (!(await toggleAuthentication())) exit(-1); console.log('Listening on ' + config.hostname + ':' + config.port + '...'); } From 83cd13f68c1bf173ef60c60200899272b393649f Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 09:03:26 -0300 Subject: [PATCH 067/139] code review --- migrations/1719409568000-multiuser.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 4e3a038c2..347b3143f 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -9,8 +9,8 @@ export const up = async function () { (id TEXT PRIMARY KEY, user_name TEXT, display_name TEXT, - enabled INTEGER, - owner INTEGER); + enabled INTEGER NOT NULL DEFAULT 1, + owner INTEGER NOT NULL DEFAULT 0); CREATE TABLE roles (id TEXT PRIMARY KEY, @@ -22,11 +22,17 @@ export const up = async function () { CREATE TABLE user_roles (user_id TEXT, - role_id TEXT); + role_id TEXT, + , FOREIGN KEY (user_id) REFERENCES users(id) + , FOREIGN KEY (role_id) REFERENCES roles(id) + ); CREATE TABLE user_access (user_id TEXT, - file_id TEXT); + file_id TEXT, + , FOREIGN KEY (user_id) REFERENCES users(id) + , FOREIGN KEY (file_id) REFERENCES files(id) + ); ALTER TABLE files ADD COLUMN owner TEXT; From 157801aad63661d750a5fa6adcd97ae2333b1b0e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 09:07:39 -0300 Subject: [PATCH 068/139] code review --- src/account-db.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 086863cd1..841bc988b 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -102,7 +102,7 @@ export function isAdmin(userId) { getAccountDb().first('SELECT owner FROM users WHERE id = ?', [userId]) || {}; if (user?.owner === 1) return true; - return getUserPermissions(userId).some((value) => value === 'ADMINISTRATOR'); + return hasPermission(userId, 'ADMINISTRATOR'); } export function hasPermission(userId, permission) { @@ -143,9 +143,7 @@ export async function disableOpenID( } if (passwordHash) { - let confirmed = - passwordHash && - bcrypt.compareSync(loginSettings.password, passwordHash); + let confirmed = bcrypt.compareSync(loginSettings.password, passwordHash); if (!confirmed) { return { error: 'invalid-password' }; @@ -185,18 +183,20 @@ export function login(password) { let token = sessionRow ? sessionRow.token : uuid.v4(); - let { c } = accountDb.first('SELECT count(*) as c FROM users'); + let { totalOfUsers } = accountDb.first('SELECT count(*) as totalOfUsers FROM users'); let userId = null; - if (c === 0) { + if (totalOfUsers === 0) { userId = uuid.v4(); accountDb.mutate( 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', [userId, '', ''], ); + const { id: adminRoleId } = accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']); + accountDb.mutate( 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], + [userId, adminRoleId], ); } else { let { id: userIdFromDb } = accountDb.first( From b26448de33a5fc485efeb61d8a758612f314aecb Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 09:07:54 -0300 Subject: [PATCH 069/139] json.parse may fail --- src/accounts/openid.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 957365dbc..c82f41669 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -75,7 +75,12 @@ export async function loginWithOpenIdSetup(body) { if (!config) { return { error: 'openid-not-configured' }; } - config = JSON.parse(config['extra_data']); + + try { + config = JSON.parse(config['extra_data']); + } catch (err) { + return { error: 'openid-setup-failed: ' + err }; + } let client; try { @@ -126,8 +131,12 @@ export async function loginWithOpenIdFinalize(body) { if (!config) { return { error: 'openid-not-configured' }; } - config = JSON.parse(config['extra_data']); - + try { + config = JSON.parse(config['extra_data']); + } catch (err) { + + return { error: 'openid-setup-failed: ' + err }; + } let client; try { client = await setupOpenIdClient(config); From 0505b0e13ba61062ce22233279b9290c59d33728 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 09:34:52 -0300 Subject: [PATCH 070/139] code review and removed duplicated methods --- src/account-db.js | 93 ---------------------------------------- src/accounts/openid.js | 47 ++++++++++++-------- src/accounts/password.js | 82 +++++++++++++++++++++++++++++------ src/app-account.js | 7 ++- 4 files changed, 100 insertions(+), 129 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 841bc988b..eb88a3b10 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -1,11 +1,9 @@ import { join } from 'node:path'; import openDatabase from './db.js'; import config from './load-config.js'; -import * as uuid from 'uuid'; import * as bcrypt from 'bcrypt'; import { bootstrapPassword } from './accounts/password.js'; import { bootstrapOpenId } from './accounts/openid.js'; -import { TOKEN_EXPIRATION_NEVER } from './app-admin.js'; let _accountDb; @@ -18,10 +16,6 @@ export default function getAccountDb() { return _accountDb; } -function hashPassword(password) { - return bcrypt.hashSync(password, 12); -} - export function needsBootstrap() { let accountDb = getAccountDb(); let rows = accountDb.all('SELECT * FROM auth'); @@ -162,93 +156,6 @@ export async function disableOpenID( getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); } -export function login(password) { - if (password === undefined || password === '') { - return { error: 'invalid-password' }; - } - - let accountDb = getAccountDb(); - const { extra_data: passwordHash } = - accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ - 'password', - ]) || {}; - - let confirmed = passwordHash && bcrypt.compareSync(password, passwordHash); - - if (!confirmed) { - return { error: 'invalid-password' }; - } - - let sessionRow = accountDb.first('SELECT * FROM sessions'); - - let token = sessionRow ? sessionRow.token : uuid.v4(); - - let { totalOfUsers } = accountDb.first('SELECT count(*) as totalOfUsers FROM users'); - let userId = null; - if (totalOfUsers === 0) { - userId = uuid.v4(); - accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', - [userId, '', ''], - ); - - const { id: adminRoleId } = accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']); - - accountDb.mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, adminRoleId], - ); - } else { - let { id: userIdFromDb } = accountDb.first( - 'SELECT id FROM users WHERE user_name = ?', - [''], - ); - - userId = userIdFromDb; - } - - let expiration = TOKEN_EXPIRATION_NEVER; - if ( - config.token_expiration != 'never' && - config.token_expiration != 'openid-provider' && - typeof config.token_expiration === 'number' - ) { - expiration = Math.floor(Date.now() / 1000) + config.token_expiration * 60; - } - - if (!sessionRow) { - accountDb.mutate( - 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', - [token, expiration, userId, 'password'], - ); - } else { - accountDb.mutate( - 'UPDATE sessions SET user_id = ?, expires_at = ? WHERE token = ?', - [userId, expiration, token], - ); - } - - return { token }; -} - -export function changePassword(newPassword) { - if (newPassword === undefined || newPassword === '') { - return { error: 'invalid-password' }; - } - - let accountDb = getAccountDb(); - - let hashed = hashPassword(newPassword); - let token = uuid.v4(); - - // Note that this doesn't have a WHERE. This table only ever has 1 - // row (maybe that will change in the future? if so this will not work) - accountDb.mutate('UPDATE auth SET password = ?', [hashed]); - accountDb.mutate('UPDATE sessions SET token = ?', [token]); - - return {}; -} - export function getSession(token) { let accountDb = getAccountDb(); return accountDb.first('SELECT * FROM sessions WHERE token = ?', [token]); diff --git a/src/accounts/openid.js b/src/accounts/openid.js index c82f41669..649a5522e 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -47,11 +47,11 @@ async function setupOpenIdClient(config) { typeof config.issuer === 'string' ? await Issuer.discover(config.issuer) : new Issuer({ - issuer: config.issuer.name, - authorization_endpoint: config.issuer.authorization_endpoint, - token_endpoint: config.issuer.token_endpoint, - userinfo_endpoint: config.issuer.userinfo_endpoint, - }); + issuer: config.issuer.name, + authorization_endpoint: config.issuer.authorization_endpoint, + token_endpoint: config.issuer.token_endpoint, + userinfo_endpoint: config.issuer.userinfo_endpoint, + }); const client = new issuer.Client({ client_id: config.client_id, @@ -134,7 +134,6 @@ export async function loginWithOpenIdFinalize(body) { try { config = JSON.parse(config['extra_data']); } catch (err) { - return { error: 'openid-setup-failed: ' + err }; } let client; @@ -144,11 +143,17 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-setup-failed: ' + err }; } - let { code_verifier, return_url } = accountDb.first( + let pendingRequest = accountDb.first( 'SELECT code_verifier, return_url FROM pending_openid_requests WHERE state = ? AND expiry_time > ?', [body.state, Date.now()], ); + if (!pendingRequest) { + return { error: 'invalid-or-expired-state' }; + } + + let { code_verifier, return_url } = pendingRequest; + try { let grant = await client.grant({ grant_type: 'authorization_code', @@ -179,9 +184,16 @@ export async function loginWithOpenIdFinalize(body) { [userId, identity, userInfo.name ?? userInfo.email ?? identity], ); + const { id: adminRoleId } = + accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']) || {}; + + if (!adminRoleId) { + return { error: 'administrator-role-not-found' }; + } + accountDb.mutate( 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, '213733c1-5645-46ad-8784-a7b20b400f93'], + [userId, adminRoleId], ); } else { let { id: userIdFromDb, display_name: displayName } = @@ -191,10 +203,7 @@ export async function loginWithOpenIdFinalize(body) { ) || {}; if (userIdFromDb == null) { - return { - error: - 'openid-grant-failed: user does not have access to Actual Budget', - }; + return { error: 'openid-grant-failed' }; } if (!displayName && userInfo.name) { @@ -209,15 +218,16 @@ export async function loginWithOpenIdFinalize(body) { const token = uuid.v4(); - let expiration = TOKEN_EXPIRATION_NEVER; - if (finalConfig.token_expiration == 'openid-provider') { + let expiration; + if (finalConfig.token_expiration === 'openid-provider') { expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; - } else if ( - finalConfig.token_expiration != 'never' && - typeof finalConfig.token_expiration === 'number' - ) { + } else if (finalConfig.token_expiration === 'never') { + expiration = TOKEN_EXPIRATION_NEVER; + } else if (typeof finalConfig.token_expiration === 'number') { expiration = Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; + } else { + expiration = TOKEN_EXPIRATION_NEVER; } accountDb.mutate( @@ -229,6 +239,7 @@ export async function loginWithOpenIdFinalize(body) { return { url: `${return_url}/openid-cb?token=${token}` }; } catch (err) { + console.error('OpenID grant failed:', err); return { error: 'openid-grant-failed: ' + err }; } } diff --git a/src/accounts/password.js b/src/accounts/password.js index bc19b5d30..1d455225d 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -1,20 +1,20 @@ import * as bcrypt from 'bcrypt'; import getAccountDb, { clearExpiredSessions } from '../account-db.js'; +import * as uuid from 'uuid'; +import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; +import finalConfig from '../load-config.js'; function hashPassword(password) { return bcrypt.hashSync(password, 12); } export function bootstrapPassword(password) { - if (!password || password === null || password === '') { + if (!password) { return { error: 'invalid-password' }; } getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['password']); - // Hash the password. There's really not a strong need for this - // since this is a self-hosted instance owned by the user. - // However, just in case we do it. let hashed = hashPassword(password); let accountDb = getAccountDb(); accountDb.mutate('UPDATE auth SET active = 0'); @@ -27,24 +27,78 @@ export function bootstrapPassword(password) { } export function loginWithPassword(password) { + if (password === undefined || password === '') { + return { error: 'invalid-password' }; + } + let accountDb = getAccountDb(); - let row = accountDb.first( - "SELECT extra_data FROM auth WHERE method = 'password'", - ); - let confirmed = row && bcrypt.compareSync(password, row.extra_data); + const { extra_data: passwordHash } = + accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ + 'password', + ]) || {}; + + let confirmed = passwordHash && bcrypt.compareSync(password, passwordHash); + + if (!confirmed) { + return { error: 'invalid-password' }; + } + + let sessionRow = accountDb.first('SELECT * FROM sessions'); + + let token = sessionRow ? sessionRow.token : uuid.v4(); + + let { totalOfUsers } = accountDb.first('SELECT count(*) as totalOfUsers FROM users'); + let userId = null; + if (totalOfUsers === 0) { + userId = uuid.v4(); + accountDb.mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', + [userId, '', ''], + ); - if (confirmed) { - let row = accountDb.first( - 'SELECT token FROM sessions WHERE user_name = ?', + const { id: adminRoleId } = accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']) || {}; + + if (!adminRoleId) { + return { error: 'administrator-role-not-found' }; + } + + accountDb.mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, adminRoleId], + ); + } else { + let { id: userIdFromDb } = accountDb.first( + 'SELECT id FROM users WHERE user_name = ?', [''], ); - clearExpiredSessions(); + userId = userIdFromDb; + } - return row.token; + let expiration = TOKEN_EXPIRATION_NEVER; + if ( + finalConfig.token_expiration != 'never' && + finalConfig.token_expiration != 'openid-provider' && + typeof finalConfig.token_expiration === 'number' + ) { + expiration = Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; + } + + if (!sessionRow) { + accountDb.mutate( + 'INSERT INTO sessions (token, expires_at, user_id, auth_method) VALUES (?, ?, ?, ?)', + [token, expiration, userId, 'password'], + ); } else { - return null; + accountDb.mutate( + 'UPDATE sessions SET user_id = ?, expires_at = ? WHERE token = ?', + [userId, expiration, token], + ); } + + clearExpiredSessions(); + + return { token }; } export function changePassword(newPassword) { diff --git a/src/app-account.js b/src/app-account.js index 66ec367b3..66e4c5c36 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -9,13 +9,12 @@ import getAccountDb, { needsBootstrap, getLoginMethod, listLoginMethods, - login, enableOpenID, disableOpenID, getUserInfo, getUserPermissions, } from './account-db.js'; -import { changePassword } from './accounts/password.js'; +import { changePassword, loginWithPassword } from './accounts/password.js'; import { loginWithOpenIdSetup, loginWithOpenIdFinalize, @@ -74,7 +73,7 @@ app.post('/login', async (req, res) => { return; } else { if (validateAuthHeader(req)) { - tokenRes = login(headerVal); + tokenRes = loginWithPassword(headerVal); } else { res.send({ status: 'error', reason: 'proxy-not-trusted' }); return; @@ -94,7 +93,7 @@ app.post('/login', async (req, res) => { case 'password': default: - tokenRes = login(req.body.password); + tokenRes = loginWithPassword(req.body.password); break; } let { error, token } = tokenRes; From cccd9950edefbed6f0be4d2ea4724ac936dcdb54 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 10:43:04 -0300 Subject: [PATCH 071/139] multiple fixes and refactories --- src/app-account.js | 63 ++++++----- src/app-admin.js | 153 ++++++++++++++------------- src/app-gocardless/app-gocardless.js | 4 +- src/app-secrets.js | 6 +- src/app-sync.js | 4 +- src/util/middlewares.js | 8 +- src/util/validate-user.js | 4 +- 7 files changed, 128 insertions(+), 114 deletions(-) diff --git a/src/app-account.js b/src/app-account.js index 66e4c5c36..a2f15549e 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -2,8 +2,9 @@ import express from 'express'; import { errorMiddleware, requestLoggerMiddleware, + validateSessionMiddleware, } from './util/middlewares.js'; -import validateUser, { validateAuthHeader } from './util/validate-user.js'; +import validateSession, { validateAuthHeader } from './util/validate-user.js'; import getAccountDb, { bootstrap, needsBootstrap, @@ -13,18 +14,17 @@ import getAccountDb, { disableOpenID, getUserInfo, getUserPermissions, + isAdmin, } from './account-db.js'; import { changePassword, loginWithPassword } from './accounts/password.js'; import { loginWithOpenIdSetup, loginWithOpenIdFinalize, } from './accounts/openid.js'; -import { getAdminSessionFromRequest } from './app-admin.js'; -import bodyParser from 'body-parser'; let app = express(); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); app.use(errorMiddleware); app.use(requestLoggerMiddleware); export { app as handlers }; @@ -48,9 +48,8 @@ app.post('/bootstrap', async (req, res) => { if (error) { res.status(400).send({ status: 'error', reason: error }); return; - } else { - res.send({ status: 'ok' }); } + res.send({ status: 'ok' }); }); app.get('/login-methods', (req, res) => { @@ -84,7 +83,7 @@ app.post('/login', async (req, res) => { case 'openid': { let { error, url } = await loginWithOpenIdSetup(req.body); if (error) { - res.send({ status: 'error', reason: error }); + res.status(400).send({ status: 'error', reason: error }); return; } res.send({ status: 'ok', data: { redirect_url: url } }); @@ -106,44 +105,54 @@ app.post('/login', async (req, res) => { res.send({ status: 'ok', data: { token } }); }); -app.post('/enable-openid', async (req, res) => { - const session = await getAdminSessionFromRequest(req, res); - if (!session) return; +app.post('/enable-openid', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } let { error } = (await enableOpenID(req.body)) || {}; if (error) { res.status(400).send({ status: 'error', reason: error }); return; - } else { - res.send({ status: 'ok' }); } + res.send({ status: 'ok' }); }); -app.post('/enable-password', async (req, res) => { - const session = await getAdminSessionFromRequest(req, res); - if (!session) return; +app.post('/enable-password', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } let { error } = (await disableOpenID(req.body, true, true)) || {}; if (error) { res.status(400).send({ status: 'error', reason: error }); return; - } else { - res.send({ status: 'ok' }); } + res.send({ status: 'ok' }); }); app.get('/openid-config', async (req, res) => { - const { cnt } = + const { ownerCount } = getAccountDb().first( - `SELECT count(*) as cnt + `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, ) || {}; - if (cnt > 0) { - res.send({}); + if (ownerCount > 0) { + res.status(400).send({ status: 'error', reason: 'already-bootstraped' }); return; } @@ -155,7 +164,7 @@ app.get('/openid-config', async (req, res) => { ) || {}; if (!auth) { - res.send({}); + res.status(500); return; } @@ -173,13 +182,13 @@ app.get('/login-openid/cb', async (req, res) => { }); app.post('/change-password', (req, res) => { - let user = validateUser(req, res); - if (!user) return; + let session = validateSession(req, res); + if (!session) return; let { error } = changePassword(req.body.password); if (error) { - res.send({ status: 'error', reason: error }); + res.status(400).send({ status: 'error', reason: error }); return; } @@ -187,7 +196,7 @@ app.post('/change-password', (req, res) => { }); app.get('/validate', (req, res) => { - let session = validateUser(req, res); + let session = validateSession(req, res); if (session) { const user = getUserInfo(session.user_id); let permissions = getUserPermissions(session.user_id); diff --git a/src/app-admin.js b/src/app-admin.js index ff8d5aa36..c10f16b2b 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -1,14 +1,18 @@ import express from 'express'; import * as uuid from 'uuid'; -import { errorMiddleware } from './util/middlewares.js'; -import validateUser from './util/validate-user.js'; +import { + errorMiddleware, + requestLoggerMiddleware, + validateSessionMiddleware, +} from './util/middlewares.js'; +import validateSession from './util/validate-user.js'; import getAccountDb, { isAdmin } from './account-db.js'; import config from './load-config.js'; -import bodyParser from 'body-parser'; let app = express(); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(requestLoggerMiddleware); app.use(errorMiddleware); export { app as handlers }; @@ -38,20 +42,6 @@ const getFileById = (fileId) => { ); }; -export const getAdminSessionFromRequest = async (req, res) => { - let session = validateUser(req, res); - if (!session) { - return null; - } - - if (!(await isAdmin(session.user_id))) { - sendErrorResponse(res, 401, 'unauthorized', 'permission-not-found'); - return null; - } - - return session; -}; - const validateUserInput = (res, user) => { if (!user.userName) { sendErrorResponse( @@ -97,10 +87,7 @@ app.get('/ownerCreated/', (req, res) => { res.json(cnt > 0); }); -app.get('/users/', (req, res) => { - const session = validateUser(req, res); - if (!session) return; - +app.get('/users/', validateSessionMiddleware, (req, res) => { const users = getAccountDb().all( `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role FROM users @@ -118,9 +105,15 @@ app.get('/users/', (req, res) => { ); }); -app.post('/users', async (req, res) => { - const user = await getAdminSessionFromRequest(req, res); - if (!user) return; +app.post('/users', validateSessionMiddleware, async (req, res) => { + if (isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } const newUser = req.body; const { id: userIdInDb } = getUserByUsername(newUser.userName); @@ -153,9 +146,15 @@ app.post('/users', async (req, res) => { res.status(200).send({ status: 'ok', data: { id: userId } }); }); -app.patch('/users', async (req, res) => { - const user = await getAdminSessionFromRequest(req, res); - if (!user) return; +app.patch('/users', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } const userToUpdate = req.body; const { id: userIdInDb } = @@ -190,9 +189,15 @@ app.patch('/users', async (req, res) => { res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); }); -app.post('/users/delete-all', async (req, res) => { - const user = await getAdminSessionFromRequest(req, res); - if (!user) return; +app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { + if (await isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } const ids = req.body.ids; let totalDeleted = 0; @@ -224,10 +229,8 @@ app.post('/users/delete-all', async (req, res) => { } }); -app.get('/access', (req, res) => { +app.get('/access', validateSessionMiddleware, (req, res) => { const fileId = req.query.fileId; - const session = validateUser(req, res); - if (!session) return; const { id: fileIdInDb } = getFileById(fileId); if (!fileIdInDb) { @@ -241,7 +244,11 @@ app.get('/access', (req, res) => { JOIN user_access ON user_access.user_id = users.id JOIN files ON files.id = user_access.file_id WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [fileId, session.user_id, !isAdmin(session.user_id) ? 0 : 1], + [ + fileId, + req.userSession.user_id, + !isAdmin(req.userSession.user_id) ? 0 : 1, + ], ); res.json(accesses); @@ -276,7 +283,7 @@ function checkFilePermission(fileId, userId, res) { app.post('/access', (req, res) => { const userAccess = req.body || {}; - const session = validateUser(req, res); + const session = validateSession(req, res); if (!session) return; @@ -313,7 +320,7 @@ app.post('/access', (req, res) => { app.post('/access/delete-all', (req, res) => { const fileId = req.query.fileId; - const session = validateUser(req, res); + const session = validateSession(req, res); if (!session) return; if (!checkFilePermission(fileId, session.user_id, res)) return; @@ -337,12 +344,10 @@ app.post('/access/delete-all', (req, res) => { } }); -app.get('/access/users', async (req, res) => { +app.get('/access/users', validateSessionMiddleware, async (req, res) => { const fileId = req.query.fileId; - const session = validateUser(req, res); - if (!session) return; - if (!checkFilePermission(fileId, session.user_id, res)) return; + if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; const users = getAccountDb().all( `SELECT users.id as userId, user_name as userName, display_name as displayName, @@ -382,7 +387,7 @@ app.post('/access/get-bulk', (req, res) => { app.get('/access/check-access', (req, res) => { const fileId = req.query.fileId; - const session = validateUser(req, res); + const session = validateSession(req, res); if (!session) return; const { id: fileIdInDb } = getFileById(fileId); @@ -407,52 +412,52 @@ app.get('/access/check-access', (req, res) => { res.json({ granted: owner === session.user_id }); }); -app.post('/access/transfer-ownership/', (req, res) => { - const newUserOwner = req.body || {}; - const session = validateUser(req, res); - if (!session) return; +app.post( + '/access/transfer-ownership/', + validateSessionMiddleware, + (req, res) => { + const newUserOwner = req.body || {}; + if (!checkFilePermission(newUserOwner.fileId, req.userSession.user_id, res)) + return; - if (!checkFilePermission(newUserOwner.fileId, session.user_id, res)) return; + if (!newUserOwner.newUserId) { + sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); + return; + } - if (!newUserOwner.newUserId) { - sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); - return; - } + const { cnt } = + getAccountDb().first( + 'SELECT count(*) AS cnt FROM users WHERE users.id = ?', + [newUserOwner.newUserId], + ) || {}; - const { cnt } = - getAccountDb().first( - 'SELECT count(*) AS cnt FROM users WHERE users.id = ?', - [newUserOwner.newUserId], - ) || {}; + if (cnt === 0) { + sendErrorResponse(res, 400, 'new-user-not-found', 'New user not found'); + return; + } - if (cnt === 0) { - sendErrorResponse(res, 400, 'new-user-not-found', 'New user not found'); - return; - } - - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - newUserOwner.newUserId, - newUserOwner.fileId, - ]); + getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ + newUserOwner.newUserId, + newUserOwner.fileId, + ]); - res.status(200).send({ status: 'ok', data: {} }); -}); + res.status(200).send({ status: 'ok', data: {} }); + }, +); -app.get('/file/owner', async (req, res) => { +app.get('/file/owner', validateSessionMiddleware, async (req, res) => { const fileId = req.query.fileId; - const session = validateUser(req, res); - if (!session) return; - if (!checkFilePermission(fileId, session.user_id, res)) return; + if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; - let canGetOwner = await isAdmin(session.user_id); + let canGetOwner = isAdmin(req.userSession.user_id); if (!canGetOwner) { const { canListAvaiableUserFromDB } = getAccountDb().first( `SELECT count(*) as canListAvaiableUserFromDB FROM files WHERE files.id = ? and files.owner = ?`, - [fileId, session.user_id], + [fileId, req.userSession.user_id], ) || {}; canGetOwner = canListAvaiableUserFromDB === 1; } diff --git a/src/app-gocardless/app-gocardless.js b/src/app-gocardless/app-gocardless.js index 44d92c132..bdfc78994 100644 --- a/src/app-gocardless/app-gocardless.js +++ b/src/app-gocardless/app-gocardless.js @@ -14,7 +14,7 @@ import { handleError } from './util/handle-error.js'; import { sha256String } from '../util/hash.js'; import { requestLoggerMiddleware, - validateUserMiddleware, + validateSessionMiddleware, } from '../util/middlewares.js'; const app = express(); @@ -26,7 +26,7 @@ app.get('/link', function (req, res) { export { app as handlers }; app.use(express.json()); -app.use(validateUserMiddleware); +app.use(validateSessionMiddleware); app.post('/status', async (req, res) => { res.send({ diff --git a/src/app-secrets.js b/src/app-secrets.js index cbd18fabd..a97a49bda 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -3,7 +3,7 @@ import { secretsService } from './services/secrets-service.js'; import getAccountDb, { isAdmin } from './account-db.js'; import { requestLoggerMiddleware, - validateUserMiddleware, + validateSessionMiddleware, } from './util/middlewares.js'; const app = express(); @@ -11,7 +11,7 @@ const app = express(); export { app as handlers }; app.use(express.json()); app.use(requestLoggerMiddleware); -app.use(validateUserMiddleware); +app.use(validateSessionMiddleware); app.post('/', async (req, res) => { const { method } = @@ -20,7 +20,7 @@ app.post('/', async (req, res) => { const { name, value } = req.body; if (method === 'openid') { - let canSaveSecrets = isAdmin(req.userSession); + let canSaveSecrets = isAdmin(req.userSession.user_id); if (!canSaveSecrets) { res.status(400).send({ diff --git a/src/app-sync.js b/src/app-sync.js index 029a6696b..2747af97a 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -5,7 +5,7 @@ import * as uuid from 'uuid'; import { errorMiddleware, requestLoggerMiddleware, - validateUserMiddleware, + validateSessionMiddleware, } from './util/middlewares.js'; import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; @@ -21,7 +21,7 @@ app.use(express.raw({ type: 'application/actual-sync' })); app.use(express.raw({ type: 'application/encrypted-file' })); app.use(express.json()); -app.use(validateUserMiddleware); +app.use(validateSessionMiddleware); export { app as handlers }; const OK_RESPONSE = { status: 'ok' }; diff --git a/src/util/middlewares.js b/src/util/middlewares.js index a8c08aa48..eea929d1b 100644 --- a/src/util/middlewares.js +++ b/src/util/middlewares.js @@ -1,4 +1,4 @@ -import validateUser from './validate-user.js'; +import validateSession from './validate-user.js'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; @@ -19,8 +19,8 @@ async function errorMiddleware(err, req, res, _next) { * @param {import('express').Response} res * @param {import('express').NextFunction} next */ -const validateUserMiddleware = async (req, res, next) => { - let session = await validateUser(req, res); +const validateSessionMiddleware = async (req, res, next) => { + let session = await validateSession(req, res); if (!session) { return; } @@ -43,4 +43,4 @@ const requestLoggerMiddleware = expressWinston.logger({ ), }); -export { validateUserMiddleware, errorMiddleware, requestLoggerMiddleware }; +export { validateSessionMiddleware, errorMiddleware, requestLoggerMiddleware }; diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 826ef6ef4..795e4b14b 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -8,7 +8,7 @@ import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; * @param {import('express').Request} req * @param {import('express').Response} res */ -export default function validateUser(req, res) { +export default function validateSession(req, res) { let { token } = req.body || {}; if (!token) { @@ -31,7 +31,7 @@ export default function validateUser(req, res) { session.expires_at !== TOKEN_EXPIRATION_NEVER && session.expires_at * 1000 <= Date.now() ) { - res.status(403); + res.status(401); res.send({ status: 'error', reason: 'token-expired', From 484abab7495a19bcd57c9c46ee5b27a3393e71cd Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 14:59:25 -0300 Subject: [PATCH 072/139] bunch of fixes --- migrations/1718889148000-openid.js | 1 + migrations/1719409568000-multiuser.js | 15 ++++-- src/accounts/openid.js | 2 +- src/accounts/password.js | 2 +- src/app-admin.js | 10 ++-- src/app-admin.test.js | 78 ++++++++------------------- src/util/validate-user.js | 3 +- 7 files changed, 42 insertions(+), 69 deletions(-) diff --git a/migrations/1718889148000-openid.js b/migrations/1718889148000-openid.js index b170aea05..5a58d7028 100644 --- a/migrations/1718889148000-openid.js +++ b/migrations/1718889148000-openid.js @@ -24,6 +24,7 @@ export const up = async function () { }; export const down = async function () { + console.log("down openid"); await getAccountDb().exec( ` BEGIN TRANSACTION; diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 347b3143f..463ed78eb 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -23,14 +23,14 @@ export const up = async function () { CREATE TABLE user_roles (user_id TEXT, role_id TEXT, - , FOREIGN KEY (user_id) REFERENCES users(id) + FOREIGN KEY (user_id) REFERENCES users(id) , FOREIGN KEY (role_id) REFERENCES roles(id) ); CREATE TABLE user_access (user_id TEXT, file_id TEXT, - , FOREIGN KEY (user_id) REFERENCES users(id) + FOREIGN KEY (user_id) REFERENCES users(id) , FOREIGN KEY (file_id) REFERENCES files(id) ); @@ -53,10 +53,16 @@ export const up = async function () { }; export const down = async function () { + console.log("down multuser"); await getAccountDb().exec( ` BEGIN TRANSACTION; + DROP TABLE IF EXISTS user_access; + DROP TABLE IF EXISTS user_roles; + DROP TABLE IF EXISTS roles; + + CREATE TABLE sessions_backup ( token TEXT PRIMARY KEY ); @@ -64,6 +70,8 @@ export const down = async function () { INSERT INTO sessions_backup (token) SELECT token FROM sessions; + DROP TABLE sessions; + ALTER TABLE sessions_backup RENAME TO sessions; CREATE TABLE files_backup ( @@ -105,9 +113,6 @@ export const down = async function () { ALTER TABLE files_backup RENAME TO files; - DROP TABLE IF EXISTS user_access; - DROP TABLE IF EXISTS user_roles; - DROP TABLE IF EXISTS roles; DROP TABLE IF EXISTS users; COMMIT; diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 649a5522e..c736069ba 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -1,8 +1,8 @@ import getAccountDb, { clearExpiredSessions } from '../account-db.js'; import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; -import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; import finalConfig from '../load-config.js'; +import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; export async function bootstrapOpenId(config) { if (!('issuer' in config)) { diff --git a/src/accounts/password.js b/src/accounts/password.js index 1d455225d..166c90bcc 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -1,8 +1,8 @@ import * as bcrypt from 'bcrypt'; import getAccountDb, { clearExpiredSessions } from '../account-db.js'; import * as uuid from 'uuid'; -import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; import finalConfig from '../load-config.js'; +import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; function hashPassword(password) { return bcrypt.hashSync(password, 12); diff --git a/src/app-admin.js b/src/app-admin.js index c10f16b2b..c1d695d62 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -12,13 +12,11 @@ import config from './load-config.js'; let app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use(requestLoggerMiddleware); app.use(errorMiddleware); +app.use(requestLoggerMiddleware); export { app as handlers }; -export const TOKEN_EXPIRATION_NEVER = -1; - const sendErrorResponse = (res, status, reason, details) => { res.status(status).send({ status: 'error', @@ -87,7 +85,7 @@ app.get('/ownerCreated/', (req, res) => { res.json(cnt > 0); }); -app.get('/users/', validateSessionMiddleware, (req, res) => { +app.get('/users/', await validateSessionMiddleware, (req, res) => { const users = getAccountDb().all( `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role FROM users @@ -106,7 +104,7 @@ app.get('/users/', validateSessionMiddleware, (req, res) => { }); app.post('/users', validateSessionMiddleware, async (req, res) => { - if (isAdmin(req.userSession.user_id)) { + if (!isAdmin(req.userSession.user_id)) { res.status(401).send({ status: 'error', reason: 'unauthorized', @@ -190,7 +188,7 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { }); app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { - if (await isAdmin(req.userSession.user_id)) { + if (!isAdmin(req.userSession.user_id)) { res.status(401).send({ status: 'error', reason: 'unauthorized', diff --git a/src/app-admin.test.js b/src/app-admin.test.js index dd54c88fc..f17c208b9 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -6,6 +6,15 @@ import { v4 as uuidv4 } from 'uuid'; const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; const BASIC_ROLE = 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc'; +// Create role helper to ensure roles exist before creating users +const createRole = (roleId, name, permissions = '') => { + getAccountDb().mutate( + 'INSERT OR IGNORE INTO roles (id, permissions, name) VALUES (?, ?, ?)', + [roleId, permissions, name], + ); +}; + +// Create user helper function const createUser = (userId, userName, role, owner = 0, enabled = 1) => { getAccountDb().mutate( 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', @@ -18,8 +27,9 @@ const createUser = (userId, userName, role, owner = 0, enabled = 1) => { }; const deleteUser = (userId) => { - getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]); + getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]); getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); + getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]); }; const createSession = (userId, sessionToken) => { @@ -31,9 +41,15 @@ const createSession = (userId, sessionToken) => { const generateSessionToken = () => `token-${uuidv4()}`; +// Ensure roles are created before each test run +beforeAll(() => { + createRole(ADMIN_ROLE, 'Admin', 'ADMINISTRATOR'); + createRole(BASIC_ROLE, 'Basic', ''); +}); + describe('/admin', () => { describe('/ownerCreated', () => { - it('should return 200 and true if a owner user is created', async () => { + it('should return 200 and true if an owner user is created', async () => { const sessionToken = generateSessionToken(); const adminId = uuidv4(); createUser(adminId, 'admin', ADMIN_ROLE, 1); @@ -243,49 +259,6 @@ describe('/admin', () => { }); describe('/access', () => { - describe('GET /access', () => { - let sessionUserId, testUserId, fileId, sessionToken; - - beforeEach(() => { - sessionUserId = uuidv4(); - testUserId = uuidv4(); - fileId = uuidv4(); - sessionToken = generateSessionToken(); - - createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); - createSession(sessionUserId, sessionToken); - createUser(testUserId, 'testUser', ADMIN_ROLE); - getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ - fileId, - sessionUserId, - ]); - }); - - afterEach(() => { - deleteUser(sessionUserId); - deleteUser(testUserId); - getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); - }); - - it('should return 200 and a list of accesses', async () => { - const res = await request(app) - .get('/access') - .query({ fileId }) - .set('x-actual-token', sessionToken); - - expect(res.statusCode).toEqual(200); - expect(res.body).toEqual([]); - }); - - it('should return 400 if fileId is missing', async () => { - const res = await request(app) - .get('/access') - .set('x-actual-token', sessionToken); - - expect(res.statusCode).toEqual(400); - }); - }); - describe('POST /access', () => { let sessionUserId, testUserId, fileId, sessionToken; @@ -313,11 +286,9 @@ describe('/admin', () => { it('should return 200 and grant access to a user', async () => { const newUserAccess = { fileId, - userId: 'newUserId1', + userId: testUserId, }; - createUser('newUserId1', 'newUser', BASIC_ROLE); - const res = await request(app) .post('/access') .send(newUserAccess) @@ -330,10 +301,9 @@ describe('/admin', () => { it('should return 400 if the user already has access', async () => { const newUserAccess = { fileId, - userId: 'newUserId2', + userId: testUserId, }; - createUser('newUserId2', 'newUser', BASIC_ROLE); await request(app) .post('/access') .send(newUserAccess) @@ -362,27 +332,25 @@ describe('/admin', () => { createUser(sessionUserId, 'sessionUser', ADMIN_ROLE); createSession(sessionUserId, sessionToken); createUser(testUserId, 'testUser', ADMIN_ROLE); - createUser('newUserId3', 'newUser', BASIC_ROLE); getAccountDb().mutate('INSERT INTO files (id, owner) VALUES (?, ?)', [ fileId, sessionUserId, ]); getAccountDb().mutate( 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', - ['newUserId3', fileId], + [testUserId, fileId], ); }); afterEach(() => { deleteUser(sessionUserId); deleteUser(testUserId); - deleteUser('newUserId3'); getAccountDb().mutate('DELETE FROM files WHERE id = ?', [fileId]); }); it('should return 200 and delete access for the specified user', async () => { const deleteAccess = { - ids: ['newUserId3'], + ids: [testUserId], }; const res = await request(app) @@ -413,4 +381,4 @@ describe('/admin', () => { }); }); }); -}); +}); \ No newline at end of file diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 795e4b14b..3481ad294 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -2,7 +2,8 @@ import config from '../load-config.js'; import proxyaddr from 'proxy-addr'; import ipaddr from 'ipaddr.js'; import { getSession } from '../account-db.js'; -import { TOKEN_EXPIRATION_NEVER } from '../app-admin.js'; + +export const TOKEN_EXPIRATION_NEVER = -1; /** * @param {import('express').Request} req From 10f6c082ca3dd061cbf94d580f278dbc57a521b7 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 15:06:04 -0300 Subject: [PATCH 073/139] removed logs --- migrations/1718889148000-openid.js | 1 - migrations/1719409568000-multiuser.js | 1 - 2 files changed, 2 deletions(-) diff --git a/migrations/1718889148000-openid.js b/migrations/1718889148000-openid.js index 5a58d7028..b170aea05 100644 --- a/migrations/1718889148000-openid.js +++ b/migrations/1718889148000-openid.js @@ -24,7 +24,6 @@ export const up = async function () { }; export const down = async function () { - console.log("down openid"); await getAccountDb().exec( ` BEGIN TRANSACTION; diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 463ed78eb..83471dd34 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -53,7 +53,6 @@ export const up = async function () { }; export const down = async function () { - console.log("down multuser"); await getAccountDb().exec( ` BEGIN TRANSACTION; From 026d8b9abbe1a5c9ce31627d0d96c2f7f0238c5e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 15:08:10 -0300 Subject: [PATCH 074/139] descriptive variable names --- src/accounts/openid.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index c736069ba..494db6cd2 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -172,12 +172,12 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-grant-failed: no identification was found' }; } - let { c } = accountDb.first( - 'SELECT count(*) as c FROM users WHERE user_name <> ?', + let { countUsersWithUserName } = accountDb.first( + 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', [''], ); let userId = null; - if (c === 0) { + if (countUsersWithUserName === 0) { userId = uuid.v4(); accountDb.mutate( 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', From 0036a2808a1bd4989ba59aec47b08b34843a648e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 4 Oct 2024 15:09:55 -0300 Subject: [PATCH 075/139] linter --- src/accounts/password.js | 10 +++++++--- src/app-admin.test.js | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/accounts/password.js b/src/accounts/password.js index 166c90bcc..2fb770d68 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -47,7 +47,9 @@ export function loginWithPassword(password) { let token = sessionRow ? sessionRow.token : uuid.v4(); - let { totalOfUsers } = accountDb.first('SELECT count(*) as totalOfUsers FROM users'); + let { totalOfUsers } = accountDb.first( + 'SELECT count(*) as totalOfUsers FROM users', + ); let userId = null; if (totalOfUsers === 0) { userId = uuid.v4(); @@ -56,7 +58,8 @@ export function loginWithPassword(password) { [userId, '', ''], ); - const { id: adminRoleId } = accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']) || {}; + const { id: adminRoleId } = + accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']) || {}; if (!adminRoleId) { return { error: 'administrator-role-not-found' }; @@ -81,7 +84,8 @@ export function loginWithPassword(password) { finalConfig.token_expiration != 'openid-provider' && typeof finalConfig.token_expiration === 'number' ) { - expiration = Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; + expiration = + Math.floor(Date.now() / 1000) + finalConfig.token_expiration * 60; } if (!sessionRow) { diff --git a/src/app-admin.test.js b/src/app-admin.test.js index f17c208b9..203f6e2cc 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -381,4 +381,4 @@ describe('/admin', () => { }); }); }); -}); \ No newline at end of file +}); From 4b8982943d18b4efd2fbc231d9248bc6b5f349e2 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 7 Oct 2024 16:50:01 -0300 Subject: [PATCH 076/139] code review --- src/account-db.js | 12 +- src/accounts/openid.js | 14 +- src/accounts/password.js | 12 +- src/app-account.js | 83 +------ src/app-admin.js | 450 +++++++++++------------------------ src/app-openid.js | 89 +++++++ src/app-secrets.js | 2 +- src/app.js | 2 + src/db.js | 25 ++ src/services/user-service.js | 192 +++++++++++++++ 10 files changed, 470 insertions(+), 411 deletions(-) create mode 100644 src/app-openid.js create mode 100644 src/services/user-service.js diff --git a/src/account-db.js b/src/account-db.js index eb88a3b10..7b742fcbc 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -200,11 +200,11 @@ export function clearExpiredSessions() { export async function toggleAuthentication() { if (config.loginMethod === 'openid') { - const { cnt } = getAccountDb().first( - 'SELECT count(*) as cnt FROM auth WHERE method = ? and active = 1', + const { openidIsEnabled } = getAccountDb().first( + 'SELECT count(*) as openidIsEnabled FROM auth WHERE method = ? and active = 1', ['openid'], ); - if (cnt == 0) { + if (openidIsEnabled == 0) { const { error } = (await enableOpenID(config, false)) || {}; if (error) { @@ -213,11 +213,11 @@ export async function toggleAuthentication() { } } } else if (config.loginMethod) { - const { cnt } = getAccountDb().first( - 'SELECT count(*) as cnt FROM auth WHERE method <> ? and active = 1', + const { enabledMethodsButOpenId } = getAccountDb().first( + 'SELECT count(*) as enabledMethodsButOpenId FROM auth WHERE method <> ? and active = 1', ['openid'], ); - if (cnt == 0) { + if (enabledMethodsButOpenId == 0) { const { error } = (await disableOpenID(config, false)) || {}; if (error) { diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 494db6cd2..2b3f00504 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -33,11 +33,13 @@ export async function bootstrapOpenId(config) { // this might not be a real issue since an analogous situation happens // if they forget their password. let accountDb = getAccountDb(); - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", - [JSON.stringify(config)], - ); + accountDb.transaction(() => { + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); + }); return {}; } @@ -56,7 +58,7 @@ async function setupOpenIdClient(config) { const client = new issuer.Client({ client_id: config.client_id, client_secret: config.client_secret, - redirect_uri: config.server_hostname + '/account/login-openid/cb', + redirect_uri: config.server_hostname + '/openid/callback', validate_id_token: true, }); diff --git a/src/accounts/password.js b/src/accounts/password.js index 2fb770d68..f261bd368 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -17,11 +17,13 @@ export function bootstrapPassword(password) { let hashed = hashPassword(password); let accountDb = getAccountDb(); - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)", - [hashed], - ); + accountDb.transaction(() => { + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)", + [hashed], + ); + }); return {}; } diff --git a/src/app-account.js b/src/app-account.js index a2f15549e..6d2e74352 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -2,24 +2,19 @@ import express from 'express'; import { errorMiddleware, requestLoggerMiddleware, - validateSessionMiddleware, } from './util/middlewares.js'; import validateSession, { validateAuthHeader } from './util/validate-user.js'; -import getAccountDb, { +import { bootstrap, needsBootstrap, getLoginMethod, listLoginMethods, - enableOpenID, - disableOpenID, getUserInfo, getUserPermissions, - isAdmin, } from './account-db.js'; import { changePassword, loginWithPassword } from './accounts/password.js'; import { loginWithOpenIdSetup, - loginWithOpenIdFinalize, } from './accounts/openid.js'; let app = express(); @@ -105,82 +100,6 @@ app.post('/login', async (req, res) => { res.send({ status: 'ok', data: { token } }); }); -app.post('/enable-openid', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ - status: 'error', - reason: 'unauthorized', - details: 'permission-not-found', - }); - return; - } - - let { error } = (await enableOpenID(req.body)) || {}; - - if (error) { - res.status(400).send({ status: 'error', reason: error }); - return; - } - res.send({ status: 'ok' }); -}); - -app.post('/enable-password', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ - status: 'error', - reason: 'unauthorized', - details: 'permission-not-found', - }); - return; - } - - let { error } = (await disableOpenID(req.body, true, true)) || {}; - - if (error) { - res.status(400).send({ status: 'error', reason: error }); - return; - } - res.send({ status: 'ok' }); -}); - -app.get('/openid-config', async (req, res) => { - const { ownerCount } = - getAccountDb().first( - `SELECT count(*) as ownerCount - FROM users - WHERE users.user_name <> '' and users.owner = 1`, - ) || {}; - - if (ownerCount > 0) { - res.status(400).send({ status: 'error', reason: 'already-bootstraped' }); - return; - } - - const auth = - getAccountDb().first( - `SELECT * FROM auth - WHERE method = ?`, - ['openid'], - ) || {}; - - if (!auth) { - res.status(500); - return; - } - - res.send({ openId: JSON.parse(auth.extra_data) }); -}); - -app.get('/login-openid/cb', async (req, res) => { - let { error, url } = await loginWithOpenIdFinalize(req.query); - if (error) { - res.send({ error }); - return; - } - - res.redirect(url); -}); - app.post('/change-password', (req, res) => { let session = validateSession(req, res); if (!session) return; diff --git a/src/app-admin.js b/src/app-admin.js index c1d695d62..f948c6cfb 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -6,8 +6,9 @@ import { validateSessionMiddleware, } from './util/middlewares.js'; import validateSession from './util/validate-user.js'; -import getAccountDb, { isAdmin } from './account-db.js'; +import { isAdmin } from './account-db.js'; import config from './load-config.js'; +import UserService from './services/user-service.js'; let app = express(); app.use(express.json()); @@ -17,83 +18,13 @@ app.use(requestLoggerMiddleware); export { app as handlers }; -const sendErrorResponse = (res, status, reason, details) => { - res.status(status).send({ - status: 'error', - reason, - details, - }); -}; - -const getUserByUsername = (userName) => { - return ( - getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ - userName, - ]) || {} - ); -}; - -const getFileById = (fileId) => { - return ( - getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [fileId]) || - {} - ); -}; - -const validateUserInput = (res, user) => { - if (!user.userName) { - sendErrorResponse( - res, - 400, - 'user-cant-be-empty', - 'Username cannot be empty', - ); - return false; - } - - if (!user.role) { - sendErrorResponse(res, 400, 'role-cant-be-empty', 'Role cannot be empty'); - return false; - } - - const { id: roleId } = - getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ - user.role, - ]) || {}; - - if (!roleId) { - sendErrorResponse( - res, - 400, - 'role-does-not-exists', - 'Selected role does not exists', - ); - return false; - } - - return true; -}; - app.get('/ownerCreated/', (req, res) => { - const { cnt } = - getAccountDb().first( - `SELECT count(*) as cnt - FROM users - WHERE users.user_name <> '' and users.owner = 1`, - ) || {}; - + const { cnt } = UserService.getOwnerCount() || {}; res.json(cnt > 0); }); app.get('/users/', await validateSessionMiddleware, (req, res) => { - const users = getAccountDb().all( - `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role - FROM users - JOIN user_roles ON user_roles.user_id = users.id - JOIN roles ON roles.id = user_roles.role_id - WHERE users.user_name <> ''`, - ); - + const users = UserService.getAllUsers(); res.json( users.map((u) => ({ ...u, @@ -114,16 +45,44 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { } const newUser = req.body; - const { id: userIdInDb } = getUserByUsername(newUser.userName); - if (!validateUserInput(res, newUser)) return; + if (!newUser.userName) { + res.status(400).send({ + status: 'error', + reason: 'user-cant-be-empty', + details: 'Username cannot be empty', + }); + return; + } + + if (!newUser.role) { + res.status(400).send({ + status: 'error', + reason: 'role-cant-be-empty', + details: 'Role cannot be empty', + }); + return; + } + + const { id: roleIdFromDb } = UserService.validateRole(newUser.role) || {}; + + if (!roleIdFromDb) { + res.status(400).send({ + status: 'error', + reason: 'role-does-not-exists', + details: 'Selected role does not exist', + }); + return; + } + + const { id: userIdInDb } = UserService.getUserByUsername(newUser.userName); + if (userIdInDb) { - sendErrorResponse( - res, - 400, - 'user-already-exists', - `User ${newUser.userName} already exists`, - ); + res.status(400).send({ + status: 'error', + reason: 'user-already-exists', + details: `User ${newUser.userName} already exists`, + }); return; } @@ -131,15 +90,8 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { let displayName = newUser.displayName || null; let enabled = newUser.enabled ? 1 : 0; - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, 0)', - [userId, newUser.userName, displayName, enabled], - ); - - getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, newUser.role], - ); + UserService.insertUser(userId, newUser.userName, displayName, enabled); + UserService.insertUserRole(userId, newUser.role); res.status(200).send({ status: 'ok', data: { id: userId } }); }); @@ -155,34 +107,57 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { } const userToUpdate = req.body; - const { id: userIdInDb } = - getAccountDb().first('SELECT id FROM users WHERE id = ?', [ - userToUpdate.id, - ]) || {}; + const { id: userIdInDb } = UserService.getUserByUsername(userToUpdate.id); + + if (!userToUpdate.userName) { + res.status(400).send({ + status: 'error', + reason: 'user-cant-be-empty', + details: 'Username cannot be empty', + }); + return; + } + + if (!userToUpdate.role) { + res.status(400).send({ + status: 'error', + reason: 'role-cant-be-empty', + details: 'Role cannot be empty', + }); + return; + } + + const { id: roleIdFromDb } = + UserService.validateRole(userToUpdate.role) || {}; + + if (!roleIdFromDb) { + res.status(400).send({ + status: 'error', + reason: 'role-does-not-exists', + details: 'Selected role does not exist', + }); + return; + } - if (!validateUserInput(res, userToUpdate)) return; if (!userIdInDb) { - sendErrorResponse( - res, - 400, - 'cannot-find-user-to-update', - `Cannot find ${userToUpdate.userName} to update`, - ); + res.status(400).send({ + status: 'error', + reason: 'cannot-find-user-to-update', + details: `Cannot find ${userToUpdate.userName} to update`, + }); return; } let displayName = userToUpdate.displayName || null; let enabled = userToUpdate.enabled ? 1 : 0; - getAccountDb().mutate( - 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', - [userToUpdate.userName, displayName, enabled, userIdInDb], - ); - - getAccountDb().mutate('UPDATE user_roles SET role_id = ? WHERE user_id = ?', [ - userToUpdate.role, + UserService.updateUser( userIdInDb, - ]); + userToUpdate.userName, + displayName, + enabled, + ); + UserService.updateUserRole(userIdInDb, userToUpdate.role); res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); }); @@ -200,21 +175,14 @@ app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { const ids = req.body.ids; let totalDeleted = 0; ids.forEach((item) => { - const { id: ownerId } = - getAccountDb().first('SELECT id FROM users WHERE owner = 1') || {}; + const { id: ownerId } = UserService.getOwnerCount() || {}; if (item === ownerId) return; - getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [item]); - getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [item]); - getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ - ownerId, - item, - ]); - const usersDeleted = getAccountDb().mutate( - 'DELETE FROM users WHERE id = ? and owner = 0', - [item], - ).changes; + UserService.deleteUserRoles(item); + UserService.deleteUserAccess(item); + UserService.updateFileOwner(ownerId, item); + const usersDeleted = UserService.deleteUser(item); totalDeleted += usersDeleted; }); @@ -223,56 +191,57 @@ app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { .status(200) .send({ status: 'ok', data: { someDeletionsFailed: false } }); } else { - sendErrorResponse(res, 400, 'not-all-deleted', ''); + res.status(400).send({ + status: 'error', + reason: 'not-all-deleted', + details: '', + }); } }); app.get('/access', validateSessionMiddleware, (req, res) => { const fileId = req.query.fileId; - const { id: fileIdInDb } = getFileById(fileId); + const { id: fileIdInDb } = UserService.getFileById(fileId); if (!fileIdInDb) { - sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + res.status(400).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); return false; } - const accesses = getAccountDb().all( - `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName - FROM users - JOIN user_access ON user_access.user_id = users.id - JOIN files ON files.id = user_access.file_id - WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [ - fileId, - req.userSession.user_id, - !isAdmin(req.userSession.user_id) ? 0 : 1, - ], + const accesses = UserService.getUserAccess( + fileId, + req.userSession.user_id, + isAdmin(req.userSession.user_id), ); res.json(accesses); }); function checkFilePermission(fileId, userId, res) { - const { granted } = getAccountDb().first( - `SELECT 1 as granted - FROM files - WHERE files.id = ? and (files.owner = ?)`, - [fileId, userId], - ) || { granted: 0 }; + const { granted } = UserService.checkFilePermission(fileId, userId) || { + granted: 0, + }; if (granted === 0 && !isAdmin(userId)) { - sendErrorResponse( - res, - 400, - 'file-denied', - "You don't have permissions over this file", - ); + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); return false; } - const { id: fileIdInDb } = getFileById(fileId); + const { id: fileIdInDb } = UserService.getFileById(fileId); if (!fileIdInDb) { - sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); + res.status(400).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); return false; } @@ -288,35 +257,33 @@ app.post('/access', (req, res) => { if (!checkFilePermission(userAccess.fileId, session.user_id, res)) return; if (!userAccess.userId) { - sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); + res.status(400).send({ + status: 'error', + reason: 'user-cant-be-empty', + details: 'User cannot be empty', + }); return; } const { cnt } = - getAccountDb().first( - 'SELECT count(*) AS cnt FROM user_access WHERE user_access.file_id = ? and user_access.user_id = ?', - [userAccess.fileId, userAccess.userId], - ) || {}; + UserService.getUserAccess(userAccess.fileId, userAccess.userId, false) || + {}; if (cnt > 0) { - sendErrorResponse( - res, - 400, - 'user-already-have-access', - 'User already have access', - ); + res.status(400).send({ + status: 'error', + reason: 'user-already-have-access', + details: 'User already have access', + }); return; } - getAccountDb().mutate( - 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', - [userAccess.userId, userAccess.fileId], - ); + UserService.addUserAccess(userAccess.userId, userAccess.fileId); res.status(200).send({ status: 'ok', data: {} }); }); -app.post('/access/delete-all', (req, res) => { +app.delete('/access', (req, res) => { const fileId = req.query.fileId; const session = validateSession(req, res); if (!session) return; @@ -324,21 +291,18 @@ app.post('/access/delete-all', (req, res) => { if (!checkFilePermission(fileId, session.user_id, res)) return; const ids = req.body.ids; - let totalDeleted = 0; - ids.forEach((item) => { - const accessDeleted = getAccountDb().mutate( - 'DELETE FROM user_access WHERE user_id = ?', - [item], - ).changes; - totalDeleted += accessDeleted; - }); + let totalDeleted = UserService.deleteUserAccessByIds(ids); if (ids.length === totalDeleted) { res .status(200) .send({ status: 'ok', data: { someDeletionsFailed: false } }); } else { - sendErrorResponse(res, 400, 'not-all-deleted', ''); + res.status(400).send({ + status: 'error', + reason: 'not-all-deleted', + details: '', + }); } }); @@ -347,146 +311,10 @@ app.get('/access/users', validateSessionMiddleware, async (req, res) => { if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; - const users = getAccountDb().all( - `SELECT users.id as userId, user_name as userName, display_name as displayName, - CASE WHEN user_access.file_id IS NULL THEN 0 ELSE 1 END as haveAccess, - CASE WHEN files.id IS NULL THEN 0 ELSE 1 END as owner - FROM users - LEFT JOIN user_access ON user_access.file_id = ? and user_access.user_id = users.id - LEFT JOIN files ON files.id = ? and files.owner = users.id - WHERE users.enabled = 1 AND users.user_name <> ''`, - [fileId, fileId], - ); + const users = UserService.getAllUserAccess(fileId); res.json(users); }); -app.post('/access/get-bulk', (req, res) => { - const fileIds = req.body || {}; - const accessMap = new Map(); - - fileIds.forEach((fileId) => { - const userAccess = getAccountDb().all( - `SELECT user_access.file_id as fileId, user_access.user_id as userId, users.display_name as displayName, users.user_name as userName - FROM users - JOIN user_access ON user_access.user_id = users.id - WHERE user_access.file_id = ? - UNION - SELECT files.id, users.id, users.display_name, users.user_name - FROM users - JOIN files ON files.owner = users.id - WHERE files.id = ?`, - [fileId, fileId], - ); - accessMap.set(fileId, userAccess); - }); - - res.status(200).send({ status: 'ok', data: Array.from(accessMap.entries()) }); -}); - -app.get('/access/check-access', (req, res) => { - const fileId = req.query.fileId; - const session = validateSession(req, res); - if (!session) return; - - const { id: fileIdInDb } = getFileById(fileId); - if (!fileIdInDb) { - sendErrorResponse(res, 400, 'invalid-file-id', 'File not found at server'); - return false; - } - - if (isAdmin(session.user_id)) { - res.json({ granted: true }); - return; - } - - const { owner } = - getAccountDb().first( - `SELECT files.owner - FROM files - WHERE files.id = ?`, - [fileId], - ) || {}; - - res.json({ granted: owner === session.user_id }); -}); - -app.post( - '/access/transfer-ownership/', - validateSessionMiddleware, - (req, res) => { - const newUserOwner = req.body || {}; - if (!checkFilePermission(newUserOwner.fileId, req.userSession.user_id, res)) - return; - - if (!newUserOwner.newUserId) { - sendErrorResponse(res, 400, 'user-cant-be-empty', 'User cannot be empty'); - return; - } - - const { cnt } = - getAccountDb().first( - 'SELECT count(*) AS cnt FROM users WHERE users.id = ?', - [newUserOwner.newUserId], - ) || {}; - - if (cnt === 0) { - sendErrorResponse(res, 400, 'new-user-not-found', 'New user not found'); - return; - } - - getAccountDb().mutate('UPDATE files SET owner = ? WHERE id = ?', [ - newUserOwner.newUserId, - newUserOwner.fileId, - ]); - - res.status(200).send({ status: 'ok', data: {} }); - }, -); - -app.get('/file/owner', validateSessionMiddleware, async (req, res) => { - const fileId = req.query.fileId; - - if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; - - let canGetOwner = isAdmin(req.userSession.user_id); - if (!canGetOwner) { - const { canListAvaiableUserFromDB } = - getAccountDb().first( - `SELECT count(*) as canListAvaiableUserFromDB - FROM files - WHERE files.id = ? and files.owner = ?`, - [fileId, req.userSession.user_id], - ) || {}; - canGetOwner = canListAvaiableUserFromDB === 1; - } - - if (canGetOwner) { - const owner = - getAccountDb().first( - `SELECT users.id, users.user_name userName, users.display_name as displayName - FROM files - JOIN users - ON users.id = files.owner - WHERE files.id = ?`, - [fileId], - ) || {}; - - res.json(owner); - } - - return null; -}); - -app.get('/auth-mode', (req, res) => { - const { method } = - getAccountDb().first( - `SELECT method from auth - where active = 1`, - ) || {}; - - res.json({ method }); -}); - app.get('/multiuser', (req, res) => { res.json(config.multiuser); }); diff --git a/src/app-openid.js b/src/app-openid.js new file mode 100644 index 000000000..b07d053f0 --- /dev/null +++ b/src/app-openid.js @@ -0,0 +1,89 @@ +import express from 'express'; +import { + errorMiddleware, + requestLoggerMiddleware, + validateSessionMiddleware, +} from './util/middlewares.js'; +import { disableOpenID, enableOpenID, isAdmin } from './account-db.js'; +import { loginWithOpenIdFinalize } from './accounts/openid.js'; +import UserService from './services/user-service.js'; + +let app = express(); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(errorMiddleware); +app.use(requestLoggerMiddleware); +export { app as handlers }; + +app.post('/enable', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } + + let { error } = (await enableOpenID(req.body)) || {}; + + if (error) { + res.status(500).send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok' }); +}); + +app.post('/disable', validateSessionMiddleware, async (req, res) => { + if (!isAdmin(req.userSession.user_id)) { + res.status(401).send({ + status: 'error', + reason: 'unauthorized', + details: 'permission-not-found', + }); + return; + } + + let { error } = (await disableOpenID(req.body, true, true)) || {}; + + if (error) { + res.status(400).send({ status: 'error', reason: error }); + return; + } + res.send({ status: 'ok' }); +}); + +app.get('/config', async (req, res) => { + const { cnt: ownerCount } = UserService.getOwnerCount() || {}; + + if (ownerCount > 0) { + res.status(400).send({ status: 'error', reason: 'already-bootstraped' }); + return; + } + + const auth = UserService.getOpenIDConfig(); + + if (!auth) { + res.status(500).send({ status: 'error', reason: 'OpenID configuration not found' }); + return; + } + + try { + const openIdConfig = JSON.parse(auth.extra_data); + res.send({ openId: openIdConfig }); + } catch (error) { + res + .status(500) + .send({ status: 'error', reason: 'Invalid OpenID configuration' }); + } +}); + +app.get('/callback', async (req, res) => { + let { error, url } = await loginWithOpenIdFinalize(req.query); + if (error) { + res.status(400).send({ error }); + return; + } + + res.redirect(url); +}); \ No newline at end of file diff --git a/src/app-secrets.js b/src/app-secrets.js index a97a49bda..aac463fa9 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -23,7 +23,7 @@ app.post('/', async (req, res) => { let canSaveSecrets = isAdmin(req.userSession.user_id); if (!canSaveSecrets) { - res.status(400).send({ + res.status(403).send({ status: 'error', reason: 'not-admin', details: 'You have to be admin to set secrets', diff --git a/src/app.js b/src/app.js index 7afd6984e..b23383b80 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,7 @@ import * as goCardlessApp from './app-gocardless/app-gocardless.js'; import * as simpleFinApp from './app-simplefin/app-simplefin.js'; import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; +import * as openidApp from './app-openid.js'; import { toggleAuthentication } from './account-db.js'; import { exit } from 'node:process'; @@ -52,6 +53,7 @@ app.use('/simplefin', simpleFinApp.handlers); app.use('/secret', secretApp.handlers); app.use('/admin', adminApp.handlers); +app.use('/openid', openidApp.handlers); app.get('/mode', (req, res) => { res.send(config.mode); diff --git a/src/db.js b/src/db.js index a4d57a6a9..3ae635638 100644 --- a/src/db.js +++ b/src/db.js @@ -50,6 +50,31 @@ class WrappedDatabase { close() { this.db.close(); } + + /** + * Delete items by a list of IDs + * @param {string} tableName + * @param {number[]} ids + * @returns {number} Total number of rows deleted + */ + deleteByIds(tableName, ids) { + if (!Array.isArray(ids) || ids.length === 0) { + throw new Error('The provided ids must be a non-empty array.'); + } + + const CHUNK_SIZE = 999; + let totalChanges = 0; + + for (let i = 0; i < ids.length; i += CHUNK_SIZE) { + const chunk = ids.slice(i, i + CHUNK_SIZE).map(String); // Convert numbers to strings + const placeholders = chunk.map(() => '?').join(','); + const sql = `DELETE FROM ${tableName} WHERE id IN (${placeholders})`; + const result = this.mutate(sql, chunk); + totalChanges += result.changes; + } + + return totalChanges; + } } /** @param {string} filename */ diff --git a/src/services/user-service.js b/src/services/user-service.js new file mode 100644 index 000000000..dee121e0a --- /dev/null +++ b/src/services/user-service.js @@ -0,0 +1,192 @@ +import getAccountDb from '../account-db.js'; + +class UserService { + static getUserByUsername(userName) { + return ( + getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ + userName, + ]) || {} + ); + } + + static getFileById(fileId) { + return ( + getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [ + fileId, + ]) || {} + ); + } + + static validateRole(roleId) { + return ( + getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ + roleId, + ]) || {} + ); + } + + static getOwnerCount() { + return ( + getAccountDb().first( + `SELECT count(*) as cnt + FROM users + WHERE users.user_name <> '' and users.owner = 1`, + ) || {} + ); + } + + static getAllUsers() { + return getAccountDb().all( + `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role + FROM users + JOIN user_roles ON user_roles.user_id = users.id + JOIN roles ON roles.id = user_roles.role_id + WHERE users.user_name <> ''`, + ); + } + + static insertUser(userId, userName, displayName, enabled) { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, 0)', + [userId, userName, displayName, enabled], + ); + } + + static insertUserRole(userId, roleId) { + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, roleId], + ); + } + + static updateUser(userId, userName, displayName, enabled) { + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', + [userName, displayName, enabled, userId], + ); + } + + static updateUserRole(userId, roleId) { + getAccountDb().mutate( + 'UPDATE user_roles SET role_id = ? WHERE user_id = ?', + [roleId, userId], + ); + } + + static deleteUserRoles(userId) { + getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); + } + + static deleteUserAccess(userId) { + getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ + userId, + ]); + } + + static updateFileOwner(ownerId, userId) { + getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ + ownerId, + userId, + ]); + } + + static deleteUser(userId) { + return getAccountDb().mutate( + 'DELETE FROM users WHERE id = ? and owner = 0', + [userId], + ).changes; + } + + static getUserAccess(fileId, userId, isAdmin) { + return getAccountDb().all( + `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName + FROM users + JOIN user_access ON user_access.user_id = users.id + JOIN files ON files.id = user_access.file_id + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [fileId, userId, isAdmin ? 1 : 0], + ); + } + + static checkFilePermission(fileId, userId) { + return ( + getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ?)`, + [fileId, userId], + ) || { granted: 0 } + ); + } + + static addUserAccess(userId, fileId) { + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + [userId, fileId], + ); + } + + static deleteUserAccessById(userId) { + return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ + userId, + ]).changes; + } + + static deleteUserAccessByIds(userIds) { + return getAccountDb().deleteByIds('user_access', userIds); + } + + static getAllUserAccess(fileId) { + return getAccountDb().all( + `SELECT users.id as userId, user_name as userName, display_name as displayName, + CASE WHEN user_access.file_id IS NULL THEN 0 ELSE 1 END as haveAccess, + CASE WHEN files.id IS NULL THEN 0 ELSE 1 END as owner + FROM users + LEFT JOIN user_access ON user_access.file_id = ? and user_access.user_id = users.id + LEFT JOIN files ON files.id = ? and files.owner = users.id + WHERE users.enabled = 1 AND users.user_name <> ''`, + [fileId, fileId], + ); + } + + static getBulkUserAccess(fileIds) { + const accessMap = new Map(); + fileIds.forEach((fileId) => { + const userAccess = getAccountDb().all( + `SELECT user_access.file_id as fileId, user_access.user_id as userId, users.display_name as displayName, users.user_name as userName + FROM users + JOIN user_access ON user_access.user_id = users.id + WHERE user_access.file_id = ? + UNION + SELECT files.id, users.id, users.display_name, users.user_name + FROM users + JOIN files ON files.owner = users.id + WHERE files.id = ?`, + [fileId, fileId], + ); + accessMap.set(fileId, userAccess); + }); + return Array.from(accessMap.entries()); + } + + static getAuthMode() { + return ( + getAccountDb().first( + `SELECT method from auth + where active = 1`, + ) || {} + ); + } + + static getOpenIDConfig() { + return ( + getAccountDb().first( + `SELECT * FROM auth + WHERE method = ?`, + ['openid'], + ) || {} + ); + } +} + +export default UserService; From 9796d79ff889cf750f068f9f7ca2eaa2884b5397 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 7 Oct 2024 16:50:26 -0300 Subject: [PATCH 077/139] linter --- src/app-account.js | 4 +--- src/app-openid.js | 6 ++++-- src/services/user-service.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app-account.js b/src/app-account.js index 6d2e74352..e7e98c07c 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -13,9 +13,7 @@ import { getUserPermissions, } from './account-db.js'; import { changePassword, loginWithPassword } from './accounts/password.js'; -import { - loginWithOpenIdSetup, -} from './accounts/openid.js'; +import { loginWithOpenIdSetup } from './accounts/openid.js'; let app = express(); app.use(express.json()); diff --git a/src/app-openid.js b/src/app-openid.js index b07d053f0..d2767e08e 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -64,7 +64,9 @@ app.get('/config', async (req, res) => { const auth = UserService.getOpenIDConfig(); if (!auth) { - res.status(500).send({ status: 'error', reason: 'OpenID configuration not found' }); + res + .status(500) + .send({ status: 'error', reason: 'OpenID configuration not found' }); return; } @@ -86,4 +88,4 @@ app.get('/callback', async (req, res) => { } res.redirect(url); -}); \ No newline at end of file +}); diff --git a/src/services/user-service.js b/src/services/user-service.js index dee121e0a..aa05bdb41 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -134,7 +134,7 @@ class UserService { static deleteUserAccessByIds(userIds) { return getAccountDb().deleteByIds('user_access', userIds); - } + } static getAllUserAccess(fileId) { return getAccountDb().all( From fa5ff8676fdd3639f6ae4d8165adf3289607d8b8 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 7 Oct 2024 16:58:37 -0300 Subject: [PATCH 078/139] improved variables --- src/app-admin.js | 12 +++++------- src/services/user-service.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index f948c6cfb..a0ba25e97 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -19,8 +19,8 @@ app.use(requestLoggerMiddleware); export { app as handlers }; app.get('/ownerCreated/', (req, res) => { - const { cnt } = UserService.getOwnerCount() || {}; - res.json(cnt > 0); + const { ownerCount } = UserService.getOwnerCount() || {}; + res.json(ownerCount > 0); }); app.get('/users/', await validateSessionMiddleware, (req, res) => { @@ -265,11 +265,9 @@ app.post('/access', (req, res) => { return; } - const { cnt } = - UserService.getUserAccess(userAccess.fileId, userAccess.userId, false) || - {}; - - if (cnt > 0) { + if ( + UserService.countUserAccess(userAccess.fileId, userAccess.userId, false) > 0 + ) { res.status(400).send({ status: 'error', reason: 'user-already-have-access', diff --git a/src/services/user-service.js b/src/services/user-service.js index aa05bdb41..a5602bb58 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -108,6 +108,20 @@ class UserService { ); } + static countUserAccess(fileId, userId, isAdmin) { + const { countUserAccess } = + getAccountDb().first( + `SELECT count(*) as countUserAccess + FROM users + JOIN user_access ON user_access.user_id = users.id + JOIN files ON files.id = user_access.file_id + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [fileId, userId, isAdmin ? 1 : 0], + ) || {}; + + return countUserAccess; + } + static checkFilePermission(fileId, userId) { return ( getAccountDb().first( From 24d8c4f3ec7de5ea0573c2099d71cb682ddb2a8f Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 8 Oct 2024 16:57:45 -0300 Subject: [PATCH 079/139] fixes and refactories --- src/app-admin.js | 137 +++++++++++++++++++++-------------- src/app-sync.js | 20 +++++ src/db.js | 25 ------- src/services/user-service.js | 126 +++++++++++++++++++++----------- 4 files changed, 186 insertions(+), 122 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index a0ba25e97..eafc04c20 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -19,7 +19,7 @@ app.use(requestLoggerMiddleware); export { app as handlers }; app.get('/ownerCreated/', (req, res) => { - const { ownerCount } = UserService.getOwnerCount() || {}; + const ownerCount = UserService.getOwnerCount(); res.json(ownerCount > 0); }); @@ -44,28 +44,18 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { return; } - const newUser = req.body; + const { userName, role, displayName, enabled } = req.body; - if (!newUser.userName) { + if (!userName || !role) { res.status(400).send({ status: 'error', - reason: 'user-cant-be-empty', - details: 'Username cannot be empty', - }); - return; - } - - if (!newUser.role) { - res.status(400).send({ - status: 'error', - reason: 'role-cant-be-empty', - details: 'Role cannot be empty', + reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`, + details: `${!userName ? 'Username' : 'Role'} cannot be empty`, }); return; } - const { id: roleIdFromDb } = UserService.validateRole(newUser.role) || {}; - + const roleIdFromDb = UserService.validateRole(role); if (!roleIdFromDb) { res.status(400).send({ status: 'error', @@ -75,23 +65,24 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { return; } - const { id: userIdInDb } = UserService.getUserByUsername(newUser.userName); - + const userIdInDb = UserService.getUserByUsername(userName); if (userIdInDb) { res.status(400).send({ status: 'error', reason: 'user-already-exists', - details: `User ${newUser.userName} already exists`, + details: `User ${userName} already exists`, }); return; } const userId = uuid.v4(); - let displayName = newUser.displayName || null; - let enabled = newUser.enabled ? 1 : 0; - - UserService.insertUser(userId, newUser.userName, displayName, enabled); - UserService.insertUserRole(userId, newUser.role); + UserService.insertUser( + userId, + userName, + displayName || null, + enabled ? 1 : 0, + ); + UserService.insertUserRole(userId, role); res.status(200).send({ status: 'ok', data: { id: userId } }); }); @@ -106,30 +97,18 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { return; } - const userToUpdate = req.body; - const { id: userIdInDb } = UserService.getUserByUsername(userToUpdate.id); + const { id, userName, role, displayName, enabled } = req.body; - if (!userToUpdate.userName) { + if (!userName || !role) { res.status(400).send({ status: 'error', - reason: 'user-cant-be-empty', - details: 'Username cannot be empty', + reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`, + details: `${!userName ? 'Username' : 'Role'} cannot be empty`, }); return; } - if (!userToUpdate.role) { - res.status(400).send({ - status: 'error', - reason: 'role-cant-be-empty', - details: 'Role cannot be empty', - }); - return; - } - - const { id: roleIdFromDb } = - UserService.validateRole(userToUpdate.role) || {}; - + const roleIdFromDb = UserService.validateRole(role); if (!roleIdFromDb) { res.status(400).send({ status: 'error', @@ -139,25 +118,23 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { return; } + const userIdInDb = UserService.getUserByUsername(id); if (!userIdInDb) { res.status(400).send({ status: 'error', reason: 'cannot-find-user-to-update', - details: `Cannot find ${userToUpdate.userName} to update`, + details: `Cannot find user ${userName} to update`, }); return; } - let displayName = userToUpdate.displayName || null; - let enabled = userToUpdate.enabled ? 1 : 0; - UserService.updateUser( userIdInDb, - userToUpdate.userName, - displayName, - enabled, + userName, + displayName || null, + enabled ? 1 : 0, ); - UserService.updateUserRole(userIdInDb, userToUpdate.role); + UserService.updateUserRole(userIdInDb, role); res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); }); @@ -175,13 +152,13 @@ app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { const ids = req.body.ids; let totalDeleted = 0; ids.forEach((item) => { - const { id: ownerId } = UserService.getOwnerCount() || {}; + const ownerId = UserService.getOwnerId(); if (item === ownerId) return; UserService.deleteUserRoles(item); UserService.deleteUserAccess(item); - UserService.updateFileOwner(ownerId, item); + UserService.transferAllFilesFromUser(ownerId, item); const usersDeleted = UserService.deleteUser(item); totalDeleted += usersDeleted; }); @@ -202,7 +179,7 @@ app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { app.get('/access', validateSessionMiddleware, (req, res) => { const fileId = req.query.fileId; - const { id: fileIdInDb } = UserService.getFileById(fileId); + const fileIdInDb = UserService.getFileById(fileId); if (!fileIdInDb) { res.status(400).send({ status: 'error', @@ -235,7 +212,7 @@ function checkFilePermission(fileId, userId, res) { return false; } - const { id: fileIdInDb } = UserService.getFileById(fileId); + const fileIdInDb = UserService.getFileById(fileId); if (!fileIdInDb) { res.status(400).send({ status: 'error', @@ -289,7 +266,7 @@ app.delete('/access', (req, res) => { if (!checkFilePermission(fileId, session.user_id, res)) return; const ids = req.body.ids; - let totalDeleted = UserService.deleteUserAccessByIds(ids); + let totalDeleted = UserService.deleteUserAccessByFileId(ids, fileId); if (ids.length === totalDeleted) { res @@ -313,6 +290,58 @@ app.get('/access/users', validateSessionMiddleware, async (req, res) => { res.json(users); }); +app.post( + '/access/transfer-ownership/', + validateSessionMiddleware, + (req, res) => { + const newUserOwner = req.body || {}; + if (!checkFilePermission(newUserOwner.fileId, req.userSession.user_id, res)) + return; + + if (!newUserOwner.newUserId) { + res.status(400).send({ + status: 'error', + reason: 'user-cant-be-empty', + details: 'Username cannot be empty', + }); + return; + } + + const newUserIdFromDb = UserService.getUserById(newUserOwner.newUserId); + if (newUserIdFromDb === 0) { + res.status(400).send({ + status: 'error', + reason: 'new-user-not-found', + details: 'New user not found', + }); + return; + } + + UserService.updateFileOwner(newUserOwner.newUserId, newUserOwner.fileId); + + res.status(200).send({ status: 'ok', data: {} }); + }, +); + +app.get('/file/owner', validateSessionMiddleware, async (req, res) => { + const fileId = req.query.fileId; + + if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; + + let canGetOwner = isAdmin(req.userSession.user_id); + if (!canGetOwner) { + const fileIdOwner = UserService.getFileOwnerId(fileId); + canGetOwner = fileIdOwner === req.userSession.user_id; + } + + if (canGetOwner) { + const owner = UserService.getFileOwnerById(fileId); + res.json(owner); + } + + return null; +}); + app.get('/multiuser', (req, res) => { res.json(config.multiuser); }); diff --git a/src/app-sync.js b/src/app-sync.js index 2747af97a..a85a5d367 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -353,6 +353,19 @@ app.get('/list-user-files', (req, res) => { [req.userSession.user_id, req.userSession.user_id], ); + let allUserAccess = accountDb.all( + `SELECT UA.user_id, users.display_name, users.user_name, UA.file_id + FROM files + JOIN user_access UA ON UA.file_id = files.id + JOIN users on users.id = UA.user_id + UNION ALL + SELECT users.id, users.display_name, users.user_name, files.id + FROM files + JOIN users on users.id = files.owner + `, + [], + ); + res.send({ status: 'ok', data: rows.map((row) => ({ @@ -362,6 +375,13 @@ app.get('/list-user-files', (req, res) => { name: row.name, encryptKeyId: row.encrypt_keyid, owner: row.owner, + usersWithAccess: allUserAccess + .filter((ua) => ua.file_id === row.id) + .map((ua) => ({ + userId: ua.user_id, + userName: ua.user_name, + displayName: ua.display_name, + })), })), }); }); diff --git a/src/db.js b/src/db.js index 3ae635638..a4d57a6a9 100644 --- a/src/db.js +++ b/src/db.js @@ -50,31 +50,6 @@ class WrappedDatabase { close() { this.db.close(); } - - /** - * Delete items by a list of IDs - * @param {string} tableName - * @param {number[]} ids - * @returns {number} Total number of rows deleted - */ - deleteByIds(tableName, ids) { - if (!Array.isArray(ids) || ids.length === 0) { - throw new Error('The provided ids must be a non-empty array.'); - } - - const CHUNK_SIZE = 999; - let totalChanges = 0; - - for (let i = 0; i < ids.length; i += CHUNK_SIZE) { - const chunk = ids.slice(i, i + CHUNK_SIZE).map(String); // Convert numbers to strings - const placeholders = chunk.map(() => '?').join(','); - const sql = `DELETE FROM ${tableName} WHERE id IN (${placeholders})`; - const result = this.mutate(sql, chunk); - totalChanges += result.changes; - } - - return totalChanges; - } } /** @param {string} filename */ diff --git a/src/services/user-service.js b/src/services/user-service.js index a5602bb58..ab99fd0b5 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -2,37 +2,69 @@ import getAccountDb from '../account-db.js'; class UserService { static getUserByUsername(userName) { - return ( + const { id } = getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ userName, - ]) || {} - ); + ]) || {}; + return id; + } + + static getUserById(userId) { + const { id } = + getAccountDb().first('SELECT id FROM users WHERE id = ?', [userId]) || {}; + return id; } static getFileById(fileId) { - return ( + const { id } = getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [ fileId, - ]) || {} - ); + ]) || {}; + return id; } static validateRole(roleId) { - return ( + const { id } = getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ roleId, - ]) || {} - ); + ]) || {}; + return id; } static getOwnerCount() { - return ( + const { cnt } = getAccountDb().first( - `SELECT count(*) as cnt - FROM users - WHERE users.user_name <> '' and users.owner = 1`, - ) || {} - ); + `SELECT count(*) as cnt FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || {}; + return cnt; + } + + static getOwnerId() { + const { id } = + getAccountDb().first( + `SELECT users.id FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || {}; + return id; + } + + static getFileOwnerId(fileId) { + const { owner } = + getAccountDb().first(`SELECT files.owner FROM files WHERE files.id = ?`, [ + fileId, + ]) || {}; + return owner; + } + + static getFileOwnerById(fileId) { + const { id, userName, displayName } = + getAccountDb().first( + `SELECT users.id, users.user_name userName, users.display_name as displayName + FROM files + JOIN users ON users.id = files.owner + WHERE files.id = ?`, + [fileId], + ) || {}; + return { id, userName, displayName }; } static getAllUsers() { @@ -73,6 +105,13 @@ class UserService { ); } + static deleteUser(userId) { + return getAccountDb().mutate( + 'DELETE FROM users WHERE id = ? and owner = 0', + [userId], + ).changes; + } + static deleteUserRoles(userId) { getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); } @@ -83,18 +122,18 @@ class UserService { ]); } - static updateFileOwner(ownerId, userId) { + static transferAllFilesFromUser(ownerId, oldUserId) { getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ ownerId, - userId, + oldUserId, ]); } - static deleteUser(userId) { - return getAccountDb().mutate( - 'DELETE FROM users WHERE id = ? and owner = 0', - [userId], - ).changes; + static updateFileOwner(ownerId, fileId) { + getAccountDb().mutate('UPDATE files set owner = ? WHERE id = ?', [ + ownerId, + fileId, + ]); } static getUserAccess(fileId, userId, isAdmin) { @@ -140,14 +179,27 @@ class UserService { ); } - static deleteUserAccessById(userId) { - return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ - userId, - ]).changes; - } + static deleteUserAccessByFileId(userIds, fileId) { + if (!Array.isArray(userIds) || userIds.length === 0) { + throw new Error('The provided userIds must be a non-empty array.'); + } + + const CHUNK_SIZE = 999; // SQLite and many databases have a limit for the number of placeholders in a query + let totalChanges = 0; + + for (let i = 0; i < userIds.length; i += CHUNK_SIZE) { + const chunk = userIds.slice(i, i + CHUNK_SIZE).map(String); // Convert numbers to strings + const placeholders = chunk.map(() => '?').join(','); + + // Use parameterized query to prevent SQL injection + const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`; + + // Execute the delete query with user IDs and the fileId as parameters + const result = getAccountDb().mutate(sql, [...chunk, fileId]); // Assuming mutate properly handles parameterized queries + totalChanges += result.changes; + } - static deleteUserAccessByIds(userIds) { - return getAccountDb().deleteByIds('user_access', userIds); + return totalChanges; } static getAllUserAccess(fileId) { @@ -183,22 +235,10 @@ class UserService { return Array.from(accessMap.entries()); } - static getAuthMode() { - return ( - getAccountDb().first( - `SELECT method from auth - where active = 1`, - ) || {} - ); - } - static getOpenIDConfig() { return ( - getAccountDb().first( - `SELECT * FROM auth - WHERE method = ?`, - ['openid'], - ) || {} + getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || + {} ); } } From 306f38d32610d42ca29b099e5650bbcf4badfd84 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 8 Oct 2024 17:26:29 -0300 Subject: [PATCH 080/139] more code review --- src/app-admin.js | 2 +- src/services/user-service.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index eafc04c20..270da8ee3 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -139,7 +139,7 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); }); -app.post('/users/delete-all', validateSessionMiddleware, async (req, res) => { +app.delete('/users', validateSessionMiddleware, async (req, res) => { if (!isAdmin(req.userSession.user_id)) { res.status(401).send({ status: 'error', diff --git a/src/services/user-service.js b/src/services/user-service.js index ab99fd0b5..c82f33061 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -36,7 +36,7 @@ class UserService { getAccountDb().first( `SELECT count(*) as cnt FROM users WHERE users.user_name <> '' and users.owner = 1`, ) || {}; - return cnt; + return cnt || 0; } static getOwnerId() { @@ -158,7 +158,7 @@ class UserService { [fileId, userId, isAdmin ? 1 : 0], ) || {}; - return countUserAccess; + return countUserAccess || 0; } static checkFilePermission(fileId, userId) { From 1a0b573834dd33708ee29e29ca7b799ef0acf177 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Tue, 8 Oct 2024 21:31:45 -0300 Subject: [PATCH 081/139] variable name --- src/services/user-service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index c82f33061..8670c07aa 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -32,11 +32,11 @@ class UserService { } static getOwnerCount() { - const { cnt } = + const { ownerCount } = getAccountDb().first( - `SELECT count(*) as cnt FROM users WHERE users.user_name <> '' and users.owner = 1`, + `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, ) || {}; - return cnt || 0; + return ownerCount || 0; } static getOwnerId() { From c7faccd8d7b05cf9c78ccaf2a84fae5f0dce54f0 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 9 Oct 2024 09:52:32 -0300 Subject: [PATCH 082/139] code review --- jest.global-setup.js | 40 ++-- src/accounts/password.js | 24 ++- src/app-account.js | 1 - src/app-admin.js | 163 ++++++++++++--- src/app-admin.test.js | 27 ++- src/app-openid.js | 12 +- src/app.js | 3 +- src/load-config.js | 2 +- src/services/user-service.js | 392 +++++++++++++++++------------------ 9 files changed, 389 insertions(+), 275 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 5a7bc6092..6fbe97e50 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -1,33 +1,40 @@ import getAccountDb from './src/account-db.js'; import runMigrations from './src/migrations.js'; +const GENERIC_ADMIN_ID = 'genericAdmin'; +const ADMIN_ROLE_ID = '213733c1-5645-46ad-8784-a7b20b400f93'; + const createUser = (userId, userName, role, owner = 0, enabled = 1) => { - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, owner], - ); - getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, role], - ); + if (!userId || !userName || !role) { + throw new Error('Missing required parameters'); + } + + try { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner], + ); + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, role], + ); + } catch (error) { + console.error('Error creating user:', error); + throw error; + } }; -const setSessionUser = (userId) => { +const setSessionUser = (userId, token = 'valid-token') => { getAccountDb().mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ userId, - 'valid-token', + token, ]); }; export default async function setup() { await runMigrations(); - createUser( - 'genericAdmin', - 'admin', - '213733c1-5645-46ad-8784-a7b20b400f93', - 1, - ); + createUser(GENERIC_ADMIN_ID, 'admin', ADMIN_ROLE_ID, 1); // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); @@ -42,4 +49,5 @@ export default async function setup() { ); setSessionUser('genericAdmin'); + setSessionUser('genericAdmin', 'valid-token-admin'); } diff --git a/src/accounts/password.js b/src/accounts/password.js index f261bd368..0514234bf 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -4,20 +4,23 @@ import * as uuid from 'uuid'; import finalConfig from '../load-config.js'; import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; +function isValidPassword(password) { + return password != null && password !== ''; +} + function hashPassword(password) { return bcrypt.hashSync(password, 12); } export function bootstrapPassword(password) { - if (!password) { + if (!isValidPassword(password)) { return { error: 'invalid-password' }; } - getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['password']); - let hashed = hashPassword(password); let accountDb = getAccountDb(); accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['password']); accountDb.mutate('UPDATE auth SET active = 0'); accountDb.mutate( "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('password', 'Password', ?, 1)", @@ -29,7 +32,7 @@ export function bootstrapPassword(password) { } export function loginWithPassword(password) { - if (password === undefined || password === '') { + if (!isValidPassword(password)) { return { error: 'invalid-password' }; } @@ -39,13 +42,16 @@ export function loginWithPassword(password) { 'password', ]) || {}; - let confirmed = passwordHash && bcrypt.compareSync(password, passwordHash); + let confirmed = bcrypt.compareSync(password, passwordHash); if (!confirmed) { return { error: 'invalid-password' }; } - let sessionRow = accountDb.first('SELECT * FROM sessions'); + let sessionRow = accountDb.first( + 'SELECT * FROM sessions WHERE auth_method = ?', + ['password'], + ); let token = sessionRow ? sessionRow.token : uuid.v4(); @@ -78,6 +84,10 @@ export function loginWithPassword(password) { ); userId = userIdFromDb; + + if (!userId) { + return { error: 'user-not-found' }; + } } let expiration = TOKEN_EXPIRATION_NEVER; @@ -110,7 +120,7 @@ export function loginWithPassword(password) { export function changePassword(newPassword) { let accountDb = getAccountDb(); - if (newPassword == null || newPassword === '') { + if (isValidPassword(newPassword)) { return { error: 'invalid-password' }; } diff --git a/src/app-account.js b/src/app-account.js index e7e98c07c..701284ee7 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -83,7 +83,6 @@ app.post('/login', async (req, res) => { return; } - case 'password': default: tokenRes = loginWithPassword(req.body.password); break; diff --git a/src/app-admin.js b/src/app-admin.js index 270da8ee3..b59eb4cff 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -8,7 +8,7 @@ import { import validateSession from './util/validate-user.js'; import { isAdmin } from './account-db.js'; import config from './load-config.js'; -import UserService from './services/user-service.js'; +import * as UserService from './services/user-service.js'; let app = express(); app.use(express.json()); @@ -19,8 +19,12 @@ app.use(requestLoggerMiddleware); export { app as handlers }; app.get('/ownerCreated/', (req, res) => { - const ownerCount = UserService.getOwnerCount(); - res.json(ownerCount > 0); + try { + const ownerCount = UserService.getOwnerCount(); + res.json(ownerCount > 0); + } catch (error) { + res.status(500).json({ error: 'Failed to retrieve owner count' }); + } }); app.get('/users/', await validateSessionMiddleware, (req, res) => { @@ -36,9 +40,9 @@ app.get('/users/', await validateSessionMiddleware, (req, res) => { app.post('/users', validateSessionMiddleware, async (req, res) => { if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ + res.status(403).send({ status: 'error', - reason: 'unauthorized', + reason: 'forbidden', details: 'permission-not-found', }); return; @@ -89,9 +93,9 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { app.patch('/users', validateSessionMiddleware, async (req, res) => { if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ + res.status(403).send({ status: 'error', - reason: 'unauthorized', + reason: 'forbidden', details: 'permission-not-found', }); return; @@ -118,7 +122,7 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { return; } - const userIdInDb = UserService.getUserByUsername(id); + const userIdInDb = UserService.getUserById(id); if (!userIdInDb) { res.status(400).send({ status: 'error', @@ -128,22 +132,22 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { return; } - UserService.updateUser( + UserService.updateUserWithRole( userIdInDb, userName, displayName || null, enabled ? 1 : 0, + role, ); - UserService.updateUserRole(userIdInDb, role); res.status(200).send({ status: 'ok', data: { id: userIdInDb } }); }); app.delete('/users', validateSessionMiddleware, async (req, res) => { if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ + res.status(403).send({ status: 'error', - reason: 'unauthorized', + reason: 'forbidden', details: 'permission-not-found', }); return; @@ -198,41 +202,38 @@ app.get('/access', validateSessionMiddleware, (req, res) => { res.json(accesses); }); -function checkFilePermission(fileId, userId, res) { - const { granted } = UserService.checkFilePermission(fileId, userId) || { +app.post('/access', (req, res) => { + const userAccess = req.body || {}; + const session = validateSession(req, res); + + if (!session) return; + + const { granted } = UserService.checkFilePermission( + userAccess.fileId, + session.user_id, + ) || { granted: 0, }; - if (granted === 0 && !isAdmin(userId)) { + if (granted === 0 && !isAdmin(session.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', details: "You don't have permissions over this file", }); - return false; + return; } - const fileIdInDb = UserService.getFileById(fileId); + const fileIdInDb = UserService.getFileById(userAccess.fileId); if (!fileIdInDb) { res.status(400).send({ status: 'error', reason: 'invalid-file-id', details: 'File not found at server', }); - return false; + return; } - return true; -} - -app.post('/access', (req, res) => { - const userAccess = req.body || {}; - const session = validateSession(req, res); - - if (!session) return; - - if (!checkFilePermission(userAccess.fileId, session.user_id, res)) return; - if (!userAccess.userId) { res.status(400).send({ status: 'error', @@ -263,7 +264,31 @@ app.delete('/access', (req, res) => { const session = validateSession(req, res); if (!session) return; - if (!checkFilePermission(fileId, session.user_id, res)) return; + const { granted } = UserService.checkFilePermission( + fileId, + session.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(session.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(fileId); + if (!fileIdInDb) { + res.status(400).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } const ids = req.body.ids; let totalDeleted = UserService.deleteUserAccessByFileId(ids, fileId); @@ -284,7 +309,31 @@ app.delete('/access', (req, res) => { app.get('/access/users', validateSessionMiddleware, async (req, res) => { const fileId = req.query.fileId; - if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; + const { granted } = UserService.checkFilePermission( + fileId, + req.userSession.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(req.userSession.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(fileId); + if (!fileIdInDb) { + res.status(400).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } const users = UserService.getAllUserAccess(fileId); res.json(users); @@ -295,8 +344,32 @@ app.post( validateSessionMiddleware, (req, res) => { const newUserOwner = req.body || {}; - if (!checkFilePermission(newUserOwner.fileId, req.userSession.user_id, res)) + + const { granted } = UserService.checkFilePermission( + newUserOwner.fileId, + req.userSession.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(req.userSession.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(newUserOwner.fileId); + if (!fileIdInDb) { + res.status(400).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); return; + } if (!newUserOwner.newUserId) { res.status(400).send({ @@ -326,7 +399,31 @@ app.post( app.get('/file/owner', validateSessionMiddleware, async (req, res) => { const fileId = req.query.fileId; - if (!checkFilePermission(fileId, req.userSession.user_id, res)) return; + const { granted } = UserService.checkFilePermission( + fileId, + req.userSession.user_id, + ) || { + granted: 0, + }; + + if (granted === 0 && !isAdmin(req.userSession.user_id)) { + res.status(400).send({ + status: 'error', + reason: 'file-denied', + details: "You don't have permissions over this file", + }); + return; + } + + const fileIdInDb = UserService.getFileById(fileId); + if (!fileIdInDb) { + res.status(400).send({ + status: 'error', + reason: 'invalid-file-id', + details: 'File not found at server', + }); + return; + } let canGetOwner = isAdmin(req.userSession.user_id); if (!canGetOwner) { diff --git a/src/app-admin.test.js b/src/app-admin.test.js index 203f6e2cc..648ca4ec4 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -95,6 +95,8 @@ describe('/admin', () => { describe('POST /users', () => { let sessionUserId, sessionToken; + let createdUserId; + let duplicateUserId; beforeEach(() => { sessionUserId = uuidv4(); @@ -105,6 +107,15 @@ describe('/admin', () => { afterEach(() => { deleteUser(sessionUserId); + if (createdUserId) { + deleteUser(createdUserId); + createdUserId = null; + } + + if (duplicateUserId) { + deleteUser(duplicateUserId); + duplicateUserId = null; + } }); it('should return 200 and create a new user', async () => { @@ -124,6 +135,8 @@ describe('/admin', () => { expect(res.statusCode).toEqual(200); expect(res.body.status).toBe('ok'); expect(res.body.data).toHaveProperty('id'); + + createdUserId = res.body.data.id; }); it('should return 400 if the user already exists', async () => { @@ -135,12 +148,14 @@ describe('/admin', () => { role: BASIC_ROLE, }; - await request(app) + let res = await request(app) .post('/users') .send(newUser) .set('x-actual-token', sessionToken); - const res = await request(app) + duplicateUserId = res.body.data.id; + + res = await request(app) .post('/users') .send(newUser) .set('x-actual-token', sessionToken); @@ -232,7 +247,7 @@ describe('/admin', () => { }; const res = await request(app) - .post('/users/delete-all') + .delete('/users') .send(userToDelete) .set('x-actual-token', sessionToken); @@ -247,7 +262,7 @@ describe('/admin', () => { }; const res = await request(app) - .post('/users/delete-all') + .delete('/users') .send(userToDelete) .set('x-actual-token', sessionToken); @@ -354,7 +369,7 @@ describe('/admin', () => { }; const res = await request(app) - .post('/access/delete-all') + .delete('/access') .send(deleteAccess) .query({ fileId }) .set('x-actual-token', sessionToken); @@ -370,7 +385,7 @@ describe('/admin', () => { }; const res = await request(app) - .post('/access/delete-all') + .delete('/access') .send(deleteAccess) .query({ fileId }) .set('x-actual-token', sessionToken); diff --git a/src/app-openid.js b/src/app-openid.js index d2767e08e..9283ddce4 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -6,7 +6,7 @@ import { } from './util/middlewares.js'; import { disableOpenID, enableOpenID, isAdmin } from './account-db.js'; import { loginWithOpenIdFinalize } from './accounts/openid.js'; -import UserService from './services/user-service.js'; +import * as UserService from './services/user-service.js'; let app = express(); app.use(express.json()); @@ -17,9 +17,9 @@ export { app as handlers }; app.post('/enable', validateSessionMiddleware, async (req, res) => { if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ + res.status(403).send({ status: 'error', - reason: 'unauthorized', + reason: 'forbidden', details: 'permission-not-found', }); return; @@ -36,9 +36,9 @@ app.post('/enable', validateSessionMiddleware, async (req, res) => { app.post('/disable', validateSessionMiddleware, async (req, res) => { if (!isAdmin(req.userSession.user_id)) { - res.status(401).send({ + res.status(403).send({ status: 'error', - reason: 'unauthorized', + reason: 'forbidden', details: 'permission-not-found', }); return; @@ -47,7 +47,7 @@ app.post('/disable', validateSessionMiddleware, async (req, res) => { let { error } = (await disableOpenID(req.body, true, true)) || {}; if (error) { - res.status(400).send({ status: 'error', reason: error }); + res.status(500).send({ status: 'error', reason: error }); return; } res.send({ status: 'ok' }); diff --git a/src/app.js b/src/app.js index b23383b80..bbe590c42 100644 --- a/src/app.js +++ b/src/app.js @@ -14,7 +14,6 @@ import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; import * as openidApp from './app-openid.js'; import { toggleAuthentication } from './account-db.js'; -import { exit } from 'node:process'; const app = express(); @@ -91,7 +90,7 @@ export default async function run() { app.listen(config.port, config.hostname); } - if (!(await toggleAuthentication())) exit(-1); + if (!(await toggleAuthentication())) process.exit(-1); console.log('Listening on ' + config.hostname + ':' + config.port + '...'); } diff --git a/src/load-config.js b/src/load-config.js index c436a604d..534661649 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -102,7 +102,7 @@ const finalConfig = { ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() : config.loginMethod, multiuser: process.env.ACTUAL_MULTIUSER - ? process.env.ACTUAL_MULTIUSER.toLowerCase() === 'true' + ? ['true', '1', 'yes', 'on'].includes(process.env.ACTUAL_MULTIUSER.toLowerCase()) : config.multiuser, trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) diff --git a/src/services/user-service.js b/src/services/user-service.js index 8670c07aa..a665d2b77 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -1,246 +1,232 @@ import getAccountDb from '../account-db.js'; -class UserService { - static getUserByUsername(userName) { - const { id } = - getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ - userName, - ]) || {}; - return id; - } +export function getUserByUsername(userName) { + const { id } = + getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ + userName, + ]) || {}; + return id; +} - static getUserById(userId) { - const { id } = - getAccountDb().first('SELECT id FROM users WHERE id = ?', [userId]) || {}; - return id; - } +export function getUserById(userId) { + const { id } = + getAccountDb().first('SELECT id FROM users WHERE id = ?', [userId]) || {}; + return id; +} - static getFileById(fileId) { - const { id } = - getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [ - fileId, - ]) || {}; - return id; - } +export function getFileById(fileId) { + const { id } = + getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [ + fileId, + ]) || {}; + return id; +} - static validateRole(roleId) { - const { id } = - getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ - roleId, - ]) || {}; - return id; - } +export function validateRole(roleId) { + const { id } = + getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ + roleId, + ]) || {}; + return id; +} - static getOwnerCount() { - const { ownerCount } = - getAccountDb().first( - `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, - ) || {}; - return ownerCount || 0; - } +export function getOwnerCount() { + const { ownerCount } = + getAccountDb().first( + `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || {}; + return ownerCount || 0; +} - static getOwnerId() { - const { id } = - getAccountDb().first( - `SELECT users.id FROM users WHERE users.user_name <> '' and users.owner = 1`, - ) || {}; - return id; - } +export function getOwnerId() { + const { id } = + getAccountDb().first( + `SELECT users.id FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || {}; + return id; +} - static getFileOwnerId(fileId) { - const { owner } = - getAccountDb().first(`SELECT files.owner FROM files WHERE files.id = ?`, [ - fileId, - ]) || {}; - return owner; - } +export function getFileOwnerId(fileId) { + const { owner } = + getAccountDb().first(`SELECT files.owner FROM files WHERE files.id = ?`, [ + fileId, + ]) || {}; + return owner; +} - static getFileOwnerById(fileId) { - const { id, userName, displayName } = - getAccountDb().first( - `SELECT users.id, users.user_name userName, users.display_name as displayName +export function getFileOwnerById(fileId) { + const { id, userName, displayName } = + getAccountDb().first( + `SELECT users.id, users.user_name userName, users.display_name as displayName FROM files JOIN users ON users.id = files.owner WHERE files.id = ?`, - [fileId], - ) || {}; - return { id, userName, displayName }; - } + [fileId], + ) || {}; + return { id, userName, displayName }; +} - static getAllUsers() { - return getAccountDb().all( - `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role - FROM users - JOIN user_roles ON user_roles.user_id = users.id - JOIN roles ON roles.id = user_roles.role_id - WHERE users.user_name <> ''`, - ); - } +export function getAllUsers() { + return getAccountDb().all( + `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role + FROM users + JOIN user_roles ON user_roles.user_id = users.id + JOIN roles ON roles.id = user_roles.role_id + WHERE users.user_name <> ''`, + ); +} - static insertUser(userId, userName, displayName, enabled) { - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, 0)', - [userId, userName, displayName, enabled], - ); - } +export function insertUser(userId, userName, displayName, enabled) { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, 0)', + [userId, userName, displayName, enabled], + ); +} - static insertUserRole(userId, roleId) { - getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, roleId], - ); - } +export function insertUserRole(userId, roleId) { + getAccountDb().mutate( + 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', + [userId, roleId], + ); +} - static updateUser(userId, userName, displayName, enabled) { +export function updateUser(userId, userName, displayName, enabled) { + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', + [userName, displayName, enabled, userId], + ); +} + +export function updateUserRole(userId, roleId) { + getAccountDb().mutate( + 'UPDATE user_roles SET role_id = ? WHERE user_id = ?', + [roleId, userId], + ); +} + +export function updateUserWithRole(userId, userName, displayName, enabled, roleId) { + getAccountDb().transaction(() => { getAccountDb().mutate( 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', [userName, displayName, enabled, userId], ); - } - - static updateUserRole(userId, roleId) { getAccountDb().mutate( 'UPDATE user_roles SET role_id = ? WHERE user_id = ?', [roleId, userId], ); - } - - static deleteUser(userId) { - return getAccountDb().mutate( - 'DELETE FROM users WHERE id = ? and owner = 0', - [userId], - ).changes; - } - - static deleteUserRoles(userId) { - getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); - } + }); +} - static deleteUserAccess(userId) { - getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ - userId, - ]); - } +export function deleteUser(userId) { + return getAccountDb().mutate( + 'DELETE FROM users WHERE id = ? and owner = 0', + [userId], + ).changes; +} - static transferAllFilesFromUser(ownerId, oldUserId) { - getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ - ownerId, - oldUserId, - ]); - } +export function deleteUserRoles(userId) { + getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); +} - static updateFileOwner(ownerId, fileId) { - getAccountDb().mutate('UPDATE files set owner = ? WHERE id = ?', [ - ownerId, - fileId, - ]); - } +export function deleteUserAccess(userId) { + getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ + userId, + ]); +} - static getUserAccess(fileId, userId, isAdmin) { - return getAccountDb().all( - `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName - FROM users - JOIN user_access ON user_access.user_id = users.id - JOIN files ON files.id = user_access.file_id - WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [fileId, userId, isAdmin ? 1 : 0], - ); - } +export function transferAllFilesFromUser(ownerId, oldUserId) { + getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ + ownerId, + oldUserId, + ]); +} - static countUserAccess(fileId, userId, isAdmin) { - const { countUserAccess } = - getAccountDb().first( - `SELECT count(*) as countUserAccess - FROM users - JOIN user_access ON user_access.user_id = users.id - JOIN files ON files.id = user_access.file_id - WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, - [fileId, userId, isAdmin ? 1 : 0], - ) || {}; - - return countUserAccess || 0; - } +export function updateFileOwner(ownerId, fileId) { + getAccountDb().mutate('UPDATE files set owner = ? WHERE id = ?', [ + ownerId, + fileId, + ]); +} - static checkFilePermission(fileId, userId) { - return ( - getAccountDb().first( - `SELECT 1 as granted - FROM files - WHERE files.id = ? and (files.owner = ?)`, - [fileId, userId], - ) || { granted: 0 } - ); - } +export function getUserAccess(fileId, userId, isAdmin) { + return getAccountDb().all( + `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName + FROM users + JOIN user_access ON user_access.user_id = users.id + JOIN files ON files.id = user_access.file_id + WHERE files.id = ? and (files.owner = ? OR 1 = ?)`, + [fileId, userId, isAdmin ? 1 : 0], + ); +} - static addUserAccess(userId, fileId) { - getAccountDb().mutate( - 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', - [userId, fileId], - ); - } +export function countUserAccess(fileId, userId, isAdmin) { + const { countUserAccess } = + getAccountDb().first( + `SELECT count(*) as countUserAccess + FROM users + LEFT JOIN user_access ON user_access.user_id = users.id + JOIN files ON files.id = user_access.file_id + WHERE files.id = ? and files.owner = ? OR users.id = ?`, + [fileId, userId, userId], + ) || {}; + + return countUserAccess || 0; +} - static deleteUserAccessByFileId(userIds, fileId) { - if (!Array.isArray(userIds) || userIds.length === 0) { - throw new Error('The provided userIds must be a non-empty array.'); - } +export function checkFilePermission(fileId, userId) { + return ( + getAccountDb().first( + `SELECT 1 as granted + FROM files + WHERE files.id = ? and (files.owner = ?)`, + [fileId, userId], + ) || { granted: 0 } + ); +} - const CHUNK_SIZE = 999; // SQLite and many databases have a limit for the number of placeholders in a query - let totalChanges = 0; +export function addUserAccess(userId, fileId) { + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + [userId, fileId], + ); +} - for (let i = 0; i < userIds.length; i += CHUNK_SIZE) { - const chunk = userIds.slice(i, i + CHUNK_SIZE).map(String); // Convert numbers to strings - const placeholders = chunk.map(() => '?').join(','); +export function deleteUserAccessByFileId(userIds, fileId) { + if (!Array.isArray(userIds) || userIds.length === 0) { + throw new Error('The provided userIds must be a non-empty array.'); + } - // Use parameterized query to prevent SQL injection - const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`; + const CHUNK_SIZE = 999; + let totalChanges = 0; - // Execute the delete query with user IDs and the fileId as parameters - const result = getAccountDb().mutate(sql, [...chunk, fileId]); // Assuming mutate properly handles parameterized queries - totalChanges += result.changes; - } + for (let i = 0; i < userIds.length; i += CHUNK_SIZE) { + const chunk = userIds.slice(i, i + CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); - return totalChanges; - } + const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`; - static getAllUserAccess(fileId) { - return getAccountDb().all( - `SELECT users.id as userId, user_name as userName, display_name as displayName, - CASE WHEN user_access.file_id IS NULL THEN 0 ELSE 1 END as haveAccess, - CASE WHEN files.id IS NULL THEN 0 ELSE 1 END as owner - FROM users - LEFT JOIN user_access ON user_access.file_id = ? and user_access.user_id = users.id - LEFT JOIN files ON files.id = ? and files.owner = users.id - WHERE users.enabled = 1 AND users.user_name <> ''`, - [fileId, fileId], - ); + const result = getAccountDb().mutate(sql, [...chunk, fileId]); + totalChanges += result.changes; } - static getBulkUserAccess(fileIds) { - const accessMap = new Map(); - fileIds.forEach((fileId) => { - const userAccess = getAccountDb().all( - `SELECT user_access.file_id as fileId, user_access.user_id as userId, users.display_name as displayName, users.user_name as userName - FROM users - JOIN user_access ON user_access.user_id = users.id - WHERE user_access.file_id = ? - UNION - SELECT files.id, users.id, users.display_name, users.user_name - FROM users - JOIN files ON files.owner = users.id - WHERE files.id = ?`, - [fileId, fileId], - ); - accessMap.set(fileId, userAccess); - }); - return Array.from(accessMap.entries()); - } + return totalChanges; +} - static getOpenIDConfig() { - return ( - getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || - {} - ); - } +export function getAllUserAccess(fileId) { + return getAccountDb().all( + `SELECT users.id as userId, user_name as userName, display_name as displayName, + CASE WHEN user_access.file_id IS NULL THEN 0 ELSE 1 END as haveAccess, + CASE WHEN files.id IS NULL THEN 0 ELSE 1 END as owner + FROM users + LEFT JOIN user_access ON user_access.file_id = ? and user_access.user_id = users.id + LEFT JOIN files ON files.id = ? and files.owner = users.id + WHERE users.enabled = 1 AND users.user_name <> ''`, + [fileId, fileId], + ); } -export default UserService; +export function getOpenIDConfig() { + return ( + getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || {} + ); +} From ea92e458abb38d7faaef53b7262d26cad91dfd6a Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 9 Oct 2024 09:59:10 -0300 Subject: [PATCH 083/139] linter --- src/app-admin.js | 4 +--- src/load-config.js | 2 +- src/services/user-service.js | 42 +++++++++++++++++++----------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index b59eb4cff..6f38432d7 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -243,9 +243,7 @@ app.post('/access', (req, res) => { return; } - if ( - UserService.countUserAccess(userAccess.fileId, userAccess.userId, false) > 0 - ) { + if (UserService.countUserAccess(userAccess.fileId, userAccess.userId) > 0) { res.status(400).send({ status: 'error', reason: 'user-already-have-access', diff --git a/src/load-config.js b/src/load-config.js index 534661649..c436a604d 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -102,7 +102,7 @@ const finalConfig = { ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() : config.loginMethod, multiuser: process.env.ACTUAL_MULTIUSER - ? ['true', '1', 'yes', 'on'].includes(process.env.ACTUAL_MULTIUSER.toLowerCase()) + ? process.env.ACTUAL_MULTIUSER.toLowerCase() === 'true' : config.multiuser, trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) diff --git a/src/services/user-service.js b/src/services/user-service.js index a665d2b77..f00872ecc 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -16,17 +16,15 @@ export function getUserById(userId) { export function getFileById(fileId) { const { id } = - getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [ - fileId, - ]) || {}; + getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [fileId]) || + {}; return id; } export function validateRole(roleId) { const { id } = - getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [ - roleId, - ]) || {}; + getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [roleId]) || + {}; return id; } @@ -98,13 +96,19 @@ export function updateUser(userId, userName, displayName, enabled) { } export function updateUserRole(userId, roleId) { - getAccountDb().mutate( - 'UPDATE user_roles SET role_id = ? WHERE user_id = ?', - [roleId, userId], - ); + getAccountDb().mutate('UPDATE user_roles SET role_id = ? WHERE user_id = ?', [ + roleId, + userId, + ]); } -export function updateUserWithRole(userId, userName, displayName, enabled, roleId) { +export function updateUserWithRole( + userId, + userName, + displayName, + enabled, + roleId, +) { getAccountDb().transaction(() => { getAccountDb().mutate( 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', @@ -118,10 +122,9 @@ export function updateUserWithRole(userId, userName, displayName, enabled, roleI } export function deleteUser(userId) { - return getAccountDb().mutate( - 'DELETE FROM users WHERE id = ? and owner = 0', - [userId], - ).changes; + return getAccountDb().mutate('DELETE FROM users WHERE id = ? and owner = 0', [ + userId, + ]).changes; } export function deleteUserRoles(userId) { @@ -129,9 +132,7 @@ export function deleteUserRoles(userId) { } export function deleteUserAccess(userId) { - getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ - userId, - ]); + getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]); } export function transferAllFilesFromUser(ownerId, oldUserId) { @@ -159,7 +160,7 @@ export function getUserAccess(fileId, userId, isAdmin) { ); } -export function countUserAccess(fileId, userId, isAdmin) { +export function countUserAccess(fileId, userId) { const { countUserAccess } = getAccountDb().first( `SELECT count(*) as countUserAccess @@ -227,6 +228,7 @@ export function getAllUserAccess(fileId) { export function getOpenIDConfig() { return ( - getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || {} + getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || + {} ); } From 880b34bd47cf107439f2e9eb9700aace57fe6702 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 9 Oct 2024 10:20:22 -0300 Subject: [PATCH 084/139] wrong logic after refactor --- src/accounts/password.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accounts/password.js b/src/accounts/password.js index 0514234bf..c5a1dea6c 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -120,7 +120,7 @@ export function loginWithPassword(password) { export function changePassword(newPassword) { let accountDb = getAccountDb(); - if (isValidPassword(newPassword)) { + if (!isValidPassword(newPassword)) { return { error: 'invalid-password' }; } From 450f78aefb03d98ca14d22cb4e75b75cca391cfa Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 9 Oct 2024 10:20:34 -0300 Subject: [PATCH 085/139] refactor query --- src/services/user-service.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index f00872ecc..d45c7897d 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -163,12 +163,17 @@ export function getUserAccess(fileId, userId, isAdmin) { export function countUserAccess(fileId, userId) { const { countUserAccess } = getAccountDb().first( - `SELECT count(*) as countUserAccess - FROM users - LEFT JOIN user_access ON user_access.user_id = users.id - JOIN files ON files.id = user_access.file_id - WHERE files.id = ? and files.owner = ? OR users.id = ?`, - [fileId, userId, userId], + `SELECT SUM(countUserAccess) as countUserAccess FROM + ( + SELECT count(*) as countUserAccess + FROM user_access + WHERE user_access.user_id = ? and user_access.file_id = ? + UNION ALL + SELECT count(*) from files + WHERE files.id = ? and files.owner = ? + ) as z + `, + [userId, fileId, fileId, userId], ) || {}; return countUserAccess || 0; @@ -229,6 +234,6 @@ export function getAllUserAccess(fileId) { export function getOpenIDConfig() { return ( getAccountDb().first(`SELECT * FROM auth WHERE method = ?`, ['openid']) || - {} + null ); } From 727dea72c46d14a170e1073d6a5f19a2295e53c8 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Wed, 9 Oct 2024 14:38:48 -0300 Subject: [PATCH 086/139] fixes --- src/accounts/openid.js | 9 +++++++++ src/app-sync.js | 1 + 2 files changed, 10 insertions(+) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 2b3f00504..f643b7837 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -3,6 +3,10 @@ import * as uuid from 'uuid'; import { generators, Issuer } from 'openid-client'; import finalConfig from '../load-config.js'; import { TOKEN_EXPIRATION_NEVER } from '../util/validate-user.js'; +import { + getUserByUsername, + transferAllFilesFromUser, +} from '../services/user-service.js'; export async function bootstrapOpenId(config) { if (!('issuer' in config)) { @@ -197,6 +201,11 @@ export async function loginWithOpenIdFinalize(body) { 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', [userId, adminRoleId], ); + + const userFromPasswordMethod = getUserByUsername(''); + if (userFromPasswordMethod) { + transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); + } } else { let { id: userIdFromDb, display_name: displayName } = accountDb.first( diff --git a/src/app-sync.js b/src/app-sync.js index a85a5d367..0004f85f5 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -381,6 +381,7 @@ app.get('/list-user-files', (req, res) => { userId: ua.user_id, userName: ua.user_name, displayName: ua.display_name, + owner: ua.user_id === row.owner, })), })), }); From 9fa99d4e19884cd50377b5551bab9309e552b681 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 11:02:18 -0300 Subject: [PATCH 087/139] changes from code review --- jest.global-setup.js | 10 ++---- migrations/1719409568000-multiuser.js | 19 +--------- src/account-db.js | 31 ++++++---------- src/accounts/openid.js | 21 ++++------- src/accounts/password.js | 16 ++------- src/app-account.js | 11 +++--- src/app-admin.js | 35 ++++++++---------- src/app-admin.test.js | 31 ++++------------ src/app-openid.js | 4 +-- src/app-secrets.js | 2 +- src/app-sync.js | 6 ++-- src/config-types.ts | 2 +- src/load-config.js | 1 + src/services/user-service.js | 52 +++++++-------------------- src/util/middlewares.js | 2 +- types/global.d.ts | 17 --------- 16 files changed, 71 insertions(+), 189 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 6fbe97e50..bb55c8f0c 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -2,7 +2,7 @@ import getAccountDb from './src/account-db.js'; import runMigrations from './src/migrations.js'; const GENERIC_ADMIN_ID = 'genericAdmin'; -const ADMIN_ROLE_ID = '213733c1-5645-46ad-8784-a7b20b400f93'; +const ADMIN_ROLE_ID = 'ADMIN'; const createUser = (userId, userName, role, owner = 0, enabled = 1) => { if (!userId || !userName || !role) { @@ -11,12 +11,8 @@ const createUser = (userId, userName, role, owner = 0, enabled = 1) => { try { getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, owner], - ); - getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, role], + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner, role], ); } catch (error) { console.error('Error creating user:', error); diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index 83471dd34..bd7b35233 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -9,24 +9,10 @@ export const up = async function () { (id TEXT PRIMARY KEY, user_name TEXT, display_name TEXT, + role TEXT, enabled INTEGER NOT NULL DEFAULT 1, owner INTEGER NOT NULL DEFAULT 0); - CREATE TABLE roles - (id TEXT PRIMARY KEY, - permissions TEXT, - name TEXT); - - INSERT INTO roles VALUES ('213733c1-5645-46ad-8784-a7b20b400f93', 'ADMINISTRATOR','Admin'); - INSERT INTO roles VALUES ('e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc', '','Basic'); - - CREATE TABLE user_roles - (user_id TEXT, - role_id TEXT, - FOREIGN KEY (user_id) REFERENCES users(id) - , FOREIGN KEY (role_id) REFERENCES roles(id) - ); - CREATE TABLE user_access (user_id TEXT, file_id TEXT, @@ -58,9 +44,6 @@ export const down = async function () { BEGIN TRANSACTION; DROP TABLE IF EXISTS user_access; - DROP TABLE IF EXISTS user_roles; - DROP TABLE IF EXISTS roles; - CREATE TABLE sessions_backup ( token TEXT PRIMARY KEY diff --git a/src/account-db.js b/src/account-db.js index 7b742fcbc..1cfae9873 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -96,11 +96,11 @@ export function isAdmin(userId) { getAccountDb().first('SELECT owner FROM users WHERE id = ?', [userId]) || {}; if (user?.owner === 1) return true; - return hasPermission(userId, 'ADMINISTRATOR'); + return hasPermission(userId, 'ADMIN'); } export function hasPermission(userId, permission) { - return getUserPermissions(userId).some((value) => value === permission); + return getUserPermission(userId) === permission; } export async function enableOpenID(loginSettings, checkFileConfig = true) { @@ -152,7 +152,6 @@ export async function disableOpenID( getAccountDb().mutate('DELETE FROM sessions'); getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?', ['']); - getAccountDb().mutate('DELETE FROM user_roles'); getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); } @@ -166,25 +165,15 @@ export function getUserInfo(userId) { return accountDb.first('SELECT * FROM users WHERE id = ?', [userId]); } -export function getUserPermissions(userId) { +export function getUserPermission(userId) { let accountDb = getAccountDb(); - const permissions = - accountDb.all( - `SELECT roles.permissions FROM users - JOIN user_roles ON user_roles.user_id = users.id - JOIN roles ON roles.id = user_roles.role_id - WHERE users.id = ?`, - [userId], - ) || []; - - const uniquePermissions = Array.from( - new Set( - permissions.flatMap((rolePermission) => - rolePermission.permissions.split(',').map((perm) => perm.trim()), - ), - ), - ); - return uniquePermissions; + const { role } = accountDb.first( + `SELECT role FROM users + WHERE users.id = ?`, + [userId], + ) || { role: '' }; + + return role; } export function clearExpiredSessions() { diff --git a/src/accounts/openid.js b/src/accounts/openid.js index f643b7837..aa085921a 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -186,20 +186,13 @@ export async function loginWithOpenIdFinalize(body) { if (countUsersWithUserName === 0) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', - [userId, identity, userInfo.name ?? userInfo.email ?? identity], - ); - - const { id: adminRoleId } = - accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']) || {}; - - if (!adminRoleId) { - return { error: 'administrator-role-not-found' }; - } - - accountDb.mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, adminRoleId], + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [ + userId, + identity, + userInfo.name ?? userInfo.email ?? identity, + 'ADMIN', + ], ); const userFromPasswordMethod = getUserByUsername(''); diff --git a/src/accounts/password.js b/src/accounts/password.js index c5a1dea6c..9109e2afa 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -62,20 +62,8 @@ export function loginWithPassword(password) { if (totalOfUsers === 0) { userId = uuid.v4(); accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, 1, 1)', - [userId, '', ''], - ); - - const { id: adminRoleId } = - accountDb.first('SELECT id FROM roles WHERE name = ?', ['Admin']) || {}; - - if (!adminRoleId) { - return { error: 'administrator-role-not-found' }; - } - - accountDb.mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, adminRoleId], + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [userId, '', '', 'ADMIN'], ); } else { let { id: userIdFromDb } = accountDb.first( diff --git a/src/app-account.js b/src/app-account.js index 701284ee7..a92eecfdc 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -10,10 +10,10 @@ import { getLoginMethod, listLoginMethods, getUserInfo, - getUserPermissions, } from './account-db.js'; import { changePassword, loginWithPassword } from './accounts/password.js'; import { loginWithOpenIdSetup } from './accounts/openid.js'; +import config from './load-config.js'; let app = express(); app.use(express.json()); @@ -31,7 +31,11 @@ export { app as handlers }; app.get('/needs-bootstrap', (req, res) => { res.send({ status: 'ok', - data: { bootstrapped: !needsBootstrap(), loginMethod: getLoginMethod() }, + data: { + bootstrapped: !needsBootstrap(), + loginMethod: getLoginMethod(), + multiuser: config.multiuser, + }, }); }); @@ -115,14 +119,13 @@ app.get('/validate', (req, res) => { let session = validateSession(req, res); if (session) { const user = getUserInfo(session.user_id); - let permissions = getUserPermissions(session.user_id); res.send({ status: 'ok', data: { validated: true, userName: user?.user_name, - permissions: permissions, + permission: user?.role, userId: session?.user_id, displayName: user?.display_name, loginMethod: session?.auth_method, diff --git a/src/app-admin.js b/src/app-admin.js index 6f38432d7..03e865c24 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -7,7 +7,6 @@ import { } from './util/middlewares.js'; import validateSession from './util/validate-user.js'; import { isAdmin } from './account-db.js'; -import config from './load-config.js'; import * as UserService from './services/user-service.js'; let app = express(); @@ -18,7 +17,7 @@ app.use(requestLoggerMiddleware); export { app as handlers }; -app.get('/ownerCreated/', (req, res) => { +app.get('/owner-created/', (req, res) => { try { const ownerCount = UserService.getOwnerCount(); res.json(ownerCount > 0); @@ -39,7 +38,7 @@ app.get('/users/', await validateSessionMiddleware, (req, res) => { }); app.post('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -86,13 +85,12 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { displayName || null, enabled ? 1 : 0, ); - UserService.insertUserRole(userId, role); res.status(200).send({ status: 'ok', data: { id: userId } }); }); app.patch('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -144,7 +142,7 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { }); app.delete('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -160,7 +158,6 @@ app.delete('/users', validateSessionMiddleware, async (req, res) => { if (item === ownerId) return; - UserService.deleteUserRoles(item); UserService.deleteUserAccess(item); UserService.transferAllFilesFromUser(ownerId, item); const usersDeleted = UserService.deleteUser(item); @@ -195,8 +192,8 @@ app.get('/access', validateSessionMiddleware, (req, res) => { const accesses = UserService.getUserAccess( fileId, - req.userSession.user_id, - isAdmin(req.userSession.user_id), + res.locals.user_id, + isAdmin(res.locals.user_id), ); res.json(accesses); @@ -309,12 +306,12 @@ app.get('/access/users', validateSessionMiddleware, async (req, res) => { const { granted } = UserService.checkFilePermission( fileId, - req.userSession.user_id, + res.locals.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(req.userSession.user_id)) { + if (granted === 0 && !isAdmin(res.locals.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', @@ -345,12 +342,12 @@ app.post( const { granted } = UserService.checkFilePermission( newUserOwner.fileId, - req.userSession.user_id, + res.locals.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(req.userSession.user_id)) { + if (granted === 0 && !isAdmin(res.locals.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', @@ -399,12 +396,12 @@ app.get('/file/owner', validateSessionMiddleware, async (req, res) => { const { granted } = UserService.checkFilePermission( fileId, - req.userSession.user_id, + res.locals.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(req.userSession.user_id)) { + if (granted === 0 && !isAdmin(res.locals.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', @@ -423,10 +420,10 @@ app.get('/file/owner', validateSessionMiddleware, async (req, res) => { return; } - let canGetOwner = isAdmin(req.userSession.user_id); + let canGetOwner = isAdmin(res.locals.user_id); if (!canGetOwner) { const fileIdOwner = UserService.getFileOwnerId(fileId); - canGetOwner = fileIdOwner === req.userSession.user_id; + canGetOwner = fileIdOwner === res.locals.user_id; } if (canGetOwner) { @@ -437,8 +434,4 @@ app.get('/file/owner', validateSessionMiddleware, async (req, res) => { return null; }); -app.get('/multiuser', (req, res) => { - res.json(config.multiuser); -}); - app.use(errorMiddleware); diff --git a/src/app-admin.test.js b/src/app-admin.test.js index 648ca4ec4..29f22ec25 100644 --- a/src/app-admin.test.js +++ b/src/app-admin.test.js @@ -3,32 +3,19 @@ import { handlers as app } from './app-admin.js'; import getAccountDb from './account-db.js'; import { v4 as uuidv4 } from 'uuid'; -const ADMIN_ROLE = '213733c1-5645-46ad-8784-a7b20b400f93'; -const BASIC_ROLE = 'e87fa1f1-ac8c-4913-b1b5-1096bdb1eacc'; - -// Create role helper to ensure roles exist before creating users -const createRole = (roleId, name, permissions = '') => { - getAccountDb().mutate( - 'INSERT OR IGNORE INTO roles (id, permissions, name) VALUES (?, ?, ?)', - [roleId, permissions, name], - ); -}; +const ADMIN_ROLE = 'ADMIN'; +const BASIC_ROLE = 'BASIC'; // Create user helper function const createUser = (userId, userName, role, owner = 0, enabled = 1) => { getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, owner], - ); - getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, role], + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner, role], ); }; const deleteUser = (userId) => { getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]); - getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); getAccountDb().mutate('DELETE FROM users WHERE id = ?', [userId]); }; @@ -41,14 +28,8 @@ const createSession = (userId, sessionToken) => { const generateSessionToken = () => `token-${uuidv4()}`; -// Ensure roles are created before each test run -beforeAll(() => { - createRole(ADMIN_ROLE, 'Admin', 'ADMINISTRATOR'); - createRole(BASIC_ROLE, 'Basic', ''); -}); - describe('/admin', () => { - describe('/ownerCreated', () => { + describe('/owner-created', () => { it('should return 200 and true if an owner user is created', async () => { const sessionToken = generateSessionToken(); const adminId = uuidv4(); @@ -56,7 +37,7 @@ describe('/admin', () => { createSession(adminId, sessionToken); const res = await request(app) - .get('/ownerCreated') + .get('/owner-created') .set('x-actual-token', sessionToken); expect(res.statusCode).toEqual(200); diff --git a/src/app-openid.js b/src/app-openid.js index 9283ddce4..3cb38c09e 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -16,7 +16,7 @@ app.use(requestLoggerMiddleware); export { app as handlers }; app.post('/enable', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -35,7 +35,7 @@ app.post('/enable', validateSessionMiddleware, async (req, res) => { }); app.post('/disable', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(req.userSession.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', diff --git a/src/app-secrets.js b/src/app-secrets.js index aac463fa9..9e81e4c31 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -20,7 +20,7 @@ app.post('/', async (req, res) => { const { name, value } = req.body; if (method === 'openid') { - let canSaveSecrets = isAdmin(req.userSession.user_id); + let canSaveSecrets = isAdmin(res.locals.user_id); if (!canSaveSecrets) { res.status(403).send({ diff --git a/src/app-sync.js b/src/app-sync.js index 0004f85f5..2fd03e4d9 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -267,7 +267,7 @@ app.post('/upload-user-file', async (req, res) => { syncFormatVersion, name, encryptMeta, - req.userSession.user_id, + res.locals.user_id, ], ); res.send({ status: 'ok', groupId }); @@ -335,7 +335,7 @@ app.post('/update-user-filename', (req, res) => { }); app.get('/list-user-files', (req, res) => { - const canSeeAll = isAdmin(req.userSession.user_id); + const canSeeAll = isAdmin(res.locals.user_id); let accountDb = getAccountDb(); let rows = canSeeAll @@ -350,7 +350,7 @@ app.get('/list-user-files', (req, res) => { JOIN user_access ON user_access.file_id = files.id AND user_access.user_id = ?`, - [req.userSession.user_id, req.userSession.user_id], + [res.locals.user_id, res.locals.user_id], ); let allUserAccess = accountDb.all( diff --git a/src/config-types.ts b/src/config-types.ts index 8db5237d6..778982d59 100644 --- a/src/config-types.ts +++ b/src/config-types.ts @@ -2,7 +2,7 @@ import { ServerOptions } from 'https'; export interface Config { mode: 'test' | 'development'; - loginMethod?: 'password' | 'header' | 'openid' | null; + loginMethod: 'password' | 'header' | 'openid'; trustedProxies: string[]; dataDir: string; projectRoot: string; diff --git a/src/load-config.js b/src/load-config.js index c436a604d..bdc29feae 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -73,6 +73,7 @@ let defaultConfig = { projectRoot, multiuser: false, token_expiration: 'never', + loginMethod: 'password', }; /** @type {import('./config-types.js').Config} */ diff --git a/src/services/user-service.js b/src/services/user-service.js index d45c7897d..5049065f2 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -22,18 +22,15 @@ export function getFileById(fileId) { } export function validateRole(roleId) { - const { id } = - getAccountDb().first('SELECT id FROM roles WHERE roles.id = ?', [roleId]) || - {}; - return id; + const possibleRoles = ['BASIC', 'ADMIN']; + return possibleRoles.some((a) => a === roleId); } export function getOwnerCount() { - const { ownerCount } = - getAccountDb().first( - `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, - ) || {}; - return ownerCount || 0; + const { ownerCount } = getAccountDb().first( + `SELECT count(*) as ownerCount FROM users WHERE users.user_name <> '' and users.owner = 1`, + ) || { ownerCount: 0 }; + return ownerCount; } export function getOwnerId() { @@ -66,25 +63,16 @@ export function getFileOwnerById(fileId) { export function getAllUsers() { return getAccountDb().all( - `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, roles.id as role + `SELECT users.id, user_name as userName, display_name as displayName, enabled, ifnull(owner,0) as owner, role FROM users - JOIN user_roles ON user_roles.user_id = users.id - JOIN roles ON roles.id = user_roles.role_id WHERE users.user_name <> ''`, ); } -export function insertUser(userId, userName, displayName, enabled) { - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner) VALUES (?, ?, ?, ?, 0)', - [userId, userName, displayName, enabled], - ); -} - -export function insertUserRole(userId, roleId) { +export function insertUser(userId, userName, displayName, enabled, role) { getAccountDb().mutate( - 'INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', - [userId, roleId], + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, 0, ?)', + [userId, userName, displayName, enabled, role], ); } @@ -95,13 +83,6 @@ export function updateUser(userId, userName, displayName, enabled) { ); } -export function updateUserRole(userId, roleId) { - getAccountDb().mutate('UPDATE user_roles SET role_id = ? WHERE user_id = ?', [ - roleId, - userId, - ]); -} - export function updateUserWithRole( userId, userName, @@ -111,12 +92,8 @@ export function updateUserWithRole( ) { getAccountDb().transaction(() => { getAccountDb().mutate( - 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', - [userName, displayName, enabled, userId], - ); - getAccountDb().mutate( - 'UPDATE user_roles SET role_id = ? WHERE user_id = ?', - [roleId, userId], + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ?, role = ? WHERE id = ?', + [userName, displayName, enabled, roleId, userId], ); }); } @@ -126,11 +103,6 @@ export function deleteUser(userId) { userId, ]).changes; } - -export function deleteUserRoles(userId) { - getAccountDb().mutate('DELETE FROM user_roles WHERE user_id = ?', [userId]); -} - export function deleteUserAccess(userId) { getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]); } diff --git a/src/util/middlewares.js b/src/util/middlewares.js index eea929d1b..c22e286fb 100644 --- a/src/util/middlewares.js +++ b/src/util/middlewares.js @@ -25,7 +25,7 @@ const validateSessionMiddleware = async (req, res, next) => { return; } - req.userSession = session; + res.locals = session; next(); }; diff --git a/types/global.d.ts b/types/global.d.ts index 64d07973e..e69de29bb 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -1,17 +0,0 @@ -// types/global.d.ts - -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Request } from 'express'; -/* eslint-enable @typescript-eslint/no-unused-vars */ - -declare global { - namespace Express { - interface Request { - userSession?: { - expires_at?: number; - token: string; - user_id?: string; - }; - } - } -} From b3cbc8b7d7adc6c9855c7d02fe7c8eeacc03033e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 12:54:09 -0300 Subject: [PATCH 088/139] added logs to toggleAuthentication --- src/account-db.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/account-db.js b/src/account-db.js index 1cfae9873..12431e6c9 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -188,18 +188,22 @@ export function clearExpiredSessions() { } export async function toggleAuthentication() { + console.log('Checking if login method changed'); if (config.loginMethod === 'openid') { const { openidIsEnabled } = getAccountDb().first( 'SELECT count(*) as openidIsEnabled FROM auth WHERE method = ? and active = 1', ['openid'], ); if (openidIsEnabled == 0) { + console.log('Enabling OpenID...'); const { error } = (await enableOpenID(config, false)) || {}; if (error) { + console.log('Error enabling OpenID:'); console.error(error); return false; } + console.log('OpenID enabled.'); } } else if (config.loginMethod) { const { enabledMethodsButOpenId } = getAccountDb().first( @@ -207,12 +211,15 @@ export async function toggleAuthentication() { ['openid'], ); if (enabledMethodsButOpenId == 0) { + console.log('Disabling OpenID...'); const { error } = (await disableOpenID(config, false)) || {}; if (error) { + console.log('Error disabling OpenID:'); console.error(error); return false; } + console.log('OpenID disabled.'); } } From fe68940db619ac3a9904e6bcc1ea1a8543327e2b Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 12:59:03 -0300 Subject: [PATCH 089/139] removed not used route --- src/app-admin.js | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index 03e865c24..989a4a15b 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -391,47 +391,4 @@ app.post( }, ); -app.get('/file/owner', validateSessionMiddleware, async (req, res) => { - const fileId = req.query.fileId; - - const { granted } = UserService.checkFilePermission( - fileId, - res.locals.user_id, - ) || { - granted: 0, - }; - - if (granted === 0 && !isAdmin(res.locals.user_id)) { - res.status(400).send({ - status: 'error', - reason: 'file-denied', - details: "You don't have permissions over this file", - }); - return; - } - - const fileIdInDb = UserService.getFileById(fileId); - if (!fileIdInDb) { - res.status(400).send({ - status: 'error', - reason: 'invalid-file-id', - details: 'File not found at server', - }); - return; - } - - let canGetOwner = isAdmin(res.locals.user_id); - if (!canGetOwner) { - const fileIdOwner = UserService.getFileOwnerId(fileId); - canGetOwner = fileIdOwner === res.locals.user_id; - } - - if (canGetOwner) { - const owner = UserService.getFileOwnerById(fileId); - res.json(owner); - } - - return null; -}); - app.use(errorMiddleware); From c220322af94f300b2e5195fe1b8ad9b1227829c3 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 13:37:44 -0300 Subject: [PATCH 090/139] merged master into the branch --- src/app-sync.js | 18 ++++----- src/app-sync/services/files-service.js | 56 +++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/app-sync.js b/src/app-sync.js index 1982fc315..a05efa56e 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -12,7 +12,7 @@ import { getPathForUserFile, getPathForGroupFile } from './util/paths.js'; import * as simpleSync from './sync-simple.js'; import { SyncProtoBuf } from '@actual-app/crdt'; -import getAccountDb, { isAdmin } from './account-db.js'; +import getAccountDb from './account-db.js'; import { File, FilesService, @@ -246,7 +246,7 @@ app.post('/upload-user-file', async (req, res) => { syncVersion: syncFormatVersion, name: name, encryptMeta: encryptMeta, - userId: res.locals.user_id, + owner: res.locals.user_id, }), ); @@ -316,14 +316,12 @@ app.get('/list-user-files', (req, res) => { name: row.name, encryptKeyId: row.encryptKeyId, owner: row.owner, - usersWithAccess: [] /*allUserAccess - .filter((ua) => ua.file_id === row.id) - .map((ua) => ({ - userId: ua.user_id, - userName: ua.user_name, - displayName: ua.display_name, - owner: ua.user_id === row.owner, - }))*/, + usersWithAccess: fileService + .findUsersWithAccess(row.id) + .map((access) => ({ + ...access, + owner: access.userId === row.owner, + })), })), }); }); diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 63fe7f373..45d8601cd 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -1,4 +1,4 @@ -import getAccountDb from '../../account-db.js'; +import getAccountDb, { isAdmin } from '../../account-db.js'; import { FileNotFound, GenericFileError } from '../errors.js'; class FileBase { @@ -11,6 +11,7 @@ class FileBase { encryptMeta, syncVersion, deleted, + owner, ) { this.name = name; this.groupId = groupId; @@ -20,6 +21,7 @@ class FileBase { this.encryptMeta = encryptMeta; this.syncVersion = syncVersion; this.deleted = typeof deleted === 'boolean' ? deleted : Boolean(deleted); + this.owner = owner; } } @@ -34,6 +36,7 @@ class File extends FileBase { encryptMeta = null, syncVersion = null, deleted = false, + owner = null, }) { super( name, @@ -44,6 +47,7 @@ class File extends FileBase { encryptMeta, syncVersion, deleted, + owner, ); this.id = id; } @@ -99,7 +103,7 @@ class FilesService { set(file) { const deletedInt = boolToInt(file.deleted); this.accountDb.mutate( - 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted) VALUES (?, ?, ?, ?, ?, ?, ?,? ,?)', + 'INSERT INTO files (id, group_id, sync_version, name, encrypt_meta, encrypt_salt, encrypt_test, encrypt_keyid, deleted, owner) VALUES (?, ?, ?, ?, ?, ?, ?, ? ,?, ?)', [ file.id, file.groupId, @@ -110,14 +114,53 @@ class FilesService { file.encrypt_test, file.encrypt_keyid, deletedInt, + file.userId, ], ); } - find(limit = 1000) { - return this.accountDb - .all('SELECT * FROM files WHERE deleted = 0 LIMIT ?', [limit]) - .map(this.validate); + find(userId, limit = 1000) { + const canSeeAll = isAdmin(userId); + + return ( + canSeeAll + ? this.accountDb.all('SELECT * FROM files WHERE deleted = 0 LIMIT ?', [ + limit, + ]) + : this.accountDb.all( + `SELECT files.* + FROM files + WHERE files.owner = ? and deleted = 0 + UNION + SELECT files.* + FROM files + JOIN user_access + ON user_access.file_id = files.id + AND user_access.user_id = ? + WHERE files.deleted = 0 LIMIT ?`, + [userId, userId, limit], + ) + ).map(this.validate); + } + + findUsersWithAccess(fileId) { + const userAccess = + this.accountDb.all( + `SELECT UA.user_id as userId, users.display_name displayName, users.user_name userName + FROM files + JOIN user_access UA ON UA.file_id = files.id + JOIN users on users.id = UA.user_id + WHERE files.id = ? + UNION ALL + SELECT users.id, users.display_name, users.user_name, files.id + FROM files + JOIN users on users.id = files.owner + WHERE files.id = ? + `, + [fileId, fileId], + ) || []; + + return userAccess; } update(id, fileUpdate) { @@ -188,6 +231,7 @@ class FilesService { encryptMeta: rawFile.encrypt_meta, syncVersion: rawFile.sync_version, deleted: Boolean(rawFile.deleted), + owner: rawFile.owner, }); } } From de1569717b54fe0039d8050f63c148e25822acce Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 14:10:09 -0300 Subject: [PATCH 091/139] removed toggleAuthenticatiomethod because you have to pass thru password config before enabling openid --- src/account-db.js | 41 +---------------------------------------- src/app.js | 3 --- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 12431e6c9..2ba706154 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -185,43 +185,4 @@ export function clearExpiredSessions() { ).changes; console.log(`Deleted ${deletedSessions} old sessions`); -} - -export async function toggleAuthentication() { - console.log('Checking if login method changed'); - if (config.loginMethod === 'openid') { - const { openidIsEnabled } = getAccountDb().first( - 'SELECT count(*) as openidIsEnabled FROM auth WHERE method = ? and active = 1', - ['openid'], - ); - if (openidIsEnabled == 0) { - console.log('Enabling OpenID...'); - const { error } = (await enableOpenID(config, false)) || {}; - - if (error) { - console.log('Error enabling OpenID:'); - console.error(error); - return false; - } - console.log('OpenID enabled.'); - } - } else if (config.loginMethod) { - const { enabledMethodsButOpenId } = getAccountDb().first( - 'SELECT count(*) as enabledMethodsButOpenId FROM auth WHERE method <> ? and active = 1', - ['openid'], - ); - if (enabledMethodsButOpenId == 0) { - console.log('Disabling OpenID...'); - const { error } = (await disableOpenID(config, false)) || {}; - - if (error) { - console.log('Error disabling OpenID:'); - console.error(error); - return false; - } - console.log('OpenID disabled.'); - } - } - - return true; -} +} \ No newline at end of file diff --git a/src/app.js b/src/app.js index bbe590c42..80504f14d 100644 --- a/src/app.js +++ b/src/app.js @@ -13,7 +13,6 @@ import * as simpleFinApp from './app-simplefin/app-simplefin.js'; import * as secretApp from './app-secrets.js'; import * as adminApp from './app-admin.js'; import * as openidApp from './app-openid.js'; -import { toggleAuthentication } from './account-db.js'; const app = express(); @@ -90,7 +89,5 @@ export default async function run() { app.listen(config.port, config.hostname); } - if (!(await toggleAuthentication())) process.exit(-1); - console.log('Listening on ' + config.hostname + ':' + config.port + '...'); } From 74612f61329edfc5a6cb4f064022986fba60b403 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 14:11:08 -0300 Subject: [PATCH 092/139] changed md file --- upcoming-release-notes/{381.md => 498.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename upcoming-release-notes/{381.md => 498.md} (100%) diff --git a/upcoming-release-notes/381.md b/upcoming-release-notes/498.md similarity index 100% rename from upcoming-release-notes/381.md rename to upcoming-release-notes/498.md From fb5ad072c0c2550894e950dcf6217b088bdd8264 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 14:11:32 -0300 Subject: [PATCH 093/139] fixes on merge --- src/app-sync.js | 2 +- src/app-sync.test.js | 16 ++++++++++++++++ src/app-sync/services/files-service.js | 8 ++++---- .../tests/services/files-service.test.js | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/app-sync.js b/src/app-sync.js index a05efa56e..fd6d2213c 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -306,7 +306,7 @@ app.post('/update-user-filename', (req, res) => { app.get('/list-user-files', (req, res) => { const fileService = new FilesService(getAccountDb()); - const rows = fileService.find(res.locals.user_id); + const rows = fileService.find({ userId: res.locals.user_id }); res.send({ status: 'ok', data: rows.map((row) => ({ diff --git a/src/app-sync.test.js b/src/app-sync.test.js index e1a6a8dd1..bc9e7ea81 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -6,6 +6,21 @@ import getAccountDb from './account-db.js'; import { SyncProtoBuf } from '@actual-app/crdt'; import crypto from 'node:crypto'; + +const ADMIN_ROLE = 'ADMIN'; + +const createUser = (userId, userName, role, owner = 0, enabled = 1) => { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', + [userId, userName, `${userName} display`, enabled, owner, role], + ); +}; + +const setSessionUser = (userId) => { + getAccountDb().mutate('UPDATE sessions SET user_id = ?', [userId]); +}; + + describe('/user-get-key', () => { it('returns 401 if the user is not authenticated', async () => { const res = await request(app).post('/user-get-key'); @@ -523,6 +538,7 @@ describe('/list-user-files', () => { }); it('returns a list of user files for an authenticated user', async () => { + createUser('fileListAdminId', 'admin', ADMIN_ROLE, 1); const fileId1 = crypto.randomBytes(16).toString('hex'); const fileId2 = crypto.randomBytes(16).toString('hex'); const fileName1 = 'file1.txt'; diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 45d8601cd..8865b3254 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -119,8 +119,8 @@ class FilesService { ); } - find(userId, limit = 1000) { - const canSeeAll = isAdmin(userId); + find({ userId = null, limit = 1000 } = {}) { + const canSeeAll = userId === null || isAdmin(userId); return ( canSeeAll @@ -150,9 +150,9 @@ class FilesService { FROM files JOIN user_access UA ON UA.file_id = files.id JOIN users on users.id = UA.user_id - WHERE files.id = ? + WHERE files.id = ? UNION ALL - SELECT users.id, users.display_name, users.user_name, files.id + SELECT users.id, users.display_name, users.user_name FROM files JOIN users on users.id = files.owner WHERE files.id = ? diff --git a/src/app-sync/tests/services/files-service.test.js b/src/app-sync/tests/services/files-service.test.js index 7d3dadd6d..f9a8ab773 100644 --- a/src/app-sync/tests/services/files-service.test.js +++ b/src/app-sync/tests/services/files-service.test.js @@ -156,7 +156,7 @@ describe('FilesService', () => { expect(allFiles.length).toBe(2); // Limit the number of files returned - const limitedFiles = filesService.find(1); + const limitedFiles = filesService.find({ limit: 1 }); expect(limitedFiles.length).toBe(1); }); From 508a4dbe2517c075d5c61b1982335a0507f11d20 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 14:12:28 -0300 Subject: [PATCH 094/139] linter --- src/account-db.js | 2 +- src/app-sync.test.js | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 2ba706154..e4a9e3dde 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -185,4 +185,4 @@ export function clearExpiredSessions() { ).changes; console.log(`Deleted ${deletedSessions} old sessions`); -} \ No newline at end of file +} diff --git a/src/app-sync.test.js b/src/app-sync.test.js index bc9e7ea81..0b77ebeed 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -6,7 +6,6 @@ import getAccountDb from './account-db.js'; import { SyncProtoBuf } from '@actual-app/crdt'; import crypto from 'node:crypto'; - const ADMIN_ROLE = 'ADMIN'; const createUser = (userId, userName, role, owner = 0, enabled = 1) => { @@ -16,11 +15,6 @@ const createUser = (userId, userName, role, owner = 0, enabled = 1) => { ); }; -const setSessionUser = (userId) => { - getAccountDb().mutate('UPDATE sessions SET user_id = ?', [userId]); -}; - - describe('/user-get-key', () => { it('returns 401 if the user is not authenticated', async () => { const res = await request(app).post('/user-get-key'); From 65b6872bb3781c23f316dc98ea048a16bab5ada1 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 14:15:59 -0300 Subject: [PATCH 095/139] fix on tests --- src/app-sync/tests/services/files-service.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app-sync/tests/services/files-service.test.js b/src/app-sync/tests/services/files-service.test.js index f9a8ab773..4a72bc6e8 100644 --- a/src/app-sync/tests/services/files-service.test.js +++ b/src/app-sync/tests/services/files-service.test.js @@ -28,6 +28,7 @@ describe('FilesService', () => { }; const clearDatabase = () => { + accountDb.mutate('DELETE FROM user_access'); accountDb.mutate('DELETE FROM files'); }; From 2dd2170089ec2f8728fc400506532fb1eed46252 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 18:13:36 -0300 Subject: [PATCH 096/139] more fixes --- src/account-db.js | 13 ++----------- src/accounts/openid.js | 27 ++++++++++++-------------- src/app-account.js | 2 +- src/app-openid.js | 2 +- src/app-sync.js | 12 ++++++------ src/app-sync.test.js | 1 + src/app-sync/services/files-service.js | 8 ++++++++ 7 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index e4a9e3dde..a10d28584 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -51,7 +51,7 @@ export function getLoginMethod(req) { export async function bootstrap(loginSettings) { const passEnabled = 'password' in loginSettings; - const openIdEnabled = 'openid' in loginSettings; + const openIdEnabled = 'openId' in loginSettings; const { countOfOwner } = getAccountDb().first( @@ -103,11 +103,7 @@ export function hasPermission(userId, permission) { return getUserPermission(userId) === permission; } -export async function enableOpenID(loginSettings, checkFileConfig = true) { - if (checkFileConfig && config.loginMethod) { - return { error: 'unable-to-change-file-config-enabled' }; - } - +export async function enableOpenID(loginSettings) { let { error } = (await bootstrapOpenId(loginSettings.openId)) || {}; if (error) { return { error }; @@ -118,13 +114,8 @@ export async function enableOpenID(loginSettings, checkFileConfig = true) { export async function disableOpenID( loginSettings, - checkFileConfig = true, checkForOldPassword = false, ) { - if (checkFileConfig && config.loginMethod) { - return { error: 'unable-to-change-file-config-enabled' }; - } - if (checkForOldPassword) { let accountDb = getAccountDb(); const { extra_data: passwordHash } = diff --git a/src/accounts/openid.js b/src/accounts/openid.js index aa085921a..82b42a9d0 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -28,22 +28,19 @@ export async function bootstrapOpenId(config) { return { error: 'configuration-error' }; } - getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); - - // Beyond verifying that the configuration exists, we do not attempt - // to check if the configuration is actually correct. - // If the user improperly configures this during bootstrap, there is - // no way to recover without manually editing the database. However, - // this might not be a real issue since an analogous situation happens - // if they forget their password. let accountDb = getAccountDb(); - accountDb.transaction(() => { - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", - [JSON.stringify(config)], - ); - }); + //accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); + + console.log(accountDb.all('select * from auth')); + //}); + + console.log(accountDb.all('select * from auth')); return {}; } diff --git a/src/app-account.js b/src/app-account.js index a92eecfdc..5c7388eb0 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -33,7 +33,7 @@ app.get('/needs-bootstrap', (req, res) => { status: 'ok', data: { bootstrapped: !needsBootstrap(), - loginMethod: getLoginMethod(), + loginMethods: listLoginMethods(), multiuser: config.multiuser, }, }); diff --git a/src/app-openid.js b/src/app-openid.js index 3cb38c09e..e5f9939b3 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -44,7 +44,7 @@ app.post('/disable', validateSessionMiddleware, async (req, res) => { return; } - let { error } = (await disableOpenID(req.body, true, true)) || {}; + let { error } = (await disableOpenID(req.body, true)) || {}; if (error) { res.status(500).send({ status: 'error', reason: error }); diff --git a/src/app-sync.js b/src/app-sync.js index fd6d2213c..417f1c139 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -316,12 +316,6 @@ app.get('/list-user-files', (req, res) => { name: row.name, encryptKeyId: row.encryptKeyId, owner: row.owner, - usersWithAccess: fileService - .findUsersWithAccess(row.id) - .map((access) => ({ - ...access, - owner: access.userId === row.owner, - })), })), }); }); @@ -357,6 +351,12 @@ app.get('/get-user-file-info', (req, res) => { groupId: file.groupId, name: file.name, encryptMeta: file.encryptMeta ? JSON.parse(file.encryptMeta) : null, + usersWithAccess: fileService + .findUsersWithAccess(file.id) + .map((access) => ({ + ...access, + owner: access.userId === file.owner, + })), }, }); }); diff --git a/src/app-sync.test.js b/src/app-sync.test.js index 0b77ebeed..3d3dd10bd 100644 --- a/src/app-sync.test.js +++ b/src/app-sync.test.js @@ -616,6 +616,7 @@ describe('/get-user-file-info', () => { groupId: fileInfo.group_id, name: fileInfo.name, encryptMeta: { key: 'value' }, + usersWithAccess: [], }, }); }); diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 8865b3254..e706e9b55 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -12,6 +12,7 @@ class FileBase { syncVersion, deleted, owner, + usersWithAccess, ) { this.name = name; this.groupId = groupId; @@ -22,6 +23,7 @@ class FileBase { this.syncVersion = syncVersion; this.deleted = typeof deleted === 'boolean' ? deleted : Boolean(deleted); this.owner = owner; + this.usersWithAccess = usersWithAccess; } } @@ -37,6 +39,7 @@ class File extends FileBase { syncVersion = null, deleted = false, owner = null, + usersWithAccess = [], }) { super( name, @@ -48,6 +51,7 @@ class File extends FileBase { syncVersion, deleted, owner, + usersWithAccess, ); this.id = id; } @@ -232,6 +236,10 @@ class FilesService { syncVersion: rawFile.sync_version, deleted: Boolean(rawFile.deleted), owner: rawFile.owner, + usersWithAccess: this.findUsersWithAccess(rawFile.id).map((access) => ({ + ...access, + owner: access.userId === rawFile.owner, + })), }); } } From 95fa6544b1c54b697ad90e0f39d6006c75c4a364 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 18:24:23 -0300 Subject: [PATCH 097/139] fix --- src/app-sync/services/files-service.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index e706e9b55..26b327bdd 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -12,7 +12,6 @@ class FileBase { syncVersion, deleted, owner, - usersWithAccess, ) { this.name = name; this.groupId = groupId; @@ -23,7 +22,6 @@ class FileBase { this.syncVersion = syncVersion; this.deleted = typeof deleted === 'boolean' ? deleted : Boolean(deleted); this.owner = owner; - this.usersWithAccess = usersWithAccess; } } @@ -39,7 +37,6 @@ class File extends FileBase { syncVersion = null, deleted = false, owner = null, - usersWithAccess = [], }) { super( name, @@ -51,7 +48,6 @@ class File extends FileBase { syncVersion, deleted, owner, - usersWithAccess, ); this.id = id; } @@ -118,7 +114,7 @@ class FilesService { file.encrypt_test, file.encrypt_keyid, deletedInt, - file.userId, + file.owner, ], ); } @@ -236,10 +232,6 @@ class FilesService { syncVersion: rawFile.sync_version, deleted: Boolean(rawFile.deleted), owner: rawFile.owner, - usersWithAccess: this.findUsersWithAccess(rawFile.id).map((access) => ({ - ...access, - owner: access.userId === rawFile.owner, - })), }); } } From e7680b8ade2ff89784947635316fb9f4c6e12127 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 18:34:02 -0300 Subject: [PATCH 098/139] another fix --- src/app-sync.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app-sync.js b/src/app-sync.js index 417f1c139..cd386bd55 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -316,6 +316,12 @@ app.get('/list-user-files', (req, res) => { name: row.name, encryptKeyId: row.encryptKeyId, owner: row.owner, + usersWithAccess: fileService + .findUsersWithAccess(row.id) + .map((access) => ({ + ...access, + owner: access.userId === row.owner, + })), })), }); }); From 490f0107275c2e52b87bf81436aaae9407fabc3f Mon Sep 17 00:00:00 2001 From: lelemm Date: Fri, 8 Nov 2024 19:38:26 -0300 Subject: [PATCH 099/139] Update src/app-admin.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/app-admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app-admin.js b/src/app-admin.js index 989a4a15b..1a936ca13 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -26,7 +26,7 @@ app.get('/owner-created/', (req, res) => { } }); -app.get('/users/', await validateSessionMiddleware, (req, res) => { +app.get('/users/', validateSessionMiddleware, (req, res) => { const users = UserService.getAllUsers(); res.json( users.map((u) => ({ From 1ccf2ae198810dd6f8150baa094bef4e2c027eb5 Mon Sep 17 00:00:00 2001 From: lelemm Date: Fri, 8 Nov 2024 19:39:27 -0300 Subject: [PATCH 100/139] Update jest.global-setup.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- jest.global-setup.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index bb55c8f0c..28706b659 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -5,17 +5,25 @@ const GENERIC_ADMIN_ID = 'genericAdmin'; const ADMIN_ROLE_ID = 'ADMIN'; const createUser = (userId, userName, role, owner = 0, enabled = 1) => { - if (!userId || !userName || !role) { - throw new Error('Missing required parameters'); + const missingParams = []; + if (!userId) missingParams.push('userId'); + if (!userName) missingParams.push('userName'); + if (!role) missingParams.push('role'); + if (missingParams.length > 0) { + throw new Error(`Missing required parameters: ${missingParams.join(', ')}`); + } + + if (typeof userId !== 'string' || typeof userName !== 'string' || typeof role !== 'string') { + throw new Error('Invalid parameter types. userId, userName, and role must be strings'); } try { getAccountDb().mutate( 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, ?, ?)', - [userId, userName, `${userName} display`, enabled, owner, role], + [userId, userName, userName, enabled, owner, role], ); } catch (error) { - console.error('Error creating user:', error); + console.error(`Error creating user ${userName}:`, error); throw error; } }; From 097c5cbe4269c28e525b4d5b8d9a5ce4a89bc7fb Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 19:47:08 -0300 Subject: [PATCH 101/139] code rabbit reviews --- src/app-admin.js | 1 - src/app-openid.js | 3 +- src/app-secrets.js | 2 +- src/app-sync/services/files-service.js | 2 +- src/scripts/reset-password.js | 40 +++++++++++++++++--------- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/app-admin.js b/src/app-admin.js index 989a4a15b..45e438b32 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -12,7 +12,6 @@ import * as UserService from './services/user-service.js'; let app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use(errorMiddleware); app.use(requestLoggerMiddleware); export { app as handlers }; diff --git a/src/app-openid.js b/src/app-openid.js index e5f9939b3..2cdca23f9 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -11,7 +11,6 @@ import * as UserService from './services/user-service.js'; let app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use(errorMiddleware); app.use(requestLoggerMiddleware); export { app as handlers }; @@ -89,3 +88,5 @@ app.get('/callback', async (req, res) => { res.redirect(url); }); + +app.use(errorMiddleware); diff --git a/src/app-secrets.js b/src/app-secrets.js index 9e81e4c31..d95e94ea8 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -29,7 +29,7 @@ app.post('/', async (req, res) => { details: 'You have to be admin to set secrets', }); - return null; + return; } } diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 26b327bdd..8865b3254 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -114,7 +114,7 @@ class FilesService { file.encrypt_test, file.encrypt_keyid, deletedInt, - file.owner, + file.userId, ], ); } diff --git a/src/scripts/reset-password.js b/src/scripts/reset-password.js index 89950af63..142269a9f 100644 --- a/src/scripts/reset-password.js +++ b/src/scripts/reset-password.js @@ -7,31 +7,45 @@ if (needsBootstrap()) { 'It looks like you don’t have a password set yet. Let’s set one up now!', ); - promptPassword().then(async (password) => { - let { error } = await bootstrap({ password }); + try { + const password = await promptPassword(); + const { error } = await bootstrap({ password }); if (error) { console.log('Error setting password:', error); console.log( 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', ); - } else { - console.log('Password set!'); + process.exit(1); } - }); + console.log('Password set!'); + } catch (err) { + console.log('Unexpected error:', err); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(1); + } } else { console.log('It looks like you already have a password set. Let’s reset it!'); - promptPassword().then((password) => { - let { error } = changePassword(password); + try { + const password = await promptPassword(); + const { error } = await changePassword(password); if (error) { console.log('Error changing password:', error); console.log( 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', ); - } else { - console.log('Password changed!'); - console.log( - 'Note: you will need to log in with the new password on any browsers or devices that are currently logged in.', - ); + process.exit(1); } - }); + console.log('Password changed!'); + console.log( + 'Note: you will need to log in with the new password on any browsers or devices that are currently logged in.', + ); + } catch (err) { + console.log('Unexpected error:', err); + console.log( + 'Please report this as an issue: https://github.com/actualbudget/actual-server/issues', + ); + process.exit(1); + } } From a96ae90124a9ff5322c336ff44ac2ea4b65c28a8 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Fri, 8 Nov 2024 19:49:33 -0300 Subject: [PATCH 102/139] linter --- jest.global-setup.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 28706b659..072dfc68d 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -12,9 +12,15 @@ const createUser = (userId, userName, role, owner = 0, enabled = 1) => { if (missingParams.length > 0) { throw new Error(`Missing required parameters: ${missingParams.join(', ')}`); } - - if (typeof userId !== 'string' || typeof userName !== 'string' || typeof role !== 'string') { - throw new Error('Invalid parameter types. userId, userName, and role must be strings'); + + if ( + typeof userId !== 'string' || + typeof userName !== 'string' || + typeof role !== 'string' + ) { + throw new Error( + 'Invalid parameter types. userId, userName, and role must be strings', + ); } try { From 37b639d5374136d82e154b0941b9a7e623ab5f85 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 09:39:52 -0300 Subject: [PATCH 103/139] Update migrations/1719409568000-multiuser.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- migrations/1719409568000-multiuser.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index bd7b35233..c39ffe03f 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -16,8 +16,9 @@ export const up = async function () { CREATE TABLE user_access (user_id TEXT, file_id TEXT, - FOREIGN KEY (user_id) REFERENCES users(id) - , FOREIGN KEY (file_id) REFERENCES files(id) + PRIMARY KEY (user_id, file_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (file_id) REFERENCES files(id) ); ALTER TABLE files From 859bec887b1aa5c00d2d03176d07c6ef263a0c7f Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 09:40:09 -0300 Subject: [PATCH 104/139] Update migrations/1719409568000-multiuser.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- migrations/1719409568000-multiuser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/1719409568000-multiuser.js b/migrations/1719409568000-multiuser.js index c39ffe03f..345da8ebd 100644 --- a/migrations/1719409568000-multiuser.js +++ b/migrations/1719409568000-multiuser.js @@ -30,10 +30,10 @@ export const up = async function () { ADD COLUMN expires_at INTEGER; ALTER TABLE sessions - ADD user_id TEXT; + ADD COLUMN user_id TEXT; ALTER TABLE sessions - ADD auth_method TEXT; + ADD COLUMN auth_method TEXT; COMMIT; `, ); From c3d3dffccfa680115bc1e7da9588666222d6ef5c Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 09:40:44 -0300 Subject: [PATCH 105/139] Update src/account-db.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/account-db.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/account-db.js b/src/account-db.js index a10d28584..adf245fe3 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -50,6 +50,9 @@ export function getLoginMethod(req) { } export async function bootstrap(loginSettings) { + if (!loginSettings) { + return { error: 'invalid-login-settings' }; + } const passEnabled = 'password' in loginSettings; const openIdEnabled = 'openId' in loginSettings; From b33da56113f1d98bacb93dfbcd21c6d9e4782684 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 09:42:10 -0300 Subject: [PATCH 106/139] Update src/accounts/openid.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/accounts/openid.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 82b42a9d0..55642f4a0 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -25,6 +25,7 @@ export async function bootstrapOpenId(config) { try { await setupOpenIdClient(config); } catch (err) { + console.error('Error setting up OpenID client:', err); return { error: 'configuration-error' }; } From e49e391c5edf09c9a5acdbb4b862427355801c34 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 09:43:04 -0300 Subject: [PATCH 107/139] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/accounts/openid.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 55642f4a0..af5a7bafc 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -30,7 +30,7 @@ export async function bootstrapOpenId(config) { } let accountDb = getAccountDb(); - //accountDb.transaction(() => { + accountDb.transaction(() => { accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); accountDb.mutate('UPDATE auth SET active = 0'); accountDb.mutate( @@ -39,7 +39,7 @@ export async function bootstrapOpenId(config) { ); console.log(accountDb.all('select * from auth')); - //}); + }); console.log(accountDb.all('select * from auth')); @@ -83,7 +83,8 @@ export async function loginWithOpenIdSetup(body) { try { config = JSON.parse(config['extra_data']); } catch (err) { - return { error: 'openid-setup-failed: ' + err }; + console.error('Error parsing OpenID configuration:', err); + return { error: 'openid-setup-failed' }; } let client; @@ -138,13 +139,15 @@ export async function loginWithOpenIdFinalize(body) { try { config = JSON.parse(config['extra_data']); } catch (err) { - return { error: 'openid-setup-failed: ' + err }; + console.error('Error parsing OpenID configuration:', err); + return { error: 'openid-setup-failed' }; } let client; try { client = await setupOpenIdClient(config); } catch (err) { - return { error: 'openid-setup-failed: ' + err }; + console.error('Error setting up OpenID client:', err); + return { error: 'openid-setup-failed' }; } let pendingRequest = accountDb.first( @@ -159,12 +162,8 @@ export async function loginWithOpenIdFinalize(body) { let { code_verifier, return_url } = pendingRequest; try { - let grant = await client.grant({ - grant_type: 'authorization_code', - code: body.code, - code_verifier, - redirect_uri: client.redirect_uris[0], - }); + const params = { code: body.code, state: body.state }; + let tokenSet = await client.callback(client.redirect_uris[0], params, { code_verifier }); const userInfo = await client.userinfo(grant); const identity = userInfo.preferred_username ?? From e56b8d81d0a2e033e2907407564bec5f268b20d6 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 09:44:15 -0300 Subject: [PATCH 108/139] fix on code suggestion --- src/accounts/openid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index af5a7bafc..cdb42cb22 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -164,7 +164,7 @@ export async function loginWithOpenIdFinalize(body) { try { const params = { code: body.code, state: body.state }; let tokenSet = await client.callback(client.redirect_uris[0], params, { code_verifier }); - const userInfo = await client.userinfo(grant); + const userInfo = await client.userinfo(tokenSet.access_token); const identity = userInfo.preferred_username ?? userInfo.login ?? From 78c12420f129f519151c390cac09b5ec069a67ea Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 09:45:47 -0300 Subject: [PATCH 109/139] suggestion from coderabbit --- src/accounts/openid.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index cdb42cb22..64ffe668c 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -31,14 +31,14 @@ export async function bootstrapOpenId(config) { let accountDb = getAccountDb(); accountDb.transaction(() => { - accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", - [JSON.stringify(config)], - ); + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); - console.log(accountDb.all('select * from auth')); + console.log(accountDb.all('select * from auth')); }); console.log(accountDb.all('select * from auth')); @@ -163,14 +163,17 @@ export async function loginWithOpenIdFinalize(body) { try { const params = { code: body.code, state: body.state }; - let tokenSet = await client.callback(client.redirect_uris[0], params, { code_verifier }); + let tokenSet = await client.callback(client.redirect_uris[0], params, { + code_verifier, + }); const userInfo = await client.userinfo(tokenSet.access_token); const identity = userInfo.preferred_username ?? userInfo.login ?? userInfo.email ?? userInfo.id ?? - userInfo.name; + userInfo.name ?? + 'default-username'; if (identity == null) { return { error: 'openid-grant-failed: no identification was found' }; } From f730a7302544888c4a3c0222d2c8f3fcb886246f Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 10:00:07 -0300 Subject: [PATCH 110/139] linter --- src/accounts/openid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 64ffe668c..12948a850 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -224,7 +224,7 @@ export async function loginWithOpenIdFinalize(body) { let expiration; if (finalConfig.token_expiration === 'openid-provider') { - expiration = grant.expires_at ?? TOKEN_EXPIRATION_NEVER; + expiration = tokenSet.expires_at ?? TOKEN_EXPIRATION_NEVER; } else if (finalConfig.token_expiration === 'never') { expiration = TOKEN_EXPIRATION_NEVER; } else if (typeof finalConfig.token_expiration === 'number') { From 20d8e40e4877d916a53c69c5d4318b6fad3398cf Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 10:06:31 -0300 Subject: [PATCH 111/139] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/accounts/openid.js | 3 ++- src/app-sync.js | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 12948a850..c0d353f46 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -244,6 +244,7 @@ export async function loginWithOpenIdFinalize(body) { return { url: `${return_url}/openid-cb?token=${token}` }; } catch (err) { console.error('OpenID grant failed:', err); - return { error: 'openid-grant-failed: ' + err }; + console.error('OpenID grant failed:', err); + return { error: 'openid-grant-failed' }; } } diff --git a/src/app-sync.js b/src/app-sync.js index cd386bd55..260442207 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -246,7 +246,11 @@ app.post('/upload-user-file', async (req, res) => { syncVersion: syncFormatVersion, name: name, encryptMeta: encryptMeta, - owner: res.locals.user_id, + encryptMeta: encryptMeta, + // Ensure user_id exists before setting owner + owner: res.locals.user_id || (() => { + throw new Error('User ID is required for file creation'); + })(), }), ); From d0b21ab46e51ff98beb575284f7b971f59d010b5 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 10:07:25 -0300 Subject: [PATCH 112/139] linter and code review --- src/app-openid.js | 3 ++- src/app-sync.js | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app-openid.js b/src/app-openid.js index 2cdca23f9..3a38358af 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -82,7 +82,8 @@ app.get('/config', async (req, res) => { app.get('/callback', async (req, res) => { let { error, url } = await loginWithOpenIdFinalize(req.query); if (error) { - res.status(400).send({ error }); + console.error('OpenID Callback Error:', error); + res.status(400).send({ status: 'error', reason: 'Invalid request' }); return; } diff --git a/src/app-sync.js b/src/app-sync.js index 260442207..f97f859a5 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -246,11 +246,11 @@ app.post('/upload-user-file', async (req, res) => { syncVersion: syncFormatVersion, name: name, encryptMeta: encryptMeta, - encryptMeta: encryptMeta, - // Ensure user_id exists before setting owner - owner: res.locals.user_id || (() => { - throw new Error('User ID is required for file creation'); - })(), + owner: + res.locals.user_id || + (() => { + throw new Error('User ID is required for file creation'); + })(), }), ); From b9cc5a596b6c7c99995e2411eafd32d98df053c8 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 10:17:05 -0300 Subject: [PATCH 113/139] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/services/user-service.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index 5049065f2..39da767ac 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -133,22 +133,18 @@ export function getUserAccess(fileId, userId, isAdmin) { } export function countUserAccess(fileId, userId) { - const { countUserAccess } = + const { accessCount } = getAccountDb().first( - `SELECT SUM(countUserAccess) as countUserAccess FROM - ( - SELECT count(*) as countUserAccess - FROM user_access - WHERE user_access.user_id = ? and user_access.file_id = ? - UNION ALL - SELECT count(*) from files - WHERE files.id = ? and files.owner = ? - ) as z - `, - [userId, fileId, fileId, userId], + `SELECT COUNT(*) as accessCount + FROM files + WHERE files.id = ? AND (files.owner = ? OR EXISTS ( + SELECT 1 FROM user_access + WHERE user_access.user_id = ? AND user_access.file_id = ?) + )`, + [fileId, userId, userId, fileId], ) || {}; - return countUserAccess || 0; + return accessCount || 0; } export function checkFilePermission(fileId, userId) { From f6273a639e342b99978b5c1d2a1beb79ef1e002a Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 10:38:36 -0300 Subject: [PATCH 114/139] Update src/util/middlewares.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/util/middlewares.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/util/middlewares.js b/src/util/middlewares.js index f8d6b4f18..e606e9688 100644 --- a/src/util/middlewares.js +++ b/src/util/middlewares.js @@ -31,15 +31,26 @@ async function errorMiddleware(err, req, res, next) { * @param {import('express').Response} res * @param {import('express').NextFunction} next */ +/** + * Middleware to validate session and attach it to response locals + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ const validateSessionMiddleware = async (req, res, next) => { let session = await validateSession(req, res); if (!session) { + res.status(401).json({ + status: 'error', + reason: 'invalid-session' + }); return; } - res.locals = session; + res.locals.session = session; next(); }; +}; const requestLoggerMiddleware = expressWinston.logger({ transports: [new winston.transports.Console()], From 3111644c8c0ba18512109e3bcb26abfb3f5ace68 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 11:59:35 -0300 Subject: [PATCH 115/139] fixes, ai code review, linter --- jest.global-setup.js | 25 ++++++++++++--- src/account-db.js | 12 ++++++++ src/accounts/openid.js | 44 +++++++++++++++++++++++++-- src/accounts/password.js | 4 +++ src/app-account.js | 15 +++++++-- src/app-admin.js | 18 +++++------ src/app-openid.js | 18 ++++++++--- src/app-secrets.js | 2 +- src/app-sync.js | 4 +-- src/load-config.js | 64 +++++++++++++++++---------------------- src/util/middlewares.js | 5 ++- src/util/validate-user.js | 6 ++-- 12 files changed, 150 insertions(+), 67 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 072dfc68d..075fc38bc 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -35,10 +35,27 @@ const createUser = (userId, userName, role, owner = 0, enabled = 1) => { }; const setSessionUser = (userId, token = 'valid-token') => { - getAccountDb().mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ - userId, - token, - ]); + if (!userId) { + throw new Error('userId is required'); + } + + try { + const db = getAccountDb(); + const session = db.get('SELECT token FROM sessions WHERE token = ?', [ + token, + ]); + if (!session) { + throw new Error(`Session not found for token: ${token}`); + } + + getAccountDb().mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ + userId, + token, + ]); + } catch (error) { + console.error(`Error updating session for user ${userId}:`, error); + throw error; + } }; export default async function setup() { diff --git a/src/account-db.js b/src/account-db.js index adf245fe3..90e68c7ff 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -107,6 +107,10 @@ export function hasPermission(userId, permission) { } export async function enableOpenID(loginSettings) { + if (!loginSettings || !loginSettings.openId) { + return { error: 'invalid-login-settings' }; + } + let { error } = (await bootstrapOpenId(loginSettings.openId)) || {}; if (error) { return { error }; @@ -119,6 +123,10 @@ export async function disableOpenID( loginSettings, checkForOldPassword = false, ) { + if (!loginSettings || !loginSettings.password) { + return { error: 'invalid-login-settings' }; + } + if (checkForOldPassword) { let accountDb = getAccountDb(); const { extra_data: passwordHash } = @@ -126,6 +134,10 @@ export async function disableOpenID( 'password', ]) || {}; + if (!passwordHash) { + return { error: 'invalid-password' }; + } + if (!loginSettings?.password) { return { error: 'invalid-password' }; } diff --git a/src/accounts/openid.js b/src/accounts/openid.js index c0d353f46..a635594da 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -67,8 +67,8 @@ async function setupOpenIdClient(config) { return client; } -export async function loginWithOpenIdSetup(body) { - if (!body.return_url) { +export async function loginWithOpenIdSetup(returnUrl) { + if (!returnUrl) { return { error: 'return-url-missing' }; } @@ -107,7 +107,7 @@ export async function loginWithOpenIdSetup(body) { ); accountDb.mutate( 'INSERT INTO pending_openid_requests (state, code_verifier, return_url, expiry_time) VALUES (?, ?, ?, ?)', - [state, code_verifier, body.return_url, expiry_time], + [state, code_verifier, returnUrl, expiry_time], ); const url = client.authorizationUrl({ @@ -165,6 +165,7 @@ export async function loginWithOpenIdFinalize(body) { const params = { code: body.code, state: body.state }; let tokenSet = await client.callback(client.redirect_uris[0], params, { code_verifier, + state: body.state, }); const userInfo = await client.userinfo(tokenSet.access_token); const identity = @@ -248,3 +249,40 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-grant-failed' }; } } + +export function getServerHostname() { + const auth = getAccountDb().first( + 'select * from auth WHERE method = ? and active = 1', + ['openid'], + ); + if (auth && auth.extra_data) { + try { + const openIdConfig = JSON.parse(auth.extra_data); + return openIdConfig.server_hostname; + } catch (error) { + console.error('Error parsing OpenID configuration:', error); + } + } + return null; +} + +export function isValidRedirectUrl(url) { + const serverHostname = getServerHostname(); + + if (!serverHostname) { + return false; + } + + try { + const redirectUrl = new URL(url); + const serverUrl = new URL(serverHostname); + + if (redirectUrl.hostname === serverUrl.hostname) { + return true; + } else { + return false; + } + } catch (err) { + return false; + } +} diff --git a/src/accounts/password.js b/src/accounts/password.js index 9109e2afa..e841697a7 100644 --- a/src/accounts/password.js +++ b/src/accounts/password.js @@ -42,6 +42,10 @@ export function loginWithPassword(password) { 'password', ]) || {}; + if (!passwordHash) { + return { error: 'invalid-password' }; + } + let confirmed = bcrypt.compareSync(password, passwordHash); if (!confirmed) { diff --git a/src/app-account.js b/src/app-account.js index 5c7388eb0..abf577407 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -12,7 +12,7 @@ import { getUserInfo, } from './account-db.js'; import { changePassword, loginWithPassword } from './accounts/password.js'; -import { loginWithOpenIdSetup } from './accounts/openid.js'; +import { isValidRedirectUrl, loginWithOpenIdSetup } from './accounts/openid.js'; import config from './load-config.js'; let app = express(); @@ -78,7 +78,14 @@ app.post('/login', async (req, res) => { break; } case 'openid': { - let { error, url } = await loginWithOpenIdSetup(req.body); + if (!isValidRedirectUrl(req.body.return_url)) { + res + .status(400) + .send({ status: 'error', reason: 'Invalid redirect URL' }); + return; + } + + let { error, url } = await loginWithOpenIdSetup(req.body.return_url); if (error) { res.status(400).send({ status: 'error', reason: error }); return; @@ -119,6 +126,10 @@ app.get('/validate', (req, res) => { let session = validateSession(req, res); if (session) { const user = getUserInfo(session.user_id); + if (!user) { + res.status(400).send({ status: 'error', reason: 'User not found' }); + return; + } res.send({ status: 'ok', diff --git a/src/app-admin.js b/src/app-admin.js index 34891200a..27589d096 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -37,7 +37,7 @@ app.get('/users/', validateSessionMiddleware, (req, res) => { }); app.post('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.user_id)) { + if (!isAdmin(res.locals.session.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -89,7 +89,7 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { }); app.patch('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.user_id)) { + if (!isAdmin(res.locals.session.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -141,7 +141,7 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { }); app.delete('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.user_id)) { + if (!isAdmin(res.locals.session.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -191,8 +191,8 @@ app.get('/access', validateSessionMiddleware, (req, res) => { const accesses = UserService.getUserAccess( fileId, - res.locals.user_id, - isAdmin(res.locals.user_id), + res.locals.session.user_id, + isAdmin(res.locals.session.user_id), ); res.json(accesses); @@ -305,12 +305,12 @@ app.get('/access/users', validateSessionMiddleware, async (req, res) => { const { granted } = UserService.checkFilePermission( fileId, - res.locals.user_id, + res.locals.session.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(res.locals.user_id)) { + if (granted === 0 && !isAdmin(res.locals.session.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', @@ -341,12 +341,12 @@ app.post( const { granted } = UserService.checkFilePermission( newUserOwner.fileId, - res.locals.user_id, + res.locals.session.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(res.locals.user_id)) { + if (granted === 0 && !isAdmin(res.locals.session.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', diff --git a/src/app-openid.js b/src/app-openid.js index 3a38358af..a4c9ce78c 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -5,7 +5,10 @@ import { validateSessionMiddleware, } from './util/middlewares.js'; import { disableOpenID, enableOpenID, isAdmin } from './account-db.js'; -import { loginWithOpenIdFinalize } from './accounts/openid.js'; +import { + isValidRedirectUrl, + loginWithOpenIdFinalize, +} from './accounts/openid.js'; import * as UserService from './services/user-service.js'; let app = express(); @@ -15,7 +18,7 @@ app.use(requestLoggerMiddleware); export { app as handlers }; app.post('/enable', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.user_id)) { + if (!isAdmin(res.locals.session.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -34,7 +37,7 @@ app.post('/enable', validateSessionMiddleware, async (req, res) => { }); app.post('/disable', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.user_id)) { + if (!isAdmin(res.locals.session.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -81,9 +84,14 @@ app.get('/config', async (req, res) => { app.get('/callback', async (req, res) => { let { error, url } = await loginWithOpenIdFinalize(req.query); + if (error) { - console.error('OpenID Callback Error:', error); - res.status(400).send({ status: 'error', reason: 'Invalid request' }); + res.status(400).send({ status: 'error', reason: error }); + return; + } + + if (!isValidRedirectUrl(url)) { + res.status(400).send({ status: 'error', reason: 'Invalid redirect URL' }); return; } diff --git a/src/app-secrets.js b/src/app-secrets.js index d95e94ea8..9cc608460 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -20,7 +20,7 @@ app.post('/', async (req, res) => { const { name, value } = req.body; if (method === 'openid') { - let canSaveSecrets = isAdmin(res.locals.user_id); + let canSaveSecrets = isAdmin(res.locals.session.user_id); if (!canSaveSecrets) { res.status(403).send({ diff --git a/src/app-sync.js b/src/app-sync.js index f97f859a5..5c7fa3d44 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -247,7 +247,7 @@ app.post('/upload-user-file', async (req, res) => { name: name, encryptMeta: encryptMeta, owner: - res.locals.user_id || + res.locals.session.user_id || (() => { throw new Error('User ID is required for file creation'); })(), @@ -310,7 +310,7 @@ app.post('/update-user-filename', (req, res) => { app.get('/list-user-files', (req, res) => { const fileService = new FilesService(getAccountDb()); - const rows = fileService.find({ userId: res.locals.user_id }); + const rows = fileService.find({ userId: res.locals.session.user_id }); res.send({ status: 'ok', data: rows.map((row) => ({ diff --git a/src/load-config.js b/src/load-config.js index 3327b7ded..abbde5a10 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -144,43 +144,35 @@ const finalConfig = { config.upload.fileSizeLimitMB, } : config.upload, - openId: - process.env.ACTUAL_OPENID_DISCOVERY_URL || - process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT - ? { - ...(process.env.ACTUAL_OPENID_DISCOVERY_URL - ? { - issuer: process.env.ACTUAL_OPENID_DISCOVERY_URL, - } - : process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT - ? { - issuer: { - name: process.env.ACTUAL_OPENID_PROVIDER_NAME, - authorization_endpoint: - process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, - token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, - userinfo_endpoint: - process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, - }, - } - : config.openId), - ...{ - client_id: process.env.ACTUAL_OPENID_CLIENT_ID - ? process.env.ACTUAL_OPENID_CLIENT_ID - : config.openId?.client_id, - }, - ...{ - client_secret: process.env.ACTUAL_OPENID_CLIENT_SECRET - ? process.env.ACTUAL_OPENID_CLIENT_SECRET - : config.openId?.client_secret, - }, - ...{ - server_hostname: process.env.ACTUAL_OPENID_SERVER_HOSTNAME - ? process.env.ACTUAL_OPENID_SERVER_HOSTNAME - : config.openId?.server_hostname, + openId: (() => { + if ( + !process.env.ACTUAL_OPENID_DISCOVERY_URL && + !process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT + ) { + return config.openId; + } + const baseConfig = process.env.ACTUAL_OPENID_DISCOVERY_URL + ? { issuer: process.env.ACTUAL_OPENID_DISCOVERY_URL } + : { + issuer: { + name: process.env.ACTUAL_OPENID_PROVIDER_NAME, + authorization_endpoint: + process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, + token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, + userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, }, - } - : config.openId, + }; + return { + ...baseConfig, + client_id: + process.env.ACTUAL_OPENID_CLIENT_ID ?? config.openId?.client_id, + client_secret: + process.env.ACTUAL_OPENID_CLIENT_SECRET ?? config.openId?.client_secret, + server_hostname: + process.env.ACTUAL_OPENID_SERVER_HOSTNAME ?? + config.openId?.server_hostname, + }; + })(), token_expiration: process.env.ACTUAL_TOKEN_EXPIRATION ? process.env.ACTUAL_TOKEN_EXPIRATION : config.token_expiration, diff --git a/src/util/middlewares.js b/src/util/middlewares.js index e606e9688..bf02e247d 100644 --- a/src/util/middlewares.js +++ b/src/util/middlewares.js @@ -40,9 +40,9 @@ async function errorMiddleware(err, req, res, next) { const validateSessionMiddleware = async (req, res, next) => { let session = await validateSession(req, res); if (!session) { - res.status(401).json({ + res.status(401).json({ status: 'error', - reason: 'invalid-session' + reason: 'invalid-session', }); return; } @@ -50,7 +50,6 @@ const validateSessionMiddleware = async (req, res, next) => { res.locals.session = session; next(); }; -}; const requestLoggerMiddleware = expressWinston.logger({ transports: [new winston.transports.Console()], diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 3481ad294..5d16273a2 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -4,6 +4,7 @@ import ipaddr from 'ipaddr.js'; import { getSession } from '../account-db.js'; export const TOKEN_EXPIRATION_NEVER = -1; +const MS_PER_SECOND = 1000; /** * @param {import('express').Request} req @@ -30,13 +31,14 @@ export default function validateSession(req, res) { if ( session.expires_at !== TOKEN_EXPIRATION_NEVER && - session.expires_at * 1000 <= Date.now() + session.expires_at * MS_PER_SECOND <= Date.now() ) { res.status(401); res.send({ status: 'error', reason: 'token-expired', - details: 'Token Expired. Login again', + details: 'token-expired', + message: 'Token expired. Please login again.', }); return null; } From 2a49e21c5d4b8b0cc50feb2c8a5204b41481a409 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 12:23:20 -0300 Subject: [PATCH 116/139] reverting res.locals and fixes --- jest.global-setup.js | 2 +- src/app-admin.js | 18 +++++++++--------- src/app-openid.js | 4 ++-- src/app-secrets.js | 2 +- src/app-sync.js | 8 +++++--- src/util/middlewares.js | 12 +----------- 6 files changed, 19 insertions(+), 27 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 075fc38bc..3694e8947 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -41,7 +41,7 @@ const setSessionUser = (userId, token = 'valid-token') => { try { const db = getAccountDb(); - const session = db.get('SELECT token FROM sessions WHERE token = ?', [ + const session = db.first('SELECT token FROM sessions WHERE token = ?', [ token, ]); if (!session) { diff --git a/src/app-admin.js b/src/app-admin.js index 27589d096..34891200a 100644 --- a/src/app-admin.js +++ b/src/app-admin.js @@ -37,7 +37,7 @@ app.get('/users/', validateSessionMiddleware, (req, res) => { }); app.post('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.session.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -89,7 +89,7 @@ app.post('/users', validateSessionMiddleware, async (req, res) => { }); app.patch('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.session.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -141,7 +141,7 @@ app.patch('/users', validateSessionMiddleware, async (req, res) => { }); app.delete('/users', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.session.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -191,8 +191,8 @@ app.get('/access', validateSessionMiddleware, (req, res) => { const accesses = UserService.getUserAccess( fileId, - res.locals.session.user_id, - isAdmin(res.locals.session.user_id), + res.locals.user_id, + isAdmin(res.locals.user_id), ); res.json(accesses); @@ -305,12 +305,12 @@ app.get('/access/users', validateSessionMiddleware, async (req, res) => { const { granted } = UserService.checkFilePermission( fileId, - res.locals.session.user_id, + res.locals.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(res.locals.session.user_id)) { + if (granted === 0 && !isAdmin(res.locals.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', @@ -341,12 +341,12 @@ app.post( const { granted } = UserService.checkFilePermission( newUserOwner.fileId, - res.locals.session.user_id, + res.locals.user_id, ) || { granted: 0, }; - if (granted === 0 && !isAdmin(res.locals.session.user_id)) { + if (granted === 0 && !isAdmin(res.locals.user_id)) { res.status(400).send({ status: 'error', reason: 'file-denied', diff --git a/src/app-openid.js b/src/app-openid.js index a4c9ce78c..045824190 100644 --- a/src/app-openid.js +++ b/src/app-openid.js @@ -18,7 +18,7 @@ app.use(requestLoggerMiddleware); export { app as handlers }; app.post('/enable', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.session.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', @@ -37,7 +37,7 @@ app.post('/enable', validateSessionMiddleware, async (req, res) => { }); app.post('/disable', validateSessionMiddleware, async (req, res) => { - if (!isAdmin(res.locals.session.user_id)) { + if (!isAdmin(res.locals.user_id)) { res.status(403).send({ status: 'error', reason: 'forbidden', diff --git a/src/app-secrets.js b/src/app-secrets.js index 9cc608460..d95e94ea8 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -20,7 +20,7 @@ app.post('/', async (req, res) => { const { name, value } = req.body; if (method === 'openid') { - let canSaveSecrets = isAdmin(res.locals.session.user_id); + let canSaveSecrets = isAdmin(res.locals.user_id); if (!canSaveSecrets) { res.status(403).send({ diff --git a/src/app-sync.js b/src/app-sync.js index 5c7fa3d44..92fdf3013 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -25,13 +25,13 @@ import { } from './app-sync/validation.js'; const app = express(); +app.use(validateSessionMiddleware); app.use(errorMiddleware); app.use(requestLoggerMiddleware); app.use(express.raw({ type: 'application/actual-sync' })); app.use(express.raw({ type: 'application/encrypted-file' })); app.use(express.json()); -app.use(validateSessionMiddleware); export { app as handlers }; const OK_RESPONSE = { status: 'ok' }; @@ -113,6 +113,8 @@ app.post('/sync', async (req, res) => { }); app.post('/user-get-key', (req, res) => { + if (!res.locals) return; + let { fileId } = req.body; const filesService = new FilesService(getAccountDb()); @@ -247,7 +249,7 @@ app.post('/upload-user-file', async (req, res) => { name: name, encryptMeta: encryptMeta, owner: - res.locals.session.user_id || + res.locals.user_id || (() => { throw new Error('User ID is required for file creation'); })(), @@ -310,7 +312,7 @@ app.post('/update-user-filename', (req, res) => { app.get('/list-user-files', (req, res) => { const fileService = new FilesService(getAccountDb()); - const rows = fileService.find({ userId: res.locals.session.user_id }); + const rows = fileService.find({ userId: res.locals.user_id }); res.send({ status: 'ok', data: rows.map((row) => ({ diff --git a/src/util/middlewares.js b/src/util/middlewares.js index bf02e247d..f8d6b4f18 100644 --- a/src/util/middlewares.js +++ b/src/util/middlewares.js @@ -31,23 +31,13 @@ async function errorMiddleware(err, req, res, next) { * @param {import('express').Response} res * @param {import('express').NextFunction} next */ -/** - * Middleware to validate session and attach it to response locals - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next - */ const validateSessionMiddleware = async (req, res, next) => { let session = await validateSession(req, res); if (!session) { - res.status(401).json({ - status: 'error', - reason: 'invalid-session', - }); return; } - res.locals.session = session; + res.locals = session; next(); }; From f0d45e3f2ad2e44f06669e435d52233d662730e6 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 13:32:15 -0300 Subject: [PATCH 117/139] Update src/accounts/openid.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/accounts/openid.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index a635594da..f33c7f23f 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -91,7 +91,8 @@ export async function loginWithOpenIdSetup(returnUrl) { try { client = await setupOpenIdClient(config); } catch (err) { - return { error: 'openid-setup-failed: ' + err }; + console.error('Error setting up OpenID client:', err); + return { error: 'openid-setup-failed' }; } const state = generators.state(); From f9c4175bae4790ddc54a0f6d5fc79be9d29d5519 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 13:34:20 -0300 Subject: [PATCH 118/139] Update src/load-config.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/load-config.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/load-config.js b/src/load-config.js index abbde5a10..0df11b227 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -154,6 +154,22 @@ const finalConfig = { const baseConfig = process.env.ACTUAL_OPENID_DISCOVERY_URL ? { issuer: process.env.ACTUAL_OPENID_DISCOVERY_URL } : { + ...(() => { + const required = { + authorization_endpoint: process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, + token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, + userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, + }; + const missing = Object.entries(required) + .filter(([_, value]) => !value) + .map(([key]) => key); + if (missing.length > 0) { + throw new Error( + `Missing required OpenID configuration: ${missing.join(', ')}` + ); + } + return {}; + })(), issuer: { name: process.env.ACTUAL_OPENID_PROVIDER_NAME, authorization_endpoint: From 81cda3fdd535d929a4661fe11d3cf53359ace100 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 13:37:09 -0300 Subject: [PATCH 119/139] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/load-config.js | 8 +++++++- src/services/user-service.js | 37 +++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/load-config.js b/src/load-config.js index 0df11b227..12b3beac0 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -108,7 +108,13 @@ const finalConfig = { ? process.env.ACTUAL_LOGIN_METHOD.toLowerCase() : config.loginMethod, multiuser: process.env.ACTUAL_MULTIUSER - ? process.env.ACTUAL_MULTIUSER.toLowerCase() === 'true' + ? (() => { + const value = process.env.ACTUAL_MULTIUSER.toLowerCase(); + if (!['true', 'false'].includes(value)) { + throw new Error('ACTUAL_MULTIUSER must be either "true" or "false"'); + } + return value === 'true'; + })() : config.multiuser, trustedProxies: process.env.ACTUAL_TRUSTED_PROXIES ? process.env.ACTUAL_TRUSTED_PROXIES.split(',').map((q) => q.trim()) diff --git a/src/services/user-service.js b/src/services/user-service.js index 39da767ac..34a05bd06 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -1,24 +1,33 @@ import getAccountDb from '../account-db.js'; export function getUserByUsername(userName) { + if (!userName || typeof userName !== 'string') { + return null; + } const { id } = getAccountDb().first('SELECT id FROM users WHERE user_name = ?', [ userName, ]) || {}; - return id; + return id || null; } export function getUserById(userId) { + if (!userId) { + return null; + } const { id } = - getAccountDb().first('SELECT id FROM users WHERE id = ?', [userId]) || {}; - return id; + getAccountDb().first('SELECT * FROM users WHERE id = ?', [userId]) || {}; + return id || null; } export function getFileById(fileId) { + if (!fileId) { + return null; + } const { id } = - getAccountDb().first('SELECT id FROM files WHERE files.id = ?', [fileId]) || + getAccountDb().first('SELECT * FROM files WHERE files.id = ?', [fileId]) || {}; - return id; + return id || null; } export function validateRole(roleId) { @@ -173,14 +182,20 @@ export function deleteUserAccessByFileId(userIds, fileId) { const CHUNK_SIZE = 999; let totalChanges = 0; - for (let i = 0; i < userIds.length; i += CHUNK_SIZE) { - const chunk = userIds.slice(i, i + CHUNK_SIZE); - const placeholders = chunk.map(() => '?').join(','); + try { + getAccountDb().transaction(() => { + for (let i = 0; i < userIds.length; i += CHUNK_SIZE) { + const chunk = userIds.slice(i, i + CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); - const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`; + const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`; - const result = getAccountDb().mutate(sql, [...chunk, fileId]); - totalChanges += result.changes; + const result = getAccountDb().mutate(sql, [...chunk, fileId]); + totalChanges += result.changes; + } + }); + } catch (error) { + throw new Error(`Failed to delete user access: ${error.message}`); } return totalChanges; From 83a40335a0514a5fdfe4362a4bbf29c088bf4d54 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 13:51:35 -0300 Subject: [PATCH 120/139] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/app-secrets.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app-secrets.js b/src/app-secrets.js index d95e94ea8..153670b87 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -14,9 +14,18 @@ app.use(requestLoggerMiddleware); app.use(validateSessionMiddleware); app.post('/', async (req, res) => { - const { method } = - getAccountDb().first('SELECT method FROM auth WHERE active = 1') || {}; - + let method; + try { + const result = getAccountDb().first('SELECT method FROM auth WHERE active = 1'); + method = result?.method; + } catch (error) { + console.error('Failed to fetch auth method:', error); + return res.status(500).send({ + status: 'error', + reason: 'database-error', + details: 'Failed to validate authentication method' + }); + } const { name, value } = req.body; if (method === 'openid') { From d71f81f770759a975db6a6753ff896beb30c837b Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 13:55:06 -0300 Subject: [PATCH 121/139] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/accounts/openid.js | 56 +++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index f33c7f23f..6693f78f6 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -30,16 +30,19 @@ export async function bootstrapOpenId(config) { } let accountDb = getAccountDb(); - accountDb.transaction(() => { - accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", - [JSON.stringify(config)], - ); - - console.log(accountDb.all('select * from auth')); - }); + try { + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); + }); + } catch (err) { + console.error('Error updating auth table:', err); + return { error: 'database-error' }; + } console.log(accountDb.all('select * from auth')); @@ -60,7 +63,7 @@ async function setupOpenIdClient(config) { const client = new issuer.Client({ client_id: config.client_id, client_secret: config.client_secret, - redirect_uri: config.server_hostname + '/openid/callback', + redirect_uri: new URL('/openid/callback', config.server_hostname).toString(), validate_id_token: true, }); @@ -180,22 +183,25 @@ export async function loginWithOpenIdFinalize(body) { return { error: 'openid-grant-failed: no identification was found' }; } - let { countUsersWithUserName } = accountDb.first( - 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', - [''], - ); let userId = null; - if (countUsersWithUserName === 0) { - userId = uuid.v4(); - accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', - [ - userId, - identity, - userInfo.name ?? userInfo.email ?? identity, - 'ADMIN', - ], + accountDb.transaction(() => { + let { countUsersWithUserName } = accountDb.first( + 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', + [''], ); + if (countUsersWithUserName === 0) { + userId = uuid.v4(); + accountDb.mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [ + userId, + identity, + userInfo.name ?? userInfo.email ?? identity, + 'ADMIN', + ], + ); + } + }); const userFromPasswordMethod = getUserByUsername(''); if (userFromPasswordMethod) { From c478da1d132250b7d0ebfa372b4a1faea3211e3e Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 13:59:30 -0300 Subject: [PATCH 122/139] Update src/account-db.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/account-db.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 90e68c7ff..3c095f04b 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -156,9 +156,17 @@ export async function disableOpenID( return { error }; } - getAccountDb().mutate('DELETE FROM sessions'); - getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?', ['']); - getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); + const accountDb = getAccountDb(); + accountDb.mutate('BEGIN TRANSACTION'); + try { + accountDb.mutate('DELETE FROM sessions'); + accountDb.mutate('DELETE FROM users WHERE user_name <> ?', ['']); + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('COMMIT'); + } catch (error) { + accountDb.mutate('ROLLBACK'); + throw error; + } } export function getSession(token) { From c595911f92e22069a1714a2f7412f5f9c496d688 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 14:03:11 -0300 Subject: [PATCH 123/139] fixes and refactories --- jest.global-setup.js | 29 ++++++++---- src/account-db.js | 27 ++++++------ src/accounts/openid.js | 4 -- src/app-sync.js | 2 - src/app-sync/services/files-service.js | 2 +- src/services/user-service.js | 61 ++++++++++++++++++++------ 6 files changed, 82 insertions(+), 43 deletions(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 3694e8947..66f63cb7b 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -59,21 +59,32 @@ const setSessionUser = (userId, token = 'valid-token') => { }; export default async function setup() { + const NEVER_EXPIRES = -1; // or consider using a far future timestamp + await runMigrations(); createUser(GENERIC_ADMIN_ID, 'admin', ADMIN_ROLE_ID, 1); // Insert a fake "valid-token" fixture that can be reused const db = getAccountDb(); - await db.mutate('DELETE FROM sessions'); - await db.mutate( - 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', - ['valid-token', -1, 'genericAdmin'], - ); - await db.mutate( - 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', - ['valid-token-admin', -1, 'genericAdmin'], - ); + try { + await db.mutate('BEGIN TRANSACTION'); + + await db.mutate('DELETE FROM sessions'); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token', NEVER_EXPIRES, 'genericAdmin'], + ); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token-admin', NEVER_EXPIRES, 'genericAdmin'], + ); + + await db.mutate('COMMIT'); + } catch (error) { + await db.mutate('ROLLBACK'); + throw new Error(`Failed to setup test sessions: ${error.message}`); + } setSessionUser('genericAdmin'); setSessionUser('genericAdmin', 'valid-token-admin'); diff --git a/src/account-db.js b/src/account-db.js index 90e68c7ff..e7faf76c8 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -138,16 +138,14 @@ export async function disableOpenID( return { error: 'invalid-password' }; } - if (!loginSettings?.password) { - return { error: 'invalid-password' }; - } - - if (passwordHash) { - let confirmed = bcrypt.compareSync(loginSettings.password, passwordHash); - - if (!confirmed) { - return { error: 'invalid-password' }; - } + const confirmed = + passwordHash && bcrypt.compareSync(loginSettings.password, passwordHash); + + if (!confirmed) { + return { + error: 'invalid-password', + message: 'The provided password is incorrect', + }; } } @@ -156,9 +154,12 @@ export async function disableOpenID( return { error }; } - getAccountDb().mutate('DELETE FROM sessions'); - getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?', ['']); - getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); + let accountDb = getAccountDb(); + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM sessions'); + accountDb.mutate('DELETE FROM users WHERE user_name <> ?', ['']); + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + }); } export function getSession(token) { diff --git a/src/accounts/openid.js b/src/accounts/openid.js index a635594da..500649b6a 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -37,12 +37,8 @@ export async function bootstrapOpenId(config) { "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", [JSON.stringify(config)], ); - - console.log(accountDb.all('select * from auth')); }); - console.log(accountDb.all('select * from auth')); - return {}; } diff --git a/src/app-sync.js b/src/app-sync.js index 92fdf3013..8e503c795 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -113,8 +113,6 @@ app.post('/sync', async (req, res) => { }); app.post('/user-get-key', (req, res) => { - if (!res.locals) return; - let { fileId } = req.body; const filesService = new FilesService(getAccountDb()); diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 8865b3254..26b327bdd 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -114,7 +114,7 @@ class FilesService { file.encrypt_test, file.encrypt_keyid, deletedInt, - file.userId, + file.owner, ], ); } diff --git a/src/services/user-service.js b/src/services/user-service.js index 39da767ac..c5f27ce1f 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -70,10 +70,17 @@ export function getAllUsers() { } export function insertUser(userId, userName, displayName, enabled, role) { - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, 0, ?)', - [userId, userName, displayName, enabled, role], - ); + if (!userId || !userName || !validateRole(role)) { + throw new Error('Invalid user parameters'); + } + try { + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, 0, ?)', + [userId, userName, displayName, enabled, role], + ); + } catch (error) { + throw new Error(`Failed to insert user: ${error.message}`); + } } export function updateUser(userId, userName, displayName, enabled) { @@ -90,12 +97,22 @@ export function updateUserWithRole( enabled, roleId, ) { - getAccountDb().transaction(() => { - getAccountDb().mutate( - 'UPDATE users SET user_name = ?, display_name = ?, enabled = ?, role = ? WHERE id = ?', - [userName, displayName, enabled, roleId, userId], - ); - }); + if (!userId || !userName || !validateRole(roleId)) { + throw new Error('Invalid user parameters'); + } + try { + getAccountDb().transaction(() => { + const result = getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ?, role = ? WHERE id = ?', + [userName, displayName, enabled, roleId, userId], + ); + if (result.changes === 0) { + throw new Error('User not found'); + } + }); + } catch (error) { + throw new Error(`Failed to update user: ${error.message}`); + } } export function deleteUser(userId) { @@ -108,10 +125,26 @@ export function deleteUserAccess(userId) { } export function transferAllFilesFromUser(ownerId, oldUserId) { - getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ - ownerId, - oldUserId, - ]); + if (!ownerId || !oldUserId) { + throw new Error('Invalid user IDs'); + } + try { + getAccountDb().transaction(() => { + const ownerExists = getUserById(ownerId); + if (!ownerExists) { + throw new Error('New owner not found'); + } + const result = getAccountDb().mutate( + 'UPDATE files set owner = ? WHERE owner = ?', + [ownerId, oldUserId], + ); + if (result.changes === 0) { + throw new Error('No files found for transfer'); + } + }); + } catch (error) { + throw new Error(`Failed to transfer files: ${error.message}`); + } } export function updateFileOwner(ownerId, fileId) { From bb75c51cd68a1a5cbd61a4818cfbb66ca765c185 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 14:32:18 -0300 Subject: [PATCH 124/139] merge fix --- src/account-db.js | 27 ++++++------ src/accounts/openid.js | 23 +++++----- src/app-sync.js | 2 + src/app-sync/services/files-service.js | 2 +- src/services/user-service.js | 61 ++++++-------------------- 5 files changed, 40 insertions(+), 75 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index e7faf76c8..90e68c7ff 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -138,14 +138,16 @@ export async function disableOpenID( return { error: 'invalid-password' }; } - const confirmed = - passwordHash && bcrypt.compareSync(loginSettings.password, passwordHash); - - if (!confirmed) { - return { - error: 'invalid-password', - message: 'The provided password is incorrect', - }; + if (!loginSettings?.password) { + return { error: 'invalid-password' }; + } + + if (passwordHash) { + let confirmed = bcrypt.compareSync(loginSettings.password, passwordHash); + + if (!confirmed) { + return { error: 'invalid-password' }; + } } } @@ -154,12 +156,9 @@ export async function disableOpenID( return { error }; } - let accountDb = getAccountDb(); - accountDb.transaction(() => { - accountDb.mutate('DELETE FROM sessions'); - accountDb.mutate('DELETE FROM users WHERE user_name <> ?', ['']); - accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); - }); + getAccountDb().mutate('DELETE FROM sessions'); + getAccountDb().mutate('DELETE FROM users WHERE user_name <> ?', ['']); + getAccountDb().mutate('DELETE FROM auth WHERE method = ?', ['openid']); } export function getSession(token) { diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 6693f78f6..4e569cc49 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -30,19 +30,16 @@ export async function bootstrapOpenId(config) { } let accountDb = getAccountDb(); - try { - accountDb.transaction(() => { - accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", - [JSON.stringify(config)], - ); - }); - } catch (err) { - console.error('Error updating auth table:', err); - return { error: 'database-error' }; - } + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); + + console.log(accountDb.all('select * from auth')); + }); console.log(accountDb.all('select * from auth')); diff --git a/src/app-sync.js b/src/app-sync.js index 8e503c795..92fdf3013 100644 --- a/src/app-sync.js +++ b/src/app-sync.js @@ -113,6 +113,8 @@ app.post('/sync', async (req, res) => { }); app.post('/user-get-key', (req, res) => { + if (!res.locals) return; + let { fileId } = req.body; const filesService = new FilesService(getAccountDb()); diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 26b327bdd..8865b3254 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -114,7 +114,7 @@ class FilesService { file.encrypt_test, file.encrypt_keyid, deletedInt, - file.owner, + file.userId, ], ); } diff --git a/src/services/user-service.js b/src/services/user-service.js index 45715f7f0..34a05bd06 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -79,17 +79,10 @@ export function getAllUsers() { } export function insertUser(userId, userName, displayName, enabled, role) { - if (!userId || !userName || !validateRole(role)) { - throw new Error('Invalid user parameters'); - } - try { - getAccountDb().mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, 0, ?)', - [userId, userName, displayName, enabled, role], - ); - } catch (error) { - throw new Error(`Failed to insert user: ${error.message}`); - } + getAccountDb().mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, ?, 0, ?)', + [userId, userName, displayName, enabled, role], + ); } export function updateUser(userId, userName, displayName, enabled) { @@ -106,22 +99,12 @@ export function updateUserWithRole( enabled, roleId, ) { - if (!userId || !userName || !validateRole(roleId)) { - throw new Error('Invalid user parameters'); - } - try { - getAccountDb().transaction(() => { - const result = getAccountDb().mutate( - 'UPDATE users SET user_name = ?, display_name = ?, enabled = ?, role = ? WHERE id = ?', - [userName, displayName, enabled, roleId, userId], - ); - if (result.changes === 0) { - throw new Error('User not found'); - } - }); - } catch (error) { - throw new Error(`Failed to update user: ${error.message}`); - } + getAccountDb().transaction(() => { + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ?, role = ? WHERE id = ?', + [userName, displayName, enabled, roleId, userId], + ); + }); } export function deleteUser(userId) { @@ -134,26 +117,10 @@ export function deleteUserAccess(userId) { } export function transferAllFilesFromUser(ownerId, oldUserId) { - if (!ownerId || !oldUserId) { - throw new Error('Invalid user IDs'); - } - try { - getAccountDb().transaction(() => { - const ownerExists = getUserById(ownerId); - if (!ownerExists) { - throw new Error('New owner not found'); - } - const result = getAccountDb().mutate( - 'UPDATE files set owner = ? WHERE owner = ?', - [ownerId, oldUserId], - ); - if (result.changes === 0) { - throw new Error('No files found for transfer'); - } - }); - } catch (error) { - throw new Error(`Failed to transfer files: ${error.message}`); - } + getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ + ownerId, + oldUserId, + ]); } export function updateFileOwner(ownerId, fileId) { From f19ab546c04800f99fdac5c275dd687bec9e57df Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 14:34:44 -0300 Subject: [PATCH 125/139] fix --- src/accounts/openid.js | 52 ++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 4e569cc49..c79ce4340 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -179,6 +179,8 @@ export async function loginWithOpenIdFinalize(body) { if (identity == null) { return { error: 'openid-grant-failed: no identification was found' }; } + + let userId = null; accountDb.transaction(() => { @@ -186,6 +188,7 @@ export async function loginWithOpenIdFinalize(body) { 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', [''], ); + let userId = null; if (countUsersWithUserName === 0) { userId = uuid.v4(); accountDb.mutate( @@ -197,34 +200,33 @@ export async function loginWithOpenIdFinalize(body) { 'ADMIN', ], ); + + const userFromPasswordMethod = getUserByUsername(''); + if (userFromPasswordMethod) { + transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); + } + } else { + let { id: userIdFromDb, display_name: displayName } = + accountDb.first( + 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', + [identity], + ) || {}; + + if (userIdFromDb == null) { + return { error: 'openid-grant-failed' }; + } + + if (!displayName && userInfo.name) { + accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ + userInfo.name, + userIdFromDb, + ]); + } + + userId = userIdFromDb; } }); - const userFromPasswordMethod = getUserByUsername(''); - if (userFromPasswordMethod) { - transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); - } - } else { - let { id: userIdFromDb, display_name: displayName } = - accountDb.first( - 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', - [identity], - ) || {}; - - if (userIdFromDb == null) { - return { error: 'openid-grant-failed' }; - } - - if (!displayName && userInfo.name) { - accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ - userInfo.name, - userIdFromDb, - ]); - } - - userId = userIdFromDb; - } - const token = uuid.v4(); let expiration; From 1d58228649d82e8ef47acbc0e734710e66e957a4 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 14:36:02 -0300 Subject: [PATCH 126/139] merge fix --- src/accounts/openid.js | 38 +++++++++++++++++++++----------------- src/app-secrets.js | 6 ++++-- src/load-config.js | 5 +++-- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index c79ce4340..5dc67d30a 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -30,18 +30,19 @@ export async function bootstrapOpenId(config) { } let accountDb = getAccountDb(); - accountDb.transaction(() => { - accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); - accountDb.mutate('UPDATE auth SET active = 0'); - accountDb.mutate( - "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", - [JSON.stringify(config)], - ); - - console.log(accountDb.all('select * from auth')); - }); - - console.log(accountDb.all('select * from auth')); + try { + accountDb.transaction(() => { + accountDb.mutate('DELETE FROM auth WHERE method = ?', ['openid']); + accountDb.mutate('UPDATE auth SET active = 0'); + accountDb.mutate( + "INSERT INTO auth (method, display_name, extra_data, active) VALUES ('openid', 'OpenID', ?, 1)", + [JSON.stringify(config)], + ); + }); + } catch (err) { + console.error('Error updating auth table:', err); + return { error: 'database-error' }; + } return {}; } @@ -60,7 +61,10 @@ async function setupOpenIdClient(config) { const client = new issuer.Client({ client_id: config.client_id, client_secret: config.client_secret, - redirect_uri: new URL('/openid/callback', config.server_hostname).toString(), + redirect_uri: new URL( + '/openid/callback', + config.server_hostname, + ).toString(), validate_id_token: true, }); @@ -200,7 +204,7 @@ export async function loginWithOpenIdFinalize(body) { 'ADMIN', ], ); - + const userFromPasswordMethod = getUserByUsername(''); if (userFromPasswordMethod) { transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); @@ -211,18 +215,18 @@ export async function loginWithOpenIdFinalize(body) { 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', [identity], ) || {}; - + if (userIdFromDb == null) { return { error: 'openid-grant-failed' }; } - + if (!displayName && userInfo.name) { accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ userInfo.name, userIdFromDb, ]); } - + userId = userIdFromDb; } }); diff --git a/src/app-secrets.js b/src/app-secrets.js index 153670b87..3c06bf8a0 100644 --- a/src/app-secrets.js +++ b/src/app-secrets.js @@ -16,14 +16,16 @@ app.use(validateSessionMiddleware); app.post('/', async (req, res) => { let method; try { - const result = getAccountDb().first('SELECT method FROM auth WHERE active = 1'); + const result = getAccountDb().first( + 'SELECT method FROM auth WHERE active = 1', + ); method = result?.method; } catch (error) { console.error('Failed to fetch auth method:', error); return res.status(500).send({ status: 'error', reason: 'database-error', - details: 'Failed to validate authentication method' + details: 'Failed to validate authentication method', }); } const { name, value } = req.body; diff --git a/src/load-config.js b/src/load-config.js index 12b3beac0..202bea5a9 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -162,7 +162,8 @@ const finalConfig = { : { ...(() => { const required = { - authorization_endpoint: process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, + authorization_endpoint: + process.env.ACTUAL_OPENID_AUTHORIZATION_ENDPOINT, token_endpoint: process.env.ACTUAL_OPENID_TOKEN_ENDPOINT, userinfo_endpoint: process.env.ACTUAL_OPENID_USERINFO_ENDPOINT, }; @@ -171,7 +172,7 @@ const finalConfig = { .map(([key]) => key); if (missing.length > 0) { throw new Error( - `Missing required OpenID configuration: ${missing.join(', ')}` + `Missing required OpenID configuration: ${missing.join(', ')}`, ); } return {}; From 436225445eeb51c85ff1eba1f9783a0571eba75a Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 14:36:12 -0300 Subject: [PATCH 127/139] linter --- src/accounts/openid.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index 5dc67d30a..c71917b5f 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -183,8 +183,6 @@ export async function loginWithOpenIdFinalize(body) { if (identity == null) { return { error: 'openid-grant-failed: no identification was found' }; } - - let userId = null; accountDb.transaction(() => { From 8d563586275b2f2af08300f42035299d8ee2f968 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 14:53:49 -0300 Subject: [PATCH 128/139] Update src/accounts/openid.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/accounts/openid.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index c71917b5f..62fd46c55 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -285,7 +285,8 @@ export function isValidRedirectUrl(url) { const redirectUrl = new URL(url); const serverUrl = new URL(serverHostname); - if (redirectUrl.hostname === serverUrl.hostname) { + // Compare origin (protocol + hostname + port) + if (redirectUrl.origin === serverUrl.origin) { return true; } else { return false; From bf473aa17090e72c05a196269bbb472793399caf Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 14:54:38 -0300 Subject: [PATCH 129/139] Update src/services/user-service.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/services/user-service.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index 34a05bd06..59a008916 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -124,10 +124,20 @@ export function transferAllFilesFromUser(ownerId, oldUserId) { } export function updateFileOwner(ownerId, fileId) { - getAccountDb().mutate('UPDATE files set owner = ? WHERE id = ?', [ - ownerId, - fileId, - ]); + if (!ownerId || !fileId) { + throw new Error('Invalid parameters'); + } + try { + const result = getAccountDb().mutate('UPDATE files set owner = ? WHERE id = ?', [ + ownerId, + fileId, + ]); + if (result.changes === 0) { + throw new Error('File not found'); + } + } catch (error) { + throw new Error(`Failed to update file owner: ${error.message}`); + } } export function getUserAccess(fileId, userId, isAdmin) { From da16b196389eebc3868e2089b4bd830b52391374 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 14:54:54 -0300 Subject: [PATCH 130/139] Update src/services/user-service.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/services/user-service.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index 59a008916..edd025bc7 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -86,10 +86,17 @@ export function insertUser(userId, userName, displayName, enabled, role) { } export function updateUser(userId, userName, displayName, enabled) { - getAccountDb().mutate( - 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', - [userName, displayName, enabled, userId], - ); + if (!userId || !userName) { + throw new Error('Invalid user parameters'); + } + try { + getAccountDb().mutate( + 'UPDATE users SET user_name = ?, display_name = ?, enabled = ? WHERE id = ?', + [userName, displayName, enabled, userId], + ); + } catch (error) { + throw new Error(`Failed to update user: ${error.message}`); + } } export function updateUserWithRole( From 3fd8546d8c8d4a9a391ef0787972ea2537ab97ef Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 11 Nov 2024 14:55:17 -0300 Subject: [PATCH 131/139] Update src/services/user-service.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/services/user-service.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index edd025bc7..a37eeef8e 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -120,7 +120,14 @@ export function deleteUser(userId) { ]).changes; } export function deleteUserAccess(userId) { - getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [userId]); + try { + return getAccountDb().mutate( + 'DELETE FROM user_access WHERE user_id = ?', + [userId], + ).changes; + } catch (error) { + throw new Error(`Failed to delete user access: ${error.message}`); + } } export function transferAllFilesFromUser(ownerId, oldUserId) { From 5987c08b4e5eb3b43bdf3dcfc65483477083083f Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 15:00:29 -0300 Subject: [PATCH 132/139] more code review --- src/account-db.js | 63 ++++++++++++++++---------- src/accounts/openid.js | 2 - src/app-sync/services/files-service.js | 2 + src/services/user-service.js | 23 ++++++++-- src/util/validate-user.js | 2 - 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index 90e68c7ff..abd276661 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -56,42 +56,55 @@ export async function bootstrap(loginSettings) { const passEnabled = 'password' in loginSettings; const openIdEnabled = 'openId' in loginSettings; - const { countOfOwner } = - getAccountDb().first( - `SELECT count(*) as countOfOwner + const accountDb = getAccountDb(); + accountDb.mutate('BEGIN TRANSACTION'); + try { + const { countOfOwner } = + accountDb.first( + `SELECT count(*) as countOfOwner FROM users WHERE users.user_name <> '' and users.owner = 1`, - ) || {}; + ) || {}; - if (!openIdEnabled || countOfOwner > 0) { - if (!needsBootstrap()) { - return { error: 'already-bootstrapped' }; + if (!openIdEnabled || countOfOwner > 0) { + if (!needsBootstrap()) { + accountDb.mutate('ROLLBACK'); + return { error: 'already-bootstrapped' }; + } } - } - if (!passEnabled && !openIdEnabled) { - return { error: 'no-auth-method-selected' }; - } + if (!passEnabled && !openIdEnabled) { + accountDb.mutate('ROLLBACK'); + return { error: 'no-auth-method-selected' }; + } - if (passEnabled && openIdEnabled) { - return { error: 'max-one-method-allowed' }; - } + if (passEnabled && openIdEnabled) { + accountDb.mutate('ROLLBACK'); + return { error: 'max-one-method-allowed' }; + } - if (passEnabled) { - let { error } = bootstrapPassword(loginSettings.password); - if (error) { - return { error }; + if (passEnabled) { + let { error } = bootstrapPassword(loginSettings.password); + if (error) { + accountDb.mutate('ROLLBACK'); + return { error }; + } } - } - if (openIdEnabled) { - let { error } = await bootstrapOpenId(loginSettings.openId); - if (error) { - return { error }; + if (openIdEnabled) { + let { error } = await bootstrapOpenId(loginSettings.openId); + if (error) { + accountDb.mutate('ROLLBACK'); + return { error }; + } } - } - return {}; + accountDb.mutate('COMMIT'); + return {}; + } catch (error) { + accountDb.mutate('ROLLBACK'); + throw error; + } } export function isAdmin(userId) { diff --git a/src/accounts/openid.js b/src/accounts/openid.js index c71917b5f..ce5c378de 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -190,7 +190,6 @@ export async function loginWithOpenIdFinalize(body) { 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', [''], ); - let userId = null; if (countUsersWithUserName === 0) { userId = uuid.v4(); accountDb.mutate( @@ -252,7 +251,6 @@ export async function loginWithOpenIdFinalize(body) { return { url: `${return_url}/openid-cb?token=${token}` }; } catch (err) { - console.error('OpenID grant failed:', err); console.error('OpenID grant failed:', err); return { error: 'openid-grant-failed' }; } diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 8865b3254..9ecb4b1b8 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -68,6 +68,7 @@ class FileUpdate extends FileBase { encryptMeta = undefined, syncVersion = undefined, deleted = undefined, + owner = undefined, }) { super( name, @@ -78,6 +79,7 @@ class FileUpdate extends FileBase { encryptMeta, syncVersion, deleted, + owner, ); } } diff --git a/src/services/user-service.js b/src/services/user-service.js index 34a05bd06..a18bafdff 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -168,10 +168,25 @@ export function checkFilePermission(fileId, userId) { } export function addUserAccess(userId, fileId) { - getAccountDb().mutate( - 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', - [userId, fileId], - ); + if (!userId || !fileId) { + throw new Error('Invalid parameters'); + } + try { + const userExists = getUserById(userId); + const fileExists = getFileById(fileId); + if (!userExists || !fileExists) { + throw new Error('User or file not found'); + } + getAccountDb().mutate( + 'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)', + [userId, fileId], + ); + } catch (error) { + if (error.message.includes('UNIQUE constraint')) { + throw new Error('Access already exists'); + } + throw new Error(`Failed to add user access: ${error.message}`); + } } export function deleteUserAccessByFileId(userIds, fileId) { diff --git a/src/util/validate-user.js b/src/util/validate-user.js index 5d16273a2..a84389e6f 100644 --- a/src/util/validate-user.js +++ b/src/util/validate-user.js @@ -37,8 +37,6 @@ export default function validateSession(req, res) { res.send({ status: 'error', reason: 'token-expired', - details: 'token-expired', - message: 'Token expired. Please login again.', }); return null; } From cbf217e8ff884790091425abb94692fd5c0507ed Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 15:01:45 -0300 Subject: [PATCH 133/139] linter accepted code --- src/services/user-service.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/services/user-service.js b/src/services/user-service.js index c17b8c7c4..a6a65b39c 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -121,10 +121,9 @@ export function deleteUser(userId) { } export function deleteUserAccess(userId) { try { - return getAccountDb().mutate( - 'DELETE FROM user_access WHERE user_id = ?', - [userId], - ).changes; + return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [ + userId, + ]).changes; } catch (error) { throw new Error(`Failed to delete user access: ${error.message}`); } @@ -142,10 +141,10 @@ export function updateFileOwner(ownerId, fileId) { throw new Error('Invalid parameters'); } try { - const result = getAccountDb().mutate('UPDATE files set owner = ? WHERE id = ?', [ - ownerId, - fileId, - ]); + const result = getAccountDb().mutate( + 'UPDATE files set owner = ? WHERE id = ?', + [ownerId, fileId], + ); if (result.changes === 0) { throw new Error('File not found'); } From 30610fdfbf0065a3e3457103eb73cc6659ddc343 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 15:36:09 -0300 Subject: [PATCH 134/139] typo --- src/app-sync/services/files-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 9ecb4b1b8..5f32a2523 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -116,7 +116,7 @@ class FilesService { file.encrypt_test, file.encrypt_keyid, deletedInt, - file.userId, + file.owner, ], ); } From 3c5e12e273c627e7a90deb86a5ece8a1b0e38b7d Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 11 Nov 2024 16:06:41 -0300 Subject: [PATCH 135/139] code review suggestion --- src/accounts/openid.js | 100 ++++++++++++++++++++++------------- src/services/user-service.js | 21 ++++++-- 2 files changed, 79 insertions(+), 42 deletions(-) diff --git a/src/accounts/openid.js b/src/accounts/openid.js index a736a187f..61ea96ca5 100644 --- a/src/accounts/openid.js +++ b/src/accounts/openid.js @@ -75,6 +75,9 @@ export async function loginWithOpenIdSetup(returnUrl) { if (!returnUrl) { return { error: 'return-url-missing' }; } + if (!isValidRedirectUrl(returnUrl)) { + return { error: 'invalid-return-url' }; + } let accountDb = getAccountDb(); let config = accountDb.first('SELECT extra_data FROM auth WHERE method = ?', [ @@ -185,48 +188,69 @@ export async function loginWithOpenIdFinalize(body) { } let userId = null; - accountDb.transaction(() => { - let { countUsersWithUserName } = accountDb.first( - 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', - [''], - ); - if (countUsersWithUserName === 0) { - userId = uuid.v4(); - accountDb.mutate( - 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', - [ - userId, - identity, - userInfo.name ?? userInfo.email ?? identity, - 'ADMIN', - ], - ); + try { + accountDb.transaction(() => { + // Lock the users table to prevent race conditions + accountDb.mutate('BEGIN EXCLUSIVE TRANSACTION'); - const userFromPasswordMethod = getUserByUsername(''); - if (userFromPasswordMethod) { - transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); - } - } else { - let { id: userIdFromDb, display_name: displayName } = - accountDb.first( - 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', + let { countUsersWithUserName } = accountDb.first( + 'SELECT count(*) as countUsersWithUserName FROM users WHERE user_name <> ?', + [''], + ); + if (countUsersWithUserName === 0) { + userId = uuid.v4(); + // Check if user was created by another transaction + const existingUser = accountDb.first( + 'SELECT id FROM users WHERE user_name = ?', [identity], - ) || {}; - - if (userIdFromDb == null) { - return { error: 'openid-grant-failed' }; - } - - if (!displayName && userInfo.name) { - accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ - userInfo.name, - userIdFromDb, - ]); + ); + if (existingUser) { + throw new Error('user-already-exists'); + } + accountDb.mutate( + 'INSERT INTO users (id, user_name, display_name, enabled, owner, role) VALUES (?, ?, ?, 1, 1, ?)', + [ + userId, + identity, + userInfo.name ?? userInfo.email ?? identity, + 'ADMIN', + ], + ); + + const userFromPasswordMethod = getUserByUsername(''); + if (userFromPasswordMethod) { + transferAllFilesFromUser(userId, userFromPasswordMethod.user_id); + } + } else { + let { id: userIdFromDb, display_name: displayName } = + accountDb.first( + 'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1', + [identity], + ) || {}; + + if (userIdFromDb == null) { + throw new Error('openid-grant-failed'); + } + + if (!displayName && userInfo.name) { + accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [ + userInfo.name, + userIdFromDb, + ]); + } + + userId = userIdFromDb; } - - userId = userIdFromDb; + }); + } catch (error) { + if (error.message === 'user-already-exists') { + return { error: 'user-already-exists' }; + } else if (error.message === 'openid-grant-failed') { + return { error: 'openid-grant-failed' }; + } else { + throw error; // Re-throw other unexpected errors } - }); + } const token = uuid.v4(); diff --git a/src/services/user-service.js b/src/services/user-service.js index a6a65b39c..fbef95107 100644 --- a/src/services/user-service.js +++ b/src/services/user-service.js @@ -130,10 +130,23 @@ export function deleteUserAccess(userId) { } export function transferAllFilesFromUser(ownerId, oldUserId) { - getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ - ownerId, - oldUserId, - ]); + if (!ownerId || !oldUserId) { + throw new Error('Invalid user IDs'); + } + try { + getAccountDb().transaction(() => { + const ownerExists = getUserById(ownerId); + if (!ownerExists) { + throw new Error('New owner not found'); + } + getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [ + ownerId, + oldUserId, + ]); + }); + } catch (error) { + throw new Error(`Failed to transfer files: ${error.message}`); + } } export function updateFileOwner(ownerId, fileId) { From f0656b0a679d4fdf9186852e955c0b8120d369a6 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Thu, 14 Nov 2024 14:29:14 -0300 Subject: [PATCH 136/139] change to enable backward compatibility --- src/account-db.js | 4 ++-- src/app-account.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/account-db.js b/src/account-db.js index abd276661..e745779db 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -2,7 +2,7 @@ import { join } from 'node:path'; import openDatabase from './db.js'; import config from './load-config.js'; import * as bcrypt from 'bcrypt'; -import { bootstrapPassword } from './accounts/password.js'; +import { bootstrapPassword, loginWithPassword } from './accounts/password.js'; import { bootstrapOpenId } from './accounts/openid.js'; let _accountDb; @@ -100,7 +100,7 @@ export async function bootstrap(loginSettings) { } accountDb.mutate('COMMIT'); - return {}; + return passEnabled ? loginWithPassword(loginSettings.password) : {}; } catch (error) { accountDb.mutate('ROLLBACK'); throw error; diff --git a/src/app-account.js b/src/app-account.js index abf577407..0edec9e03 100644 --- a/src/app-account.js +++ b/src/app-account.js @@ -40,13 +40,13 @@ app.get('/needs-bootstrap', (req, res) => { }); app.post('/bootstrap', async (req, res) => { - let { error } = await bootstrap(req.body); + let boot = await bootstrap(req.body); - if (error) { - res.status(400).send({ status: 'error', reason: error }); + if (boot?.error) { + res.status(400).send({ status: 'error', reason: boot?.error }); return; } - res.send({ status: 'ok' }); + res.send({ status: 'ok', data: boot }); }); app.get('/login-methods', (req, res) => { From 10eee0b97a3bf2842bec0752c4c5274ecdb96146 Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 18 Nov 2024 11:23:20 -0300 Subject: [PATCH 137/139] removed the userId = null --- src/app-sync/services/files-service.js | 4 ++-- src/app-sync/tests/services/files-service.test.js | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app-sync/services/files-service.js b/src/app-sync/services/files-service.js index 5f32a2523..a01ba4172 100644 --- a/src/app-sync/services/files-service.js +++ b/src/app-sync/services/files-service.js @@ -121,8 +121,8 @@ class FilesService { ); } - find({ userId = null, limit = 1000 } = {}) { - const canSeeAll = userId === null || isAdmin(userId); + find({ userId, limit = 1000 }) { + const canSeeAll = isAdmin(userId); return ( canSeeAll diff --git a/src/app-sync/tests/services/files-service.test.js b/src/app-sync/tests/services/files-service.test.js index 4a72bc6e8..984af5624 100644 --- a/src/app-sync/tests/services/files-service.test.js +++ b/src/app-sync/tests/services/files-service.test.js @@ -124,7 +124,7 @@ describe('FilesService', () => { ); test('find should return a list of files', () => { - const files = filesService.find(); + const files = filesService.find({ userId: 'genericAdmin' }); expect(files.length).toBe(1); expect(files[0]).toEqual( new File({ @@ -153,11 +153,14 @@ describe('FilesService', () => { }), ); // Make sure that the file was inserted - const allFiles = filesService.find(); + const allFiles = filesService.find({ userId: 'genericAdmin' }); expect(allFiles.length).toBe(2); // Limit the number of files returned - const limitedFiles = filesService.find({ limit: 1 }); + const limitedFiles = filesService.find({ + userId: 'genericAdmin', + limit: 1, + }); expect(limitedFiles.length).toBe(1); }); From e9bf0455314b926408ffcca5dd147a59398ded6e Mon Sep 17 00:00:00 2001 From: Leandro Menezes Date: Mon, 18 Nov 2024 12:30:33 -0300 Subject: [PATCH 138/139] fixes from code review --- jest.global-setup.js | 9 ++++++ src/account-db.js | 4 --- .../tests/services/files-service.test.js | 31 +++++++++++++++++++ src/load-config.js | 2 +- types/global.d.ts | 0 5 files changed, 41 insertions(+), 5 deletions(-) delete mode 100644 types/global.d.ts diff --git a/jest.global-setup.js b/jest.global-setup.js index 66f63cb7b..6b72e3834 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -2,7 +2,9 @@ import getAccountDb from './src/account-db.js'; import runMigrations from './src/migrations.js'; const GENERIC_ADMIN_ID = 'genericAdmin'; +const GENERIC_USER_ID = 'genericUser'; const ADMIN_ROLE_ID = 'ADMIN'; +const BASIC_ROLE_ID = 'BASIC'; const createUser = (userId, userName, role, owner = 0, enabled = 1) => { const missingParams = []; @@ -80,6 +82,11 @@ export default async function setup() { ['valid-token-admin', NEVER_EXPIRES, 'genericAdmin'], ); + await db.mutate( + 'INSERT INTO sessions (token, expires_at, user_id) VALUES (?, ?, ?)', + ['valid-token-user', NEVER_EXPIRES, 'genericUser'], + ); + await db.mutate('COMMIT'); } catch (error) { await db.mutate('ROLLBACK'); @@ -88,4 +95,6 @@ export default async function setup() { setSessionUser('genericAdmin'); setSessionUser('genericAdmin', 'valid-token-admin'); + + createUser(GENERIC_USER_ID, 'user', BASIC_ROLE_ID, 1); } diff --git a/src/account-db.js b/src/account-db.js index e745779db..2c735247f 100644 --- a/src/account-db.js +++ b/src/account-db.js @@ -108,10 +108,6 @@ export async function bootstrap(loginSettings) { } export function isAdmin(userId) { - const user = - getAccountDb().first('SELECT owner FROM users WHERE id = ?', [userId]) || - {}; - if (user?.owner === 1) return true; return hasPermission(userId, 'ADMIN'); } diff --git a/src/app-sync/tests/services/files-service.test.js b/src/app-sync/tests/services/files-service.test.js index 984af5624..9807d99a0 100644 --- a/src/app-sync/tests/services/files-service.test.js +++ b/src/app-sync/tests/services/files-service.test.js @@ -192,6 +192,37 @@ describe('FilesService', () => { ); }); + test('find should return only files accessible to the user', () => { + filesService.set( + new File({ + id: crypto.randomBytes(16).toString('hex'), + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + deleted: false, + owner: 'genericAdmin', + }), + ); + + filesService.set( + new File({ + id: crypto.randomBytes(16).toString('hex'), + groupId: 'group2', + syncVersion: 1, + name: 'file2', + encryptMeta: '{"key":"value2"}', + deleted: false, + owner: 'genericUser', + }), + ); + + expect(filesService.find({ userId: 'genericUser' })).toHaveLength(1); + expect( + filesService.find({ userId: 'genericAdmin' }).length, + ).toBeGreaterThan(1); + }); + test.each([['update-group', null]])( 'update should modify a single attribute with groupId = $groupId', (newGroupId) => { diff --git a/src/load-config.js b/src/load-config.js index 202bea5a9..0f1bfff19 100644 --- a/src/load-config.js +++ b/src/load-config.js @@ -53,6 +53,7 @@ if (process.env.ACTUAL_CONFIG_PATH) { /** @type {Omit} */ let defaultConfig = { + loginMethod: 'password', // assume local networks are trusted for header authentication trustedProxies: [ '10.0.0.0/8', @@ -78,7 +79,6 @@ let defaultConfig = { projectRoot, multiuser: false, token_expiration: 'never', - loginMethod: 'password', }; /** @type {import('./config-types.js').Config} */ diff --git a/types/global.d.ts b/types/global.d.ts deleted file mode 100644 index e69de29bb..000000000 From 557b488e0a9c20ac0ede03123c53bed9d68c4e78 Mon Sep 17 00:00:00 2001 From: lelemm Date: Mon, 18 Nov 2024 14:46:36 -0300 Subject: [PATCH 139/139] Update jest.global-setup.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- jest.global-setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.global-setup.js b/jest.global-setup.js index 6b72e3834..524054dd5 100644 --- a/jest.global-setup.js +++ b/jest.global-setup.js @@ -50,7 +50,7 @@ const setSessionUser = (userId, token = 'valid-token') => { throw new Error(`Session not found for token: ${token}`); } - getAccountDb().mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ + db.mutate('UPDATE sessions SET user_id = ? WHERE token = ?', [ userId, token, ]);